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

qiyu-openapi-sdk

v0.2.0

Published

七鱼客服开放平台 TypeScript/JavaScript SDK

Downloads

148

Readme

七鱼客服开放平台 SDK

npm version License: MIT

七鱼客服开放平台的 TypeScript/JavaScript SDK,提供完整的类型定义和易用的 API 接口。

特性

  • ✅ 完整的 TypeScript 类型定义
  • ✅ 自动处理鉴权(MD5 + SHA1 + checksum)
  • ✅ 基于 Promise 的异步 API
  • ✅ 模块化设计,按需使用
  • ✅ 完善的错误处理
  • ✅ 支持员工管理、工单管理等核心功能
  • ✅ 支持客户群详情查询
  • ✅ 工单客服信息自动增强(内建缓存,免 N+1 查询)

安装

npm install qiyu-openapi-sdk
# 或
yarn add qiyu-openapi-sdk
# 或
pnpm add qiyu-openapi-sdk

快速开始

import { QiyuKfClient } from 'qiyu-openapi-sdk';

// 初始化客户端
const client = new QiyuKfClient({
  appKey: 'your_app_key',
  appSecret: 'your_app_secret',
});

// 使用员工管理功能
async function example() {
  try {
    // 获取客服列表
    const staffList = await client.staff.list();
    console.log('客服列表:', staffList);

    // 创建工单
    const ticketId = await client.ticket.create({
      title: '测试工单',
      content: '这是一个测试工单',
      staffId: 12345,
      userMobile: '18888888888',
    });
    console.log('工单ID:', ticketId);
  } catch (error) {
    console.error('发生错误:', error);
  }
}

example();

初始化

import { QiyuKfClient } from 'qiyu-openapi-sdk';

const client = new QiyuKfClient({
  appKey: 'your_app_key',         // 必填,七鱼企业 appKey
  appSecret: 'your_app_secret',   // 必填,七鱼企业 appSecret
  baseURL: 'https://qiyukf.com',  // 可选,API 基础地址,默认为官方地址
  timeout: 30000,                 // 可选,请求超时(ms),默认 30000
  staffCache: { ttl: 300000 },    // 可选,启用客服缓存后工单自动注入名称,默认 TTL=5分钟
});

通过 client.staff 访问员工管理,client.ticket 访问工单管理,client.customerGroup 访问客户群详情查询。


客服信息自动增强

启用 staffCache 后,工单查询接口会自动将客服 ID 转换为可读名称,避免 N+1 查询。

const client = new QiyuKfClient({
  appKey: 'your_app_key',
  appSecret: 'your_app_secret',
  staffCache: { ttl: 300000 },  // 传对象即启用
});

const result = await client.ticket.getTicketList({ staffId, filterId });
const t = result.tickets[0];
console.log(t.staffName);   // '张工' - 创建人名称
console.log(t.holderName);  // '李工' - 受理人名称
console.log(t.followerNames); // { id: name } - 关注人名称映射

增强字段

启用后,以下字段自动注入到工单列表、搜索和详情返回中:

| 字段 | 来源 | 说明 | |------|------|------| | staffName | staffId | 创建人名称 | | holderName | holderId | 受理人名称 | | lastFinishProcessorName | lastFinishProcessor | 最后完结人名称 | | followerNames | follower | 关注人名称映射(仅列表,详情已有) |

