@ecodrix/erix-api

中文文档
4.3.2发布于 2026/11/13MITTypeScriptESM + CJS近 30 天下载 2194

Official TypeScript SDK for the ERIX API by ECODrIx: CRM, Flow automations, one inbox across WhatsApp, Instagram and email, GST invoices and payments. Node and browser (proxy mode).

原始使用文档
文档目录18 节
  1. Install
  2. Quick start
  3. How the client is laid out
  4. Browser use (proxy mode)
  5. Website chat visitor client
  6. Scoped keys
  7. CRM: leads and pipelines
  8. Flow: events and custom triggers
  9. Channels: inbox and direct send
  10. Invoices and payment links
  11. Storage uploads
  12. Realtime events
  13. Webhook signature verification
  14. Raw requests
  15. Errors
  16. TypeScript
  17. Versioning
  18. License

@ecodrix/erix-api

npm License: MIT

The official TypeScript SDK for the ECODrIx platform. Use it from your own backend to work with your workspace: CRM leads and pipelines, Flow automations, the unified inbox (WhatsApp, Instagram, Facebook, Telegram, email, webchat), invoices and payment links, file storage, and live events. Every call is scoped to the workspace your key belongs to.

Install

pnpm add @ecodrix/erix-api
# or: npm install @ecodrix/erix-api
  • Node.js 18+ (uses the global fetch and Blob). Ships ESM and CommonJS builds with bundled type definitions.
  • Browsers: supported through a bundler (Vite, Next.js, webpack) in proxy mode only, plus the keyless website chat visitor client. There is no CDN build.

Quick start

Run this on your server. Keep the key in a server-only environment variable.

import { ECODrIxAPI } from "@ecodrix/erix-api";

const ecod = new ECODrIxAPI({
  apiKey: process.env.ECOD_API_KEY!,
  clientCode: "ERIX_CLNT_XXXXXX", // Settings → Developer in the dashboard
});

const { data: lead } = await ecod.product.erix.leads.create({
  firstName: "Test",
  lastName: "Customer",
  phone: "+91XXXXXXXXXX",
  source: "website",
});

await ecod.product.erix.events.trigger({
  trigger: "enquiry_received",
  phone: "+91XXXXXXXXXX",
});

// The client keeps a realtime socket open. Close it when your process stops.
process.on("SIGTERM", () => ecod.disconnect());
OptionTypeDefaultNotes
apiKeystringnoneWorkspace key or a scoped key. Server only.
clientCodestringnoneYour workspace code, e.g. ERIX_CLNT_XXXXXX.
version"v1""v1"API route version.
timeoutnumber (ms)30000Per request. 0 disables.
proxy{ url, ticket? }noneBrowser mode. Replaces apiKey.

How the client is laid out

The client follows the API's route tree:

NamespaceWhat it covers
ecod.product.erix.*CRM (leads, pipelines, activities, analytics), events (Flow triggers), chat (inbox), invoices, payments, ai
ecod.infra.connect.*Channels: whatsapp.templates, email, meet, connections
ecod.infra.storage.*Files: media, files, folders
ecod.platform.*Workspace: knowledge (AI knowledge base), users.notifications
ecod.webhooksWebhook signature verification
ecod.on() / ecod.off()Realtime events
ecod.request()Authenticated call to any endpoint without a typed method yet

Browser use (proxy mode)

Never put a workspace key in the browser. That includes NEXT_PUBLIC_*, VITE_* or any other variable that ends up in a client bundle. Anyone who opens dev tools can read it, and a workspace key has full access.

