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

wechat-ilink-photon-sdk

v0.1.2

Published

WeChat iLink bot provider for Spectrum (Photon) — a definePlatform provider over the iLink bot protocol, with durable claim-before-emit inbound and per-contact history.

Readme

wechat-ilink-photon-sdk

A Spectrum (Photon) provider for the WeChat iLink bot protocol, with durable, no-loss inbound and a separate conversation per contact.

It wraps wechat-ilink-client (embedded and patched — see UPSTREAM.md) and exposes it as a first-class definePlatform provider you can drop into a Spectrum({ providers }) app next to iMessage, Slack, etc.

Why this exists

The iLink client is deliberately stateless — it persists nothing. A naive integration loses messages, because:

  • Its long-poll monitor advances the sync cursor before dispatching a batch, so a crash mid-batch drops messages.
  • A Spectrum provider's emit is fire-and-forget past the broadcast boundary — "emitted" never means "processed", and if the app's message loop dies without app.stop(), messages are silently dropped.

This SDK closes both gaps with a claim-before-emit runtime: every poll commits messages + context tokens + cursor to a caller-supplied store in one step, and only then emits into Spectrum. The app acks each message out-of-band once it is durably handled; unacked messages are re-emitted on restart. The result is at-least-once delivery with dedupe — never silent loss.

Install / consume

This package is consumed as a vendored, prebuilt dependency (the same pattern emo.studio uses for its other SDKs) — not from npm. Build it and reference the dist/ as a file: dependency:

// consumer package.json
{ "dependencies": { "wechat-ilink-photon-sdk": "file:vendor/wechat-ilink-photon-sdk" } }

@spectrum-ts/core is a peer dependency (^8.2.0), provided by the host app. See scripts/sync-vendor.sh for the build-and-copy workflow.

This package is not currently published to npm.

Quick start

import { Spectrum } from "@spectrum-ts/core";
import { wechatIlink, runQRLogin } from "wechat-ilink-photon-sdk";
import { MyStore } from "./my-store"; // implements IlinkStateStore

const store = new MyStore();

// First run, or any time you want another scanner: QR login persists/refreshes
// the scanner's own credential. Existing scanners are not replaced.
await runQRLogin(store, { onQRCode: (url) => console.log("Scan:", url) });

const app = await Spectrum({
  providers: [wechatIlink.config({ store, lineRef: "wechat" })],
});

for await (const [space, message] of app.messages) {
  if (message.content.type === "text" && /ping/i.test(message.content.text)) {
    await space.send("pong");
  }
  // Durably mark handled — the out-of-band ack the stream contract lacks.
  await wechatIlink(app).ackDispatched(message.id);
}

The state store

All durability lives behind IlinkStateStore (the SDK ships an in-memory implementation for examples/tests; production supplies a database-backed one):

interface IlinkStateStore {
  getCredentials(): Promise<IlinkCredentials | null>;
  listCredentials?(): Promise<IlinkCredentials[]>;        // active scanner sessions
  saveCredentials(creds: IlinkCredentials): Promise<void>;
  getCursor(credentialKey?: string): Promise<string | null>;
  claimBatch(batch: ClaimBatch): Promise<ClaimResult>;  // messages + tokens + cursor, atomically
  listUnacked(limit: number): Promise<ClaimedMessage[]>; // restart-sweep source
  ackDispatched(key: string): Promise<void>;
  getContextToken(userId: string, credentialKey?: string): Promise<string | null>;
  markCredentialExpired?(credentialKey: string, botToken: string): Promise<void>;
  acquireExclusive?(): Promise<() => Promise<void>>;      // optional single-poller fence
}

When listCredentials() is implemented, the runtime starts one poll session per credential. Each credential gets its own cursor, context tokens, and inbound dedupe scope. This matters because two scanner accounts can see the same contact ids or message counters; treating those as global makes the newest QR scan kick out the previous scanner.

Inbound records use session-scoped space.id / sender.id values of the form ilink:{credential}:{user} (encoded for Spectrum ids). Reply through the same space you received and the SDK routes the outbound through the right scanner. If you call runtime.send("raw-user-id", ...) while multiple sessions are ready, the SDK refuses the ambiguous send instead of guessing the wrong scanner.

claimBatch must persist atomically and be idempotent on message key — that idempotency is what absorbs the server re-delivery you get after a stale-cursor restart.

What the provider handles

  • Inbound: deterministic sender-scoped dedupe keys; a filter chain that drops bot echoes, streaming partials, group messages, recalls, and unrenderable types (each counted for observability); lazy, size-capped, timed media downloads.
  • Sends: text / markdown / rich link / image / video / file / voice, paced; server-side rejections (ret != 0) surface as thrown errors instead of silent success; a missing context token throws ContextTokenMissingError (a plain error, so a queue can defer it — UnsupportedError would be swallowed by core).
  • Control content: Spectrum typing("start") / typing("stop") maps to WeChat's native iLink sendtyping indicator. The runtime fetches the required typing_ticket through getconfig, includes the otherwise-undocumented ilink_user_id, and caches each ticket per scanner/contact for 20 hours. Spectrum read content remains a no-op because iLink does not expose it.
  • Health: derived from poll recency (healthSnapshot action); session expiry (-14) marks only that credential down and heals when that scanner refreshes credentials — no redeploy needed.
  • Session start: never throws on missing credentials (that would kill the whole multi-provider app) — it starts degraded and self-heals after a QR login.

Examples

pnpm add qrcode-terminal   # optional, for inline QR rendering

pnpm example:pong          # replies "pong" to any "ping"
pnpm example:smoke         # sends one of every content type, reports what worked

Development

pnpm install
pnpm build       # tsup → dist (ESM + d.ts)
pnpm typecheck
pnpm test        # vitest — runtime crash-injection against an in-process fake server

License

MIT