@xw-tech/egg-timeline
v0.1.3
Published
Egg.js client plugin for Timeline open platform APIs
Maintainers
Readme
@xw-tech/egg-timeline
@xw-tech/egg-timeline 是马老板助手开放平台的 Egg.js 客户端插件。业务系统传入开放平台应用的 ak 和 secret 后,可以直接在 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.timelinectx.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 可以直接放入 createMoment 的 media 数组。
朋友圈任务
查询任务列表
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_type 传 scheduled,并传 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 | 通过 listRobots 或 getRobot 获得 |
| 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.successexecution.failedmoment.successmoment.partial_successmoment.failedmoment.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 | 当前不在应用允许的发布窗口内 |
