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

@xw-tech/egg-timeline

v0.1.3

Published

Egg.js client plugin for Timeline open platform APIs

Readme

@xw-tech/egg-timeline

@xw-tech/egg-timeline 是马老板助手开放平台的 Egg.js 客户端插件。业务系统传入开放平台应用的 aksecret 后,可以直接在 Egg 应用里调用机器人、素材、朋友圈任务、执行查询、取消与重试接口。

默认开放平台地址:

http://timeline.api.yuehuivip.com/api/open/v1

安装

npm install @xw-tech/egg-timeline

本仓库内本地调试:

npm install /path/to/egg-timeline

开启插件

config/plugin.js

'use strict';

exports.timeline = {
  enable: true,
  package: '@xw-tech/egg-timeline',
};

配置

config/config.default.js

'use strict';

exports.timeline = {
  baseUrl: 'http://timeline.api.yuehuivip.com/api/open/v1',
  ak: process.env.TIMELINE_AK,
  secret: process.env.TIMELINE_SECRET,
  timeout: 15000,
  unwrapData: true,
};

也兼容 appKey / appSecret

exports.timeline = {
  appKey: process.env.TIMELINE_APP_KEY,
  appSecret: process.env.TIMELINE_APP_SECRET,
};

unwrapData 默认是 true,方法直接返回开放平台响应里的 data。如果设置为 false,会返回完整结构:

{
  code: 'OK',
  message: 'success',
  request_id: 'req_xxx',
  data: {}
}

使用入口

插件会同时暴露:

  • app.timeline
  • ctx.timeline
const robots = await ctx.timeline.listRobots({ page: 1, page_size: 20 });

字段取值

| 字段 | 可取值 | 说明 | | --- | --- | --- | | media_type | text, image | 朋友圈类型 | | schedule_type | immediate, scheduled | 立即发布或定时发布 | | interval_type | fixed, random | 多机器人分发间隔 | | moment.status | pending, running, success, partial_success, failed, cancelled | 总任务状态 | | execution.status | pending, locked, received, downloading, running, published, commenting, success, failed, timeout, cancelled | 单机器人执行状态 | | robot.online_status | online, offline | 机器人在线状态 | | robot.execution_status | idle, busy, paused, error | 机器人执行状态 | | robot.accessibility_status | enabled, disabled, unknown | 无障碍服务状态 |

机器人

查询机器人列表

const robots = await ctx.timeline.listRobots({
  keyword: '红米',
  online_status: 'online',
  execution_status: 'idle',
  accessibility_status: 'enabled',
  status: 'active',
  tag_id: 1,
  page: 1,
  page_size: 20,
});

参数:

| 字段 | 类型 | 取值 | 说明 | | --- | --- | --- | --- | | keyword | string | - | 按设备名、备注、微信昵称、微信号搜索 | | online_status | string | online, offline | 在线状态 | | execution_status | string | idle, busy, paused, error | 执行状态 | | accessibility_status | string | enabled, disabled, unknown | 无障碍服务状态 | | status | string | active, disabled | 机器人启用状态 | | tag_id | number|string | 标签 ID | 标签筛选 | | page | number | 默认 1 | 页码 | | page_size | number | 1-100,默认 20 | 每页数量 |

返回:

{
  items: [
    {
      robot_id: 101,
      name: '红米6A-01',
      status: 'active',
      wechat_nickname: '紫菱',
      online_status: 'online',
      execution_status: 'idle',
      tags: [{ id: 1, name: '第一组', color: '#16a34a' }],
    },
  ],
  page: 1,
  page_size: 20,
  total: 1,
}

查询机器人详情

const robot = await ctx.timeline.getRobot(101);

素材

登记或转存图片素材

const media = await ctx.timeline.uploadMedia({
  url: 'https://cdn.example.com/moments/product-1.jpg',
  mime_type: 'image/jpeg',
  transfer: false,
});

参数:

| 字段 | 类型 | 取值 | 说明 | | --- | --- | --- | --- | | url | string | http/https URL | 图片 URL,必填 | | transfer | boolean | true, false | true 表示由平台转存图片;false 或不传表示只登记 URL | | mime_type | string | image/jpeg, image/png, image/webp, image/gif | 不传时平台按 URL 后缀推断 | | file_size | number | 字节数 | 文件大小 | | original_name | string | - | 原始文件名 | | storage_key | string | - | 外部存储 key |

返回的 media.url 可以直接放入 createMomentmedia 数组。

朋友圈任务

查询任务列表

const moments = await ctx.timeline.listMoments({
  external_id: 'crm_scheduled_202606300001',
  status: 'pending',
  created_at_start: '2026-06-30 00:00:00',
  created_at_end: '2026-06-30 23:59:59',
  page: 1,
  page_size: 20,
});

立即发布文本朋友圈

const task = await ctx.timeline.createMoment({
  external_id: `crm_text_${Date.now()}`,
  title: 'CRM 自动发布',
  content: '朋友圈正文',
  media_type: 'text',
  comments: ['评论1'],
  robot_ids: [101, 102],
  schedule_type: 'immediate',
  interval_type: 'fixed',
  interval_seconds: 30,
});

立即发布图文朋友圈并追加评论

const task = await ctx.timeline.createMoment({
  external_id: `crm_image_${Date.now()}`,
  title: '新品图文朋友圈',
  content: '今日新品上架,欢迎咨询。',
  media_type: 'image',
  media: [
    'https://cdn.example.com/moments/product-1.jpg',
    'https://cdn.example.com/moments/product-2.jpg',
  ],
  comments: [
    '价格和库存以客服回复为准。',
    '需要同款图片或报价可以私信我。',
  ],
  robot_ids: [101, 102],
  schedule_type: 'immediate',
  interval_type: 'fixed',
  interval_seconds: 30,
});