缓存机制

  • 首次工单查询时后台异步加载全量客服列表(staff.list()
  • 缓存驻内存(Map),默认 TTL 5 分钟过期自动刷新
  • 并发请求共享同一次加载,不重复请求
  • 缓存未就绪时降级返回原始数据,不阻塞业务
  • 可通过 client.staffCache 手动控制:refresh()clear()isEmpty

性能对比:

| 方式 | 查询 100 条工单 | |------|----------------| | 无缓存 | 1 次工单 + 100 次 staff.getDetail = 101 次 API 调用 | | 启用缓存 | 1 次工单 + 1 次 staff.list = 2 次 API 调用 |


枚举

StaffRole - 客服角色

| 值 | 常量 | 说明 | |----|------|------| | 0 | NORMAL | 普通客服 | | 1 | ADMIN | 管理员 | | 2 | SUPER_ADMIN | 超级管理员 | | -1 | TICKET | 工单客服 | | -2 | CALL | 呼叫客服 |

StaffStatus - 客服状态

| 值 | 常量 | 说明 | |----|------|------| | 0 | ALL | 所有(查询用) | | 1 | NORMAL | 正常 | | 2 | DELETED | 已删除 | | 3 | DISABLED | 已停用 |

OnlineStatus - 在线客服状态

| 值 | 常量 | 说明 | |----|------|------| | 0 | OFFLINE | 离线 | | 1 | ONLINE | 在线可接待 | | 2 | SUSPEND | 挂起 | | 3 | BREAK | 小休 | | 9 | ADMIN_ONLINE | 管理端在线 |

TicketPriority - 工单优先级

| 值 | 常量 | 说明 | |----|------|------| | 2 | LOW | 低 | | 5 | NORMAL | 一般 | | 8 | URGENT | 紧急 | | 10 | CRITICAL | 非常紧急 |

TicketStatus - 工单状态

| 值 | 常量 | 说明 | |----|------|------| | 1 | SUBMITTED | 已提交 | | 5 | PENDING_CLAIM | 待申领 | | 10 | PROCESSING | 处理中 | | 20 | FINISHED | 已完结 | | 25 | REJECTED | 已驳回 | | 50 | PENDING | 已挂起 |

FieldType - 模板字段类型

| 值 | 常量 | 说明 | |----|------|------| | 0 | TEXT | 文本 | | 1 | SELECT | 单选 | | 2 | MULTI_SELECT | 多选 | | 3 | TIME | 时间控件 | | 6 | CASCADE | 级联 | | 7 | ATTACHMENT | 附件 |

FieldRequired - 字段必填

| 值 | 常量 | 说明 | |----|------|------| | 0 | OPTIONAL | 非必填 | | 1 | REQUIRED | 必填 | | 2 | CONDITIONAL | 条件必填 |

EvaluationModel / EvaluationValue - 满意度评价

| 枚举 | 值 | 常量 | |------|----|------| | EvaluationModel | 2/3/4/5 | TWO / THREE / FOUR / FIVE | | EvaluationValue | 0/1/25/50/75/100 | BAD / POOR / FAIR / GOOD / GREAT / EXCELLENT |

EvaluationStatus - 评价状态

| 值 | 常量 | 说明 | |----|------|------| | 0 | UNAVAILABLE | 不可评价 | | 1 | AVAILABLE | 可以评价 | | 2 | COMPLETED | 已完成 | | 3 | EXPIRED | 已过期 | | 4 | INVALID | 已失效 |

FollowerOpType - 关注人操作

| 值 | 常量 | 说明 | |----|------|------| | 0 | FOLLOW | 关注 | | 1 | UNFOLLOW | 取消关注 |

AutoAcceptSwitch - 自动分配开关

| 值 | 常量 | |----|------| | 0 | CLOSE | | 1 | OPEN |


StaffModule - 员工管理

list(params?)

获取客服列表。

const list = await client.staff.list();                                // 所有客服
const list = await client.staff.list({ status: StaffStatus.NORMAL });  // 按状态
const list = await client.staff.list({ role: StaffRole.NORMAL });      // 按角色

| 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | status | number | 否 | StaffStatus: 0=所有, 1=正常, 2=删除, 3=停用 | | role | number | 否 | StaffRole |

返回 Promise<Staff[]>

query(params)

条件查询客服信息(多条件全匹配)。

const result = await client.staff.query({ id: 123 });
const result = await client.staff.query({ email: '[email protected]' });

| 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | id | number | 条件其一 | 客服ID | | nickname | string | 条件其一 | 昵称 | | email | string | 条件其一 | 邮箱 | | phone | string | 条件其一 | 电话 | | workNumber | string | 条件其一 | 工号 | | realname | string | 条件其一 | 姓名 | | thirdPlatformUserId | string | 条件其一 | 三方客服ID | | thirdPlatformType | number | 否 | 三方渠道:11=企微, 12=飞书, 13=钉钉, 14=POPO |

返回 Promise<Staff[]>

getDetail(params)

查询单个客服详情。

const detail = await client.staff.getDetail({ id: 123 });

返回 Promise<Staff | null>

create(params)

创建客服。

import crypto from 'crypto';

const staffId = await client.staff.create({
  username: 'staff001',
  password: crypto.createHash('md5').update('password123').digest('hex'), // MD5 小写
  role: StaffRole.NORMAL,
  subRoleId: 12715018,
  realname: '张三',
  nickname: '小张',
  mobile: '13800138000',
  email: '[email protected]',
  maxSession: 10,
  groupIds: [1, 2],
  callEnable: 1,
  skillScoreChat: 5,
  skillScoreIpcc: 3,
});

| 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | username | string | 是 | 客服账号,字母开头,不超过20位 | | password | string | 是 | MD5加密后的小写32位字符串 | | role | number | 是 | StaffRole: 0=普通, 1=管理员, -1=工单客服, -2=呼叫客服 | | subRoleId | number | 是 | 角色ID(管理后台获取) | | realname | string | 否 | 不超过20字符 | | nickname | string | 否 | 不超过20字符 | | pinyin | string | 否 | 拼音 | | mobile | string | 否 | 联系电话 | | email | string | 否 | 邮箱 | | maxSession | number | 否 | 最大接待数 | | groupIds | number[] | 否 | 分组ID数组 | | callEnable | number | 否 | 电话权限 0=否 1=有 | | skillScoreChat | number | 否 | 在线会话技能值(0-10) | | skillScoreIpcc | number | 否 | 电话技能值(0-10) |

返回 Promise<number>(客服ID)。

update(params)

修改客服信息。

const ok = await client.staff.update({ id: 100, realname: '李四', maxSession: 15 });

参数同 createid 必填,其余可选。返回 Promise<boolean>

delete(id)

删除客服(不支持删除超管)。

const ok = await client.staff.delete(100);

返回 Promise<boolean>

updateStatus(id, status)

启用/停用客服。

await client.staff.updateStatus(100, StaffStatus.DISABLED); // 停用
await client.staff.updateStatus(100, StaffStatus.NORMAL);    // 启用

返回 Promise<boolean>

login(staffName)

服务端登录(免登录嵌入 iframe)。

const info = await client.staff.login('staff001');
// { sdk_url, corp_code, token }

返回 Promise<LoginResult | null>

listOnline(params?)

查询已登录客服信息(限每分钟500次)。

const online = await client.staff.listOnline();                       // 所有
const online = await client.staff.listOnline({ groupIds: [1, 2] });   // 按组

返回 Promise<OnlineStaff[]>

listThirdParty(params)

条件查询外部员工信息(企微/飞书/钉钉/POPO)。

const list = await client.staff.listThirdParty({
  platform: 1,     // 1=企微, 2=飞书, 3=钉钉, 4=POPO
  offset: 0,
  limit: 50,
});

返回 Promise<ThirdPartyStaff[]>

updateThirdParty(params)

批量操作外部员工。

const ok = await client.staff.updateThirdParty({
  type: 1,                      // 1=开启IM工单, 2=关闭IM工单, 3=开启IM负责人, 4=关闭
  corpMappingId: '7087393',
  userMappingIdList: [7239857],
});

返回 Promise<boolean>

createGroup(name)

创建客服组。

const groupId = await client.staff.createGroup('技术支持组');

name 不超过60字符。返回 Promise<number>(组ID)。

updateGroup(groupId, name)

修改客服组名称。

const ok = await client.staff.updateGroup(160, '高级技术支持组');

返回 Promise<boolean>

deleteGroup(groupId)

删除客服组。

const ok = await client.staff.deleteGroup(160);

返回 Promise<boolean>

assignToGroup(groupId, kefuIds)

分配客服到客服组。

const ok = await client.staff.assignToGroup(154, [101, 102, 103]);

返回 Promise<boolean>

listGroups(params?)

获取客服组列表。

const groups = await client.staff.listGroups();
const groups = await client.staff.listGroups({ staff: true, emptyGroup: true });

返回 Promise<StaffGroup[]>

listGroupMembers(groupId, role?)

获取客服组下员工列表。

const members = await client.staff.listGroupMembers(123);
const members = await client.staff.listGroupMembers(123, StaffRole.NORMAL);

返回 Promise<Staff[]>


CustomerGroupModule - 客户群管理

getDetail(params)

根据企微客户群的 chatId 查询群资料,包括群名称、群人数、群主、公告、负责人、企微主体、自定义字段、群成员和会话存档同意情况等信息。

const group = await client.customerGroup.getDetail({
  chatId: '<wechat-work-group-chat-id>',
});

if (group) {
  console.log(group.groupName);
  console.log(group.members);
}

| 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | chatId | string | 是 | 企微客户群的唯一 ID |

请求使用 POST /openapi/crm/group/queryGroupInfo。SDK 自动根据 JSON 请求体生成 appKeytimechecksum,返回值已解包为响应中的 data

返回 Promise<CustomerGroupInfo | null>。HTTP 状态码或业务响应码异常时会抛出错误。


TicketModule - 工单管理

create(params)

创建工单。

const ticketId = await client.ticket.create({
  title: '产品使用问题',
  content: '无法登录系统',
  staffId: 12345,            // 操作客服ID
  uid: 'user_001',           // 用户ID(与 userMobile 二选一)
  userName: '王小明',
  userMobile: '13800138000',
  userEmail: '[email protected]',
  priority: TicketPriority.NORMAL,
  templateId: 200,           // 工单模板ID
  typeId: 100,               // 工单分类ID
  targetStaffId: 12346,      // 指定客服(与 targetGroupId 二选一)
  followerIds: ['12347'],    // 关注人
  attachments: [{            // 附件(最多5个, 总大小≤30MB)
    fileName: '截图.png',
    type: 1,                 // 1=Base64
    payload: 'base64...',
  }],
  customFields: [{           // 自定义字段
    id: fieldId,
    fieldId,
    value: '选项值',
  }],
  properties: '{"key":"val"}', // 扩展属性JSON字符串
  fromType: 8,               // 8=邮件工单
});

| 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | title | string | 是 | 工单标题 | | content | string | 是 | 工单内容 | | staffId | number | 是 | 操作客服ID | | uid | string | 条件 | 用户ID(与 userMobile 二选一) | | userMobile | string | 条件 | 用户手机号 | | userName | string | 否 | 用户名称 | | userEmail | string | 否 | 用户邮箱 | | priority | number | 否 | TicketPriority: 2=低, 5=一般, 8=紧急, 10=非常紧急 | | templateId | number | 否 | 模板ID | | typeId | number | 否 | 分类ID | | targetStaffId | number | 否 | 处理客服(与 targetGroupId 二选一) | | targetGroupId | number | 否 | 处理客服组 | | followerIds | string[] | 否 | 关注人 | | attachments | Attachment[] | 否 | 附件 | | customFields | CustomField[] | 否 | 自定义字段 | | properties | string | 否 | 扩展属性,JSON 字符串格式 | | fromType | number | 否 | 来源,8=邮件工单 |

返回 Promise<number>(工单ID)。

modify(params)

通用修改工单接口(单字段修改)。

// type: 0=自定义字段, 1=标题, 2=优先级, 3=分类
const ok = await client.ticket.modify({
  ticketId: 12345, staffId: 100, type: 1, value: '新标题'
});

返回 Promise<boolean>

modifyTitle / modifyPriority / modifyCategory / modifyCustomField

modify 的便捷方法。

await client.ticket.modifyTitle(ticketId, staffId, '新标题');
await client.ticket.modifyPriority(ticketId, staffId, TicketPriority.URGENT);
await client.ticket.modifyCategory(ticketId, staffId, categoryId);
await client.ticket.modifyCustomField(ticketId, staffId, customId, fieldId, '值');

返回 Promise<boolean>

batchModify(params)

批量修改工单自定义字段(最多20个)。

const ok = await client.ticket.batchModify({
  ticketId: 12345,
  staffId: 100,
  customFields: [
    { id: customId1, fieldId: fieldId1, value: '值1' },
    { id: customId2, fieldId: fieldId2, value: '值2' },
  ],
});

返回 Promise<boolean>

apply(params)

申领工单。

const ok = await client.ticket.apply({ ticketId: 12345, staffId: 100 });

返回 Promise<boolean>

transfer(params)

转交工单。

const ok = await client.ticket.transfer({
  ticketId: 12345,
  staffId: 100,
  targetStaffId: 101,  // 与 targetGroupId 二选一
  comment: '转交说明',
  attachments: [],
});

返回 Promise<boolean>

reply(params)

回复工单。

const ok = await client.ticket.reply({
  ticketId: 12345,
  staffId: 100,
  comment: '已处理',
  customerSee: true,  // 访客是否可见
  attachments: [],
});

返回 Promise<boolean>

reminder(params)

催单。

const reminderId = await client.ticket.reminder({
  ticketId: 12345, staffId: 100, comment: '请尽快处理',
});

返回 Promise<number>(催单ID)。

finish(params)

完结工单。

const ok = await client.ticket.finish({
  ticketId: 12345,
  staffId: 100,
  comment: '问题已解决',
  nodeId: 5001,   // 画布模式必传
});

返回 Promise<boolean>

reopen(params)

重新打开工单。

const ok = await client.ticket.reopen({
  ticketId: 12345,
  staffId: 100,
  targetStaff: 101,   // 与 targetGroup 二选一
  comment: '需继续处理',
});

返回 Promise<boolean>

getTemplateList(params?)

获取工单模板列表。

const templates = await client.ticket.getTemplateList();                // 启用
const templates = await client.ticket.getTemplateList({ status: -1 });  // 全部

| status | 说明 | |--------|------| | -1 | 全部状态 | | 0 | 停用 | | 1 | 启用 | | 2 | 已删除 |

返回 Promise<TicketTemplate[]>

getTemplateFields(params)

获取模板字段列表。对于级联字段(type=6),cascadeData自动填充完整的级联树,无需额外调用 getCascadeField

const fields = await client.ticket.getTemplateFields({ templateId: 200 });

for (const f of fields) {
  if (f.type === FieldType.CASCADE && f.cascadeData) {
    // f.cascadeData 已包含完整级联树
    console.log(f.name, f.cascadeData[0].children);
  } else if (f.type === FieldType.SELECT) {
    // f.description 是选项数组 [{text: "选项1"}, ...]
    console.log(f.name, f.description);
  }
}

返回 Promise<TemplateField[]>

getFilterList(params?)

获取工单过滤器列表。

const filters = await client.ticket.getFilterList();                        // 管理员视角
const filters = await client.ticket.getFilterList({ staffId: 123, status: 1 });

返回 Promise<TicketFilter[]>

getFilterCount(params)

获取过滤器下的工单数量。

const count = await client.ticket.getFilterCount({ staffId: 123, filterId: 456 });

返回 Promise<number>

getTicketList(params)

获取过滤器下的工单列表。

const result = await client.ticket.getTicketList({
  staffId: 123, filterId: 456,
  limit: 20, offset: 0,
  sortBy: 'ct',    // ct=创建时间, pr=优先级, ut=最后操作时间
  order: 'desc',
});
// { total: 100, tickets: [...] }

limit 最大50。返回 Promise<TicketListResult>

searchTickets(params)

搜索工单(ticketId 与 mobile 二选一)。

const result = await client.ticket.searchTickets({
  ticketId: 12345,
  limit: 50, withCustomField: true, withMailReplyList: true,
});

// 或按手机号+时间范围
const result = await client.ticket.searchTickets({
  mobile: '13800138000',
  start: Date.now() - 30 * 86400000,
  end: Date.now(),
});

limit 最大500。返回 Promise<TicketListResult>

getTicketDetail(params)

获取工单详情。

const detail = await client.ticket.getTicketDetail({ ticketId: 12345 });
// detail.custom  — 自定义字段列表
// detail.status — TicketStatus

返回 Promise<TicketDetailResponse | null>

getTicketLogs(params)

获取工单日志。

const logs = await client.ticket.getTicketLogs({ ticketId: 12345 });

返回 Promise<TicketLogItem[]>

updateFollowers(params)

更新工单关注人。

const result = await client.ticket.updateFollowers({
  ticketId: 12345, staffId: 100,
  optype: FollowerOpType.FOLLOW,
  followers: ['101', '102'],
});

返回 Promise<UpdateFollowersResult>

getCascadeField(params)

获取级联字段的完整树(一般通过 getTemplateFieldscascadeData 自动获取)。

const tree = await client.ticket.getCascadeField({ fieldId: 4696492 });

返回 Promise<CascadeFieldResult>

getCategoryList(params)

获取工单分类列表。

const categories = await client.ticket.getCategoryList({ type: 2 });

返回 Promise<TicketCategory[]>

getTicketCategories()

获取所有工单分类的快捷方法(等价于 getCategoryList({ type: 2 }))。

const categories = await client.ticket.getTicketCategories();

返回 Promise<TicketCategory[]>

batchAutoAcceptConfig(params)

批量设置员工工单自动分配开关。

const result = await client.ticket.batchAutoAcceptConfig({
  itemList: [
    { staffId: 123, open: AutoAcceptSwitch.OPEN },
    { staffId: 456, open: AutoAcceptSwitch.CLOSE },
  ],
});

返回 Promise<BatchAutoAcceptConfigResult>

ticketCallback(params)

工单流程节点回调。

const ok = await client.ticket.ticketCallback({
  ticketId: 123, nodeExecRecordId: 456, conditionLineId: 789,
  result: true,
});

返回 Promise<boolean>

pushImCard(params)

推送三方渠道卡片消息。

const ok = await client.ticket.pushImCard({
  title: '提醒', content: '有工单', ticketId: 123,
  corpMappingId: 100, userMappingId: 200,
});

返回 Promise<boolean>

getEvaluationStatus(ticketId)

获取工单满意度评价状态。

const status = await client.ticket.getEvaluationStatus(12345);
// 0=不可评价, 1=可评价, 2=已完成, 3=已过期, 4=已失效

返回 Promise<number>,参见 EvaluationStatus 枚举。

getEvaluationRule(ticketId)

获取工单满意度评价规则。

const rule = await client.ticket.getEvaluationRule(12345);

返回 Promise<object | null>

submitEvaluation(params)

提交满意度评价。

const ok = await client.ticket.submitEvaluation({
  ticketId: 12345,
  evaluationValue: EvaluationValue.EXCELLENT,
  evaluationModel: EvaluationModel.FIVE,
  tagList: ['服务好'],
  content: '非常满意',
  evaluationWay: 4,   // 固定为 4
  isSolved: true,
});

返回 Promise<boolean>

getEvaluationResult(ticketId)

获取满意度评价结果。

const result = await client.ticket.getEvaluationResult(12345);

返回 Promise<object | null>


核心类型

CustomerGroupInfo

interface CustomerGroupInfo {
  archiveInfo: CustomerGroupArchiveInfo;
  chatId: string;
  createtime: number;          // 13 位毫秒时间戳
  customFieldValues: CustomerGroupCustomFieldValue[];
  customerMemberSize: number;
  groupIcon: string;
  groupName: string;
  groupNotice: string;
  lastSessionTime: number;     // 13 位毫秒时间戳
  leader: string;
  leaderId: number;
  memberSize: number;
  members: CustomerGroupMember[];
  ownerName: string;
  platformType: number;
  staffMemberSize: number;
  vipLevel: number;
  wxCorpId: string;
  wxCorpName: string;
}

CustomerGroupMember

interface CustomerGroupMember {
  userId: number;
  userGroupName: string;
  userGroupIcon: string;
  userGroupRole: number;
  escrow: number;
  friendFlag?: boolean;
}

CustomerGroupCustomFieldValue、CustomerGroupArchiveInfo

客户群详情中的自定义字段值和会话存档信息分别对应这两个类型。真实接口的自定义字段 value 返回字符串,选择项等结构化定义则以 JSON 字符串存放在 description 中。

Staff

interface Staff {
  id: number;
  username: string;
  realname?: string;
  nickname?: string;
  role: number;          // StaffRole
  roleId?: number;
  phone?: string;
  email?: string;
  status: number;        // StaffStatus: 1=正常, 2=删除, 3=停用
  createtime?: number;
  updateTime?: number;
  maxServiceCount?: number;
  skillScoreChat?: number;
  skillScoreIpcc?: number;
  workNumber?: string;
  staffGroupIds?: number[];
  groups?: StaffGroup[];
}

CustomField

interface CustomField {
  id: number;       // 字段实例ID(工单详情中的 custom.id)
  fieldId?: number; // 字段定义ID(模板中的 fieldId)
  value: string;    // 字段值
  name?: string;    // 字段名
}

Attachment

interface Attachment {
  fileName: string;  // 文件名
  type: number;      // 1=Base64编码
  payload: string;   // Base64 编码的文件内容
}

TicketDetailResponse

interface TicketDetailResponse {
  id: number;
  title: string;
  content: string;
  status: number;          // TicketStatus
  staffId: number;         // 创建人 ID
  staffName?: string;      // 创建人名称(缓存增强)
  holderId?: number;       // 受理人 ID
  holderName?: string;     // 受理人名称(缓存增强)
  templateId: number;
  templateName?: string;
  priority?: number;       // TicketPriority
  custom?: CustomField[];  // 自定义字段列表
  follower?: Record<number, string>; // 关注人(id → 名称)
  createTime: number;
  attachments?: Array<{ name: string; size: number; type: number; url: string }>;
  questionType?: { id: number; name: string; parent: number; path: string };
  // ... 更多字段
}

TicketDetail(列表/搜索)

interface TicketDetail {
  id: number;
  title: string;
  content: string;
  status: number;           // TicketStatus
  staffId: number;          // 创建人 ID
  staffName?: string;       // 创建人名称(缓存增强)
  holderId?: number;        // 受理人 ID
  holderName?: string;      // 受理人名称(缓存增强)
  lastFinishProcessor?: number;         // 最后完结人 ID
  lastFinishProcessorName?: string;     // 最后完结人名称(缓存增强)
  follower?: string[];                 // 关注人 ID 列表
  followerNames?: Record<number, string>; // 关注人名称映射(缓存增强)
  priority?: number;        // TicketPriority
  templateId: number;
  createTime: number;
  groupId?: number;
  // ... 更多字段
}

TemplateField

interface TemplateField {
  id: number;
  fieldId: number;
  name: string;
  required: number;    // FieldRequired: 0=非必填, 1=必填, 2=条件必填
  type: number;        // FieldType: 0=文本, 1=单选, 2=多选, 3=时间, 6=级联, 7=附件
  status: number;
  description?: any;   // 单选/多选的选项列表 [{text: "选项1"}, ...]
  prefill?: any;       // 预填值
  hint?: string;
  customer: number;    // 0=关, 1=开(访客填写)
  cascadeData?: CascadeFieldNode[]; // 级联字段完整树(自动填充)
}

CascadeFieldNode

interface CascadeFieldNode {
  id: number;
  name: string;
  children?: CascadeFieldNode[];
  disabled: boolean;
  highlight?: string;
  ai?: number;
  sort?: number;
  cloud?: boolean;
}

TicketListResult

interface TicketListResult {
  total: number;
  tickets: TicketDetail[];
}

错误码 (ErrorCode)

| 常量 | 值 | 说明 | |------|----|------| | OK | 200 | 成功 | | INVALID_APP_KEY | 14001 | 无效 appKey | | INVALID_CHECKSUM | 14002 | checksum 错误 | | INVALID_TIME | 14003 | 时间戳错误 | | INVALID_CONTENT | 14004 | 请求内容格式错误 | | INVALID_IP | 14008 | IP 不在白名单 | | RATE_LIMIT | 14009 | 请求频率超限 | | STAFF_NOT_EXIST | 14100 | 客服不存在 | | CATEGORY_NOT_EXIST | 14101 | 分类不存在 | | GROUP_NOT_EXIST | 14102 | 客服组不存在 | | USER_NOT_EXIST | 14105 | 用户不存在 | | TICKET_NOT_EXIST | 14106 | 工单不存在 | | TICKET_DUPLICATE | 14108 | 工单重复创建 | | TEMPLATE_NOT_EXIST | 14110 | 模板不存在 | | STAFF_ALREADY_EXISTS | 14704 | 客服账号已存在 | | STAFF_USERNAME_INVALID | 14705 | 客服账号不合法 | | SERVER_ERROR | 14500 | 服务器错误 | | DATA_TOO_LARGE | 14501 | 数据量过大 | | PERMISSION_DENIED | 14515 | 权限不足 | | SERVICE_UNAVAILABLE | 16001 | 服务不可用 | | TICKET_NO_PERMISSION | 8802 | 客服无权限 |


错误处理

SDK 自动检查 API 响应,code !== 200 时抛出 Error

try {
  await client.ticket.create({ ... });
} catch (error) {
  console.error(error.message);
  // "API Error 14110: 模板不存在"
}

timeout 超时未响应则抛出 AbortError。HTTP 状态码非 2xx 则抛出含响应体的 Error

TypeScript 支持

SDK 提供了完整的 TypeScript 类型定义:

import {
  QiyuKfClient,
  StaffCache,
  Staff,
  StaffGroup,
  CreateStaffParams,
  TicketDetail,
  TicketDetailResponse,
  CreateTicketParams,
  CustomerGroupInfo,
  CustomerGroupMember,
  CustomerGroupDetailParams,
  ErrorCode,
} from 'qiyu-openapi-sdk';

// 所有参数都有类型检查
const params: CreateTicketParams = {
  title: '测试工单',
  content: '内容',
  staffId: 123,
  userMobile: '13800138000',
};

const ticketId: number = await client.ticket.create(params);

开发与构建

# 安装依赖
npm install

# 开发模式(监听文件变化)
npm run dev

# 构建
npm run build

# 代码检查
npm run lint

# 代码格式化
npm run format

许可证

MIT License

相关链接