In the browser, run the SDK in proxy mode:

  1. Your server exposes /api/erix/*. It checks your signed-in user, adds the key, and forwards the request to ECODrIx.
  2. For realtime, your server mints a 5-minute socket ticket. The browser gets the ticket, never the key.

Proxy route (Next.js App Router, app/api/erix/[...path]/route.ts):

const API = "https://api.ecodrix.com";
const PASS_HEADERS = ["content-type", "accept", "idempotency-key"];
const DROP_HEADERS = ["content-encoding", "content-length", "transfer-encoding", "connection"];

async function proxy(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
  const user = await requireUser(req); // your own session check
  if (!user) {
    return Response.json({ success: false, error: "Unauthorized" }, { status: 401 });
  }

  const { path } = await ctx.params;
  const rel = `/${path.map(encodeURIComponent).join("/")}`;
  if (!/^\/v\d+\/api\//.test(rel)) {
    return Response.json({ success: false, error: "Not found" }, { status: 404 });
  }

  const headers = new Headers({
    "x-api-key": process.env.ECOD_API_KEY!,
    "x-client-code": "ERIX_CLNT_XXXXXX",
  });
  for (const name of PASS_HEADERS) {
    const value = req.headers.get(name);
    if (value) headers.set(name, value);
  }

  const hasBody = req.method !== "GET" && req.method !== "HEAD";
  const upstream = await fetch(`${API}${rel}${new URL(req.url).search}`, {
    method: req.method,
    headers,
    body: hasBody ? await req.arrayBuffer() : undefined,
    redirect: "manual",
  });

  const out = new Headers(upstream.headers);
  for (const name of DROP_HEADERS) out.delete(name);
  return new Response(upstream.body, { status: upstream.status, headers: out });
}

export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE };

Ticket route (app/api/erix/ticket/route.ts):

export async function POST(req: Request) {
  const user = await requireUser(req); // your own session check
  if (!user) return new Response("Unauthorized", { status: 401 });

  const res = await fetch("https://api.ecodrix.com/v1/api/platform/realtime/ticket", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ECOD_API_KEY!,
      "x-client-code": "ERIX_CLNT_XXXXXX",
    },
    cache: "no-store",
  });
  const body = (await res.json().catch(() => null)) as { data?: { ticket?: string } } | null;
  const ticket = body?.data?.ticket;
  if (!res.ok || !ticket) return new Response("Ticket unavailable", { status: 502 });

  return new Response(ticket, {
    headers: { "content-type": "text/plain", "cache-control": "no-store" },
  });
}

Browser client:

import { ECODrIxAPI } from "@ecodrix/erix-api";

export const ecod = new ECODrIxAPI({
  proxy: {
    url: "/api/erix",
    ticket: () =>
      fetch("/api/erix/ticket", { method: "POST" }).then((r) =>
        r.ok ? r.text() : Promise.reject(new Error("No realtime ticket")),
      ),
  },
});

The SDK fetches a fresh ticket on every reconnect. Without ticket, REST calls work and no socket opens.

Anyone signed in to your app can now do whatever the workspace key can. If your users shouldn't have all of that, allow-list the paths they need in the proxy route.

Website chat visitor client

@ecodrix/erix-api/visitor is a separate entry for an anonymous website visitor chatting with a workspace. It takes no key and reaches only that visitor's own thread. It uses fetch and loads socket.io-client only when you go live, so a chat widget doesn't ship the full SDK. The ERIX chat widget runs on it.

import { WebchatVisitor, WebchatVisitorError } from "@ecodrix/erix-api/visitor";

const chat = new WebchatVisitor({
  clientCode: "ERIX_CLNT_XXXXXX",
  // The visitor's only secret: random, 20–80 characters of A-Z a-z 0-9 _ -.
  // Keep it (e.g. in localStorage) to continue the same thread next visit.
  sessionId,
  // The site the chat runs on. It must be on the workspace's allowed domains.
  parent: "https://www.example.in",
});

const { data: messages, widget, consentNotice } = await chat.messages();

try {
  await chat.send("Do you open on Sunday?", {
    messageId: crypto.randomUUID(), // resend with the same id: stored once
    page: { path: location.pathname, title: document.title },
  });
} catch (err) {
  if (err instanceof WebchatVisitorError) {
    console.warn(err.code, err.status, err.field, err.message);
  }
}

await chat.contact({ name: "Ravi", phone: "98765 43210" });

Live replies and typing (/visitor socket):

chat.connect({
  onMessage: (message) => upsert(message), // `from`: visitor | ai | team | system
  onTyping: ({ on, waiting }) => showTyping(on, waiting), // waiting: the team will reply
  onStatus: ({ ids, status }) => markTicks(ids, status), // "read": the team opened the chat
  onError: () => startPolling(), // live is down: poll messages()
  onReconnect: () => {
    stopPolling();
    chat.messages().then(redraw); // nothing missed is replayed
  },
});

chat.disconnect();

Ticks, as on WhatsApp. Each message has status (sent ✓, delivered ✓✓, read ✓✓ seen): the visitor's own are delivered once stored and read once the team opened the chat. Report the business's replies back, up to 50 ids a call; ticks only move forward:

await chat.receipts(replyIds, "delivered"); // the chat got them
await chat.receipts(replyIds, "read"); // open and on screen
err.codeMeaning
VALIDATION_ERRORBad input. err.field names it, err.message is visitor copy
ORIGIN_NOT_ALLOWEDThe site isn't on the workspace's allowed domains
NOT_FOUNDUnknown client code
RATE_LIMITEDToo many messages or connects. Wait a minute
MALICIOUS_INPUTThe text was refused as a script or injection
INTERNAL_ERRORServer error. Try again
NETWORK_ERRORNo answer (offline, 30 s timeout). Try again

The live socket only accepts the ERIX widget frame's origin. A chat UI on your own page works over HTTP when that site is the workspace's website (browser CORS), and polls messages() for replies.

Scoped keys

For an integration that does one job (a booking form that fires a Flow event, say), create a scoped key under Settings → Developer → Scoped API keys. Scoped keys can be revoked, can expire and have usage tracking. Each one carries a single grant:

PurposeGrantWorks for
Trigger eventsflow:eventsproduct.erix.events.trigger()
Send WhatsAppconnect:whatsappchat.messages.send({ to }), sendTemplate()
Send emailconnect:emailPOST /infra/connect/send/email
Send Telegramconnect:telegramPOST /infra/connect/send/telegram

Keys look like erix_<scope>_<secret>. Pass one as apiKey. The server enforces its grant, so any call outside the grant fails with AuthenticationError (HTTP 403). Over plain HTTP you can also send it as Authorization: Bearer erix_<scope>_….

import { ECODrIxAPI } from "@ecodrix/erix-api";

const events = new ECODrIxAPI({
  apiKey: process.env.ECOD_EVENTS_KEY!, // erix_flow_… with flow:events
  clientCode: "ERIX_CLNT_XXXXXX",
});

await events.product.erix.events.trigger({ trigger: "booking_created", phone: "+91XXXXXXXXXX" });

CRM: leads and pipelines

// Create or update by phone. `refs` are your own IDs, kept on the lead.
await ecod.product.erix.leads.upsert({
  leadData: { phone: "+91XXXXXXXXXX", firstName: "Test", source: "website" },
  refs: { orderId: "ORD-1042" },
});

const { data: leads, pagination } = await ecod.product.erix.leads.list({
  status: "open", // open | won | lost | archived
  search: "test",
  limit: 25,
});

await ecod.product.erix.leads.move("LEAD_ID", "STAGE_ID"); // pipeline column
await ecod.product.erix.leads.setFollowUp("LEAD_ID", "2026-10-20T15:00:00+05:30", "Share the quote");
await ecod.product.erix.leads.tags("LEAD_ID", { add: ["hot"], remove: ["cold"] });
await ecod.product.erix.leads.convert("LEAD_ID", "won", "Advance received");
await ecod.product.erix.leads.delete("LEAD_ID"); // archives; history stays

Other lookups: retrieve(id), retrieveByPhone(phone), retrieveByRef(key, value), update(id, patch), notes(id), createNote(id, { content }), activities(id), profile(id), import(leads) and merge(primaryId, secondaryId).

Data protection (DPDP). Only owners and admins can do this:

const { data: everything } = await ecod.product.erix.leads.export("LEAD_ID"); // (3.2.0)

// Irreversible. Wipes personal data across CRM, inbox and automations.
// Invoices are kept for GST records.
const { data: result } = await ecod.product.erix.leads.erase("LEAD_ID"); // (3.2.0)
console.log(result.invoicesKept);

Pipelines:

const { data: pipeline } = await ecod.product.erix.pipelines.create({
  name: "Admissions",
  stages: ["Enquiry", "Visit booked", "Fee paid", "Admitted"],
});

const { data: pipelines } = await ecod.product.erix.pipelines.list();
const { data: board } = await ecod.product.erix.pipelines.board("PIPELINE_ID");

await ecod.product.erix.pipelines.addStage("PIPELINE_ID", { name: "Follow-up", color: "#f59e0b" });
// Moves the stage's leads to another stage of the same pipeline, then deletes it.
await ecod.product.erix.pipelines.deleteStage("STAGE_ID", "TARGET_STAGE_ID");

Flow: events and custom triggers

events.trigger() fires an event. Every active flow with a matching trigger starts a run for that contact. You need phone or email.

const res = await ecod.product.erix.events.trigger({
  trigger: "appointment_booked",
  phone: "+91XXXXXXXXXX",
  variables: { slot: "Mon 11:00", branch: "Indore" },
  createLeadIfMissing: true,
  leadData: { firstName: "Test", source: "website" },
  delayMinutes: 30, // or delaySeconds, or runAt (ISO with offset). Use one.
  idempotencyKey: "booking-8812", // a retry with the same key won't start flows twice
});
console.log(res.data?.idempotencyKey, res.data?.leadId);

createLeadIfMissing, the delays and runAt are released per workspace. Without the release the server ignores them and says so in the response.

Define your own triggers so flows can start on them:

await ecod.product.erix.events.createCustomEvent({
  name: "appointment_confirm", // lowercase letters, digits, _ . -
  displayName: "Appointment confirmed",
});

const { data } = await ecod.product.erix.events.listCustomEvents(); // data.triggers
await ecod.product.erix.events.deleteCustomEvent("TRIGGER_ID");

Some runs stop at an approval step and wait for a person. You can list those and decide on them:

const { data } = await ecod.product.erix.ai.approvals(); // { drafts, runs }
for (const run of data.runs) {
  // Pass run.nodeId (3.2.0) so a stale view can't decide a different step.
  await ecod.product.erix.ai.decideRun(run.id, "approve", run.nodeId);
}

Channels: inbox and direct send

All channels share one inbox. Any thread can be answered with chat.messages.send({ conversationId }).

const { data: threads } = await ecod.product.erix.chat.conversations.list({
  channel: "whatsapp",
  limit: 20,
});

const { data: messages, hasMore } = await ecod.product.erix.chat.conversations.messages(
  "CONVERSATION_ID",
  { limit: 50 }, // page back with { before: oldestMessage.createdAt }
);

await ecod.product.erix.chat.messages.send({
  conversationId: "CONVERSATION_ID",
  text: "Your order is packed and ships today.",
});
await ecod.product.erix.chat.conversations.markRead("CONVERSATION_ID");

// Open (or find) a thread for a number and link it to a lead.
const { data: thread } = await ecod.product.erix.chat.conversations.create({
  phone: "+91XXXXXXXXXX",
  channel: "whatsapp",
});
await ecod.product.erix.chat.conversations.linkLead(thread.id, "LEAD_ID");

Send a file. Upload it to your storage first, then attach it:

const input = document.querySelector<HTMLInputElement>("#attachment")!;
const media = await ecod.product.erix.chat.messages.upload(input.files![0]);

await ecod.product.erix.chat.messages.send({
  conversationId: "CONVERSATION_ID",
  media,
  text: "Invoice attached",
});

Direct WhatsApp send, without an inbox thread ID:

// An approved template. Use this to start a conversation.
await ecod.product.erix.chat.messages.sendTemplate({
  to: "+91XXXXXXXXXX",
  templateName: "order_update",
  language: "en",
  variables: ["ORD-1042", "₹1,499"],
});

// Free text. Only works inside WhatsApp's 24-hour customer service window.
await ecod.product.erix.chat.messages.send({ to: "+91XXXXXXXXXX", text: "Thanks, we got your enquiry." });

Manage templates with ecod.infra.connect.whatsapp.templates: list(), sync() and retrieve(name).

Amounts are in rupees. This invoice comes to ₹25,000 + 18% GST = ₹29,500:

const { data: invoice } = await ecod.product.erix.invoices.create({
  clientName: "Test Customer",
  clientPhone: "+91XXXXXXXXXX",
  leadId: "LEAD_ID",
  items: [{ description: "Website design", quantity: 1, rate: 25000, hsnSac: "998314" }],
  taxRate: 18,
  dueDate: "2026-11-15",
});

// A link on your connected gateway (Razorpay or Stripe). Calling again returns the same link.
const { data: link } = await ecod.product.erix.invoices.paymentLink(invoice.id); // (3.2.0)
console.log(link.paymentLinkUrl);

await ecod.product.erix.invoices.sendWhatsApp(invoice.id); // (3.2.0) with the payment link
await ecod.product.erix.invoices.sendEmail(invoice.id); // (3.2.0) PDF + payment link

Invoices can't be deleted, only voided (invoices.void(id)), so GST records stay intact. Other calls: list({ status }), getInvoice(id) and update(id, patch). Accounting exports live under product.erix.invoiceExport: csv(), tally() and zoho().

A one-off payment link that isn't tied to an invoice, or a payment you took offline:

await ecod.infra.connect.connections.createPaymentLink({
  amount: 1499, // ₹1,499
  description: "Consultation fee",
  customer: { phone: "+91XXXXXXXXXX" },
});

// Cash, UPI or bank transfer. With orderId, a retry is counted once.
await ecod.product.erix.payments.capture({ leadId: "LEAD_ID", amount: 5000, orderId: "UPI-2026-0042" });

Storage uploads

media.upload() gets a presigned URL, uploads straight to storage and then confirms the upload, all in one call. The file bytes never pass through the API, and your key never goes to the storage host.

import { readFile } from "node:fs/promises";

const buffer = await readFile("./brochure.pdf");
const { data: file } = await ecod.infra.storage.media.upload(buffer, {
  folder: "documents",
  filename: "brochure.pdf",
  contentType: "application/pdf",
});

const { data: download } = await ecod.infra.storage.files.getDownloadUrl<{
  data: { downloadUrl: string };
}>(file.key);
console.log(download.downloadUrl); // short-lived URL

The same call takes a browser File. Other calls: files.list(folder) and files.delete(key).

Realtime events

Events stream over a socket that's scoped to your workspace. You only receive your own workspace's events.

import type { Message } from "@ecodrix/erix-api";

const onMessage = ({ conversationId, message }: { conversationId: string; message: Message }) => {
  console.log(conversationId, message.direction, message.text);
};

ecod
  .on("inbox.message", onMessage)
  .on("message_status_update", ({ messageId, status }) => console.log(messageId, status))
  .on("resync", () => {
    // Reconnected after a gap too large to replay. Refetch your data.
  });

// Later, e.g. in a React effect cleanup:
ecod.off("inbox.message", onMessage);
EventPayload / meaning
inbox.message{ conversationId, message }. Every stored message: any channel, inbound or outbound
message_status_update{ messageId, conversationId, status }. Sent, delivered, read or failed
ai_suggestion / ai_handoffAn AI draft is waiting / the AI handed a chat to a person
workflow-run-updateFlow run progress
platform:notificationA new in-app notification
knowledge.source_updatedA knowledge base source changed status
resyncMissed events couldn't be replayed. Refetch

Each event's last argument is { seq }. After a reconnect the SDK sends back the last seq it saw and the server replays what was missed. A replay can repeat an event, so write handlers that are safe to run twice (upsert by ID).

Webhook signature verification

Flow's webhook step sends JSON to your URL. If you set a secret on the Webhook connection, the request is signed with it in x-ecodrix-signature: sha256=<hex>. Verify the signature against the raw body, before you parse it. Node only.

import { APIError } from "@ecodrix/erix-api";
import { ecod } from "./ecodrix"; // your server-side client from Quick start

export async function POST(req: Request) {
  const raw = await req.text();
  try {
    const event = await ecod.webhooks.constructEvent(
      raw,
      req.headers.get("x-ecodrix-signature") ?? undefined,
      process.env.ECOD_WEBHOOK_SECRET!,
    );
    console.log("verified", event);
    return Response.json({ received: true });
  } catch (err) {
    // WebhookSignatureError: APIError with code "invalid_signature"
    const message = err instanceof APIError ? err.message : "Bad request";
    return new Response(message, { status: 400 });
  }
}

Raw requests

Use ecod.request() for an endpoint that has no typed method yet. You still get auth, retries and the timeout. Paths are relative to /v1/api:

await ecod.request("POST", "/infra/connect/send/email", {
  to: "[email protected]",
  subject: "Your booking is confirmed",
  html: "<p>See you on Monday at 11:00.</p>",
});

Errors

Failed calls throw. The API answers with an envelope like { success: false, error, message, code? }, and the SDK turns it into one of these classes:

ClassWhen
EcodrixErrorBase class of everything below
APIErrorAny other failure. Has .status, .code (when sent) and .message
AuthenticationErrorHTTP 401 or 403: bad key or client code, missing scope, or a role that isn't allowed
RateLimitErrorHTTP 429, after the SDK's own retries run out
import { APIError, AuthenticationError, RateLimitError } from "@ecodrix/erix-api";

try {
  await ecod.product.erix.leads.retrieve("LEAD_ID");
} catch (err) {
  if (err instanceof AuthenticationError) {
    console.error("Check the key, client code, scope or role:", err.message);
  } else if (err instanceof RateLimitError) {
    console.warn("Rate limited, back off and retry later");
  } else if (err instanceof APIError) {
    console.error(`API error ${err.status ?? "network"}: ${err.message}`, err.code);
  } else {
    throw err;
  }
}

ecod.request() throws a plain APIError for every status.

Retries and timeouts. The SDK retries up to 3 times with exponential backoff. It retries network errors (any method), 5xx responses on idempotent methods (GET, PUT, DELETE) and 429 responses. Each request times out after 30 s; change that with the timeout option. A timed-out request isn't retried. A network error can also hit a POST whose response was lost, and the retry then sends it a second time. Pass your own reference where a method takes one (refs, orderId) so a duplicate is safe.

TypeScript

Methods return the response body, so pass a type argument to get it typed. The SDK exports the main entity and parameter types (Lead, CreateLeadParams, ListLeadsParams, Pipeline, PipelineStage, Conversation, Message, SendMessageParams, PausedRun, AiSuggestion, KnowledgeSource, PaginatedResponse and more).

import type { Lead, PaginatedResponse } from "@ecodrix/erix-api";

const page = await ecod.product.erix.leads.list<PaginatedResponse<Lead>>({ limit: 50 });
console.log(page.pagination.total, page.data[0]?.firstName);

// Page through every lead without handling pages yourself.
for await (const lead of ecod.product.erix.leads.listAutoPaging<Lead>({ status: "open" })) {
  console.log(lead.phone);
}

Versioning

  • The SDK follows semver on its own line (3.x). Breaking changes only land in a major release. See CHANGELOG.md.
  • The API server has its own version numbers: platform v1.0 is server 1.0.0. The SDK calls the /v1 routes, and the version option chooses the route version.
  • A method marked (3.2.0) needs SDK 3.2.0 or later.

Guides and every endpoint: https://api.ecodrix.com/docs.

License

MIT

版本与发布记录

@ecodrix/erix-api 共发布 67 个版本,首次发布于 2026/04/03;过去 12 个月发布了 67 个版本。

版本发布日期距今
4.3.2latest2026/11/13今天
4.3.12026/11/13今天
4.3.02026/11/103 天前
4.2.02026/11/094 天前
4.1.1patch2026/11/085 天前
4.1.02026/11/075 天前

关于 @ecodrix/erix-api 的常见问题

@ecodrix/erix-api 是什么?

Official TypeScript SDK for the ERIX API by ECODrIx: CRM, Flow automations, one inbox across WhatsApp, Instagram and email, GST invoices and payments. Node and browser (proxy mode).

如何安装 @ecodrix/erix-api?

在项目目录运行 npm install @ecodrix/erix-api,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 erix,可以用 npx erix 直接运行。

@ecodrix/erix-api 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ecodrix/erix-api 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ecodrix/erix-api 可以商用吗?

@ecodrix/erix-api 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ecodrix/erix-api 的下载量有多少?

根据 npm 官方下载统计,@ecodrix/erix-api 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,194 次,日均约 73 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ecodrix/erix-api 还在维护吗?

最新版本 4.3.2 发布于 2026/11/13(今天);至今共发布 67 个版本,其中过去 12 个月发布了 67 个;首次发布于 2026/04/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。