定时发布朋友圈

schedule_typescheduled,并传 scheduled_at。时间格式为 YYYY-MM-DD HH:mm:ss。第一台机器人会从 scheduled_at 开始执行。

const task = await ctx.timeline.createMoment({
  external_id: `crm_scheduled_${Date.now()}`,
  title: '今晚 8 点定时朋友圈',
  content: '今晚 8 点准时发布的朋友圈正文。',
  media_type: 'image',
  media: [
    'https://cdn.example.com/moments/night-sale-1.jpg',
    'https://cdn.example.com/moments/night-sale-2.jpg',
  ],
  comments: ['活动有效期到今晚 24 点。'],
  robot_ids: [101],
  schedule_type: 'scheduled',
  scheduled_at: '2026-06-30 20:00:00',
  interval_type: 'fixed',
  interval_seconds: 0,
});

定时发布并随机间隔分发到多台机器人

多台机器人时,第一台从 scheduled_at 开始;后续机器人按随机秒数错峰执行。

const task = await ctx.timeline.createMoment({
  external_id: `crm_random_${Date.now()}`,
  title: '多机器人错峰发布',
  content: '多台机器人按随机间隔错峰发布。',
  media_type: 'image',
  media: [
    'https://cdn.example.com/moments/campaign-1.jpg',
  ],
  comments: [],
  robot_ids: [101, 102, 103],
  schedule_type: 'scheduled',
  scheduled_at: '2026-06-30 20:00:00',
  interval_type: 'random',
  random_interval_min_seconds: 60,
  random_interval_max_seconds: 180,
});

createMoment 字段

| 字段 | 必填 | 类型 | 取值 / 格式 | 说明 | | --- | --- | --- | --- | --- | | external_id | 否 | string | 建议唯一 | 外部业务 ID。相同 external_id 和相同请求体重复提交会返回同一任务 | | title | 是 | string | 非空 | 任务标题 | | content | 否 | string | - | 朋友圈正文 | | media_type | 是 | string | text, image | text 不允许带 media;image 必须带 1-9 张图片 | | media | image 必填 | string[] 或 object[] | 1-9 个 URL | 推荐直接传图片 URL 字符串数组,平台按数组顺序发布 | | comments | 否 | string[] | - | 发布成功后按数组顺序追加评论 | | robot_ids | 是 | number[] | 机器人 ID | 通过 listRobotsgetRobot 获得 | | schedule_type | 是 | string | immediate, scheduled | 立即发布或定时发布 | | scheduled_at | scheduled 必填 | string | YYYY-MM-DD HH:mm:ss | 定时发布时间 | | interval_type | 否 | string | fixed, random,默认 fixed | 多机器人之间的分发间隔类型 | | interval_seconds | fixed 时建议传 | number | >= 0 | 固定间隔秒数 | | random_interval_min_seconds | random 必填 | number | >= 0 | 随机间隔最小秒数 | | random_interval_max_seconds | random 必填 | number | >= min | 随机间隔最大秒数 |

查询详情、执行明细、取消、重试

const detail = await ctx.timeline.getMoment(690521848);
const executions = await ctx.timeline.listMomentExecutions(690521848);
const cancelled = await ctx.timeline.cancelMoment(690521848);
const retried = await ctx.timeline.retryExecution(9001);

retryExecution 使用的是 execution_id,不是 moment_id

原始请求

如果后续开放平台新增接口,可以直接使用底层 request

await ctx.timeline.request('GET', '/robots', {
  params: {
    page: 1,
    page_size: 20,
  },
});

回调签名校验

开放平台回调 Header 与接口调用签名一致。外部系统接收回调时可以使用:

const ok = ctx.timeline.verifyCallbackSignature({
  method: ctx.method,
  path: ctx.path,
  queryString: ctx.querystring,
  body: ctx.request.rawBody,
  timestamp: ctx.get('X-Timestamp'),
  nonce: ctx.get('X-Nonce'),
  signature: ctx.get('X-Signature'),
});

回调事件类型:

  • execution.success
  • execution.failed
  • moment.success
  • moment.partial_success
  • moment.failed
  • moment.cancelled

请确保应用已保留原始请求 body 字符串,否则 JSON 重新序列化后可能导致签名不一致。

错误处理

开放平台返回非 2xx 或 code !== "OK" 时会抛出 TimelineOpenApiError

try {
  await ctx.timeline.createMoment(payload);
} catch (error) {
  ctx.logger.error(error.code, error.status, error.requestId, error.response);
  throw error;
}

常见错误:

| code | 说明 | | --- | --- | | TIMELINE_AK_REQUIRED | 未配置 ak/appKey | | TIMELINE_SECRET_REQUIRED | 未配置 secret/appSecret | | TIMELINE_OPEN_API_TIMEOUT | 请求超时 | | TIMELINE_OPEN_API_NETWORK_ERROR | 网络错误 | | MEDIA_INVALID | 图片 URL 不合法或素材类型不支持 | | IDEMPOTENCY_CONFLICT | external_id 已被相同应用使用,但请求体不同 | | RATE_LIMITED | 触发限流、每日任务上限或单机器人每日上限 | | PUBLISH_WINDOW_CLOSED | 当前不在应用允许的发布窗口内 |