wip
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# 企业微信API配置示例
|
||||||
|
# 复制此文件为 .env 并填入实际的值
|
||||||
|
|
||||||
|
# 企业ID
|
||||||
|
WEIXIN_CORP_ID=${WEIXIN_CORP_ID}
|
||||||
|
|
||||||
|
# 应用的Secret
|
||||||
|
WEIXIN_CORP_SECRET=${WEIXIN_CORP_SECRET}
|
||||||
|
|
||||||
|
# API基础URL(可选,默认为 https://qyapi.weixin.qq.com)
|
||||||
|
WEIXIN_CORP_URL=https://qyapi.weixin.qq.com
|
||||||
|
|
||||||
|
# 企业成员userid(用于获取客户列表)
|
||||||
|
# 注意:这个成员需要配置了客户联系功能
|
||||||
|
WEIXIN_USER_ID=zhangsan
|
||||||
|
|
||||||
|
# 外部联系人ID(用于测试获取客户详情)
|
||||||
|
# 注意:这个ID需要通过其他接口(如获取客户列表)获取
|
||||||
|
# WEIXIN_EXTERNAL_USER_ID=''
|
||||||
|
|
||||||
|
WEIXIN_EVENT_TOKEN=${WEIXIN_EVENT_TOKEN};
|
||||||
|
WEIXIN_EVENT_ENCODING_AES_KEY=${WEIXIN_EVENT_ENCODING_AES_KEY};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
.environment/*
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "▶️:dev",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node",
|
||||||
|
"runtimeExecutable": "pnpm",
|
||||||
|
"args": [
|
||||||
|
"start",
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"envFile": "${workspaceFolder}/.environment/.env"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# 企业微信客户联系API Demo
|
||||||
|
|
||||||
|
这是一个使用企业微信客户联系API的TypeScript示例项目,演示如何获取客户列表和客户详情。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ 获取access_token(带自动缓存和过期管理)
|
||||||
|
- ✅ 获取配置了客户联系功能的成员列表
|
||||||
|
- ✅ 获取客户列表
|
||||||
|
- ✅ 获取客户详情
|
||||||
|
- ✅ 批量获取客户详情
|
||||||
|
- ✅ 支持分页获取(跟进人超过500人时)
|
||||||
|
- ✅ 完整的TypeScript类型定义
|
||||||
|
- ✅ 错误处理和响应验证
|
||||||
|
- ✅ 自动使用第一个成员获取客户列表
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
pg-demo/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.ts # 主程序入口
|
||||||
|
│ ├── wechat-client.ts # 企业微信API客户端
|
||||||
|
│ └── types/
|
||||||
|
│ └── wechat.ts # TypeScript类型定义
|
||||||
|
├── .env.example # 环境变量配置示例
|
||||||
|
├── package.json
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置环境变量
|
||||||
|
|
||||||
|
复制 `.env.example` 为 `.env` 并填入实际的值:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
WEIXIN_CORP_ID=your_corp_id
|
||||||
|
WEIXIN_CORP_SECRET=your_corp_secret
|
||||||
|
WEIXIN_EXTERNAL_USER_ID=your_external_user_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 运行程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 自动使用第一个配置了客户联系功能的成员
|
||||||
|
pnpm start
|
||||||
|
|
||||||
|
# 或者指定特定的企业成员userid
|
||||||
|
pnpm start <userid>
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```bash
|
||||||
|
# 自动使用第一个成员
|
||||||
|
pnpm start
|
||||||
|
|
||||||
|
# 指定特定成员
|
||||||
|
pnpm start zhangsan
|
||||||
|
```
|
||||||
|
|
||||||
|
程序会依次执行以下操作:
|
||||||
|
1. 获取access_token
|
||||||
|
2. 获取配置了客户联系功能的成员列表
|
||||||
|
3. 获取指定成员(或第一个成员)的客户列表
|
||||||
|
4. 获取第一个客户的详情
|
||||||
|
5. 批量获取所有客户详情
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 获取配置了客户联系功能的成员列表
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { WeChatClient } from './wechat-client';
|
||||||
|
|
||||||
|
// 创建客户端
|
||||||
|
const client = new WeChatClient({
|
||||||
|
corpId: process.env.WEIXIN_CORP_ID!,
|
||||||
|
corpSecret: process.env.WEIXIN_CORP_SECRET!,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取配置了客户联系功能的成员列表
|
||||||
|
const followUserList = await client.getFollowUserList();
|
||||||
|
|
||||||
|
console.log('成员数量:', followUserList.follow_user?.length || 0);
|
||||||
|
console.log('成员ID列表:', followUserList.follow_user);
|
||||||
|
|
||||||
|
// 使用第一个成员获取客户列表
|
||||||
|
if (followUserList.follow_user && followUserList.follow_user.length > 0) {
|
||||||
|
const firstUserId = followUserList.follow_user[0];
|
||||||
|
const contactList = await client.getExternalContactList(firstUserId);
|
||||||
|
console.log('客户数量:', contactList.external_userid?.length || 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取客户列表
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取指定成员的客户列表
|
||||||
|
const contactList = await client.getExternalContactList('zhangsan');
|
||||||
|
|
||||||
|
console.log('客户数量:', contactList.external_userid?.length || 0);
|
||||||
|
console.log('客户ID列表:', contactList.external_userid);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取客户详情
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取单个客户的详细信息
|
||||||
|
const contactDetail = await client.getExternalContact('external_user_id');
|
||||||
|
|
||||||
|
console.log('客户姓名:', contactDetail.external_contact.name);
|
||||||
|
console.log('客户类型:', contactDetail.external_contact.type === 1 ? '微信用户' : '企业微信用户');
|
||||||
|
console.log('跟进人数量:', contactDetail.follow_user.length);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量获取客户详情
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 批量获取客户详情(支持分页)
|
||||||
|
const batchContacts = await client.batchGetExternalContact('zhangsan', undefined, 100);
|
||||||
|
|
||||||
|
console.log('本次返回客户数量:', batchContacts.external_contact_list?.length || 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动分页获取所有客户
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 自动分页获取所有客户详情
|
||||||
|
const allContacts = await client.batchGetExternalContactFull('zhangsan');
|
||||||
|
|
||||||
|
console.log('总客户数量:', allContacts.length);
|
||||||
|
allContacts.forEach((contact, index) => {
|
||||||
|
console.log(`${index + 1}. ${contact.name} (${contact.external_userid})`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取客户详情(支持跟进人分页)
|
||||||
|
|
||||||
|
当跟进人超过500人时,使用 `getExternalContactFull` 方法自动获取所有跟进人:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fullDetail = await client.getExternalContactFull('external_user_id');
|
||||||
|
console.log('总跟进人数量:', fullDetail.follow_user.length);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API说明
|
||||||
|
|
||||||
|
### 获取配置了客户联系功能的成员列表
|
||||||
|
|
||||||
|
**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `access_token`: 调用接口凭证
|
||||||
|
|
||||||
|
**响应字段**:
|
||||||
|
- `follow_user`: 配置了客户联系功能的成员userid列表
|
||||||
|
|
||||||
|
### 获取客户列表
|
||||||
|
|
||||||
|
**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `access_token`: 调用接口凭证
|
||||||
|
- `userid`: 企业成员的userid
|
||||||
|
|
||||||
|
**响应字段**:
|
||||||
|
- `external_userid`: 外部联系人的userid列表
|
||||||
|
|
||||||
|
### 获取客户详情
|
||||||
|
|
||||||
|
**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `access_token`: 调用接口凭证
|
||||||
|
- `external_userid`: 外部联系人的userid
|
||||||
|
- `cursor`: (可选)上次请求返回的next_cursor,用于分页
|
||||||
|
|
||||||
|
**响应字段**:
|
||||||
|
- `external_contact`: 外部联系人详情
|
||||||
|
- `name`: 名称
|
||||||
|
- `type`: 1-微信用户,2-企业微信用户
|
||||||
|
- `gender`: 0-未知,1-男性,2-女性
|
||||||
|
- `position`: 职位(仅企业微信用户)
|
||||||
|
- `corp_name`: 企业简称(仅企业微信用户)
|
||||||
|
- `follow_user`: 添加了此外部联系人的企业成员列表
|
||||||
|
- `userid`: 成员userid
|
||||||
|
- `remark`: 备注
|
||||||
|
- `description`: 描述
|
||||||
|
- `createtime`: 添加时间
|
||||||
|
- `tags`: 标签列表
|
||||||
|
|
||||||
|
### 批量获取客户详情
|
||||||
|
|
||||||
|
**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `access_token`: 调用接口凭证
|
||||||
|
- `userid`: 企业成员的userid
|
||||||
|
- `cursor`: (可选)上次请求返回的next_cursor,用于分页
|
||||||
|
- `limit`: 返回的最大记录数,最大值1000,默认值为100
|
||||||
|
|
||||||
|
**响应字段**:
|
||||||
|
- `external_contact_list`: 外部联系人详情列表
|
||||||
|
- `next_cursor`: 下一次分页的起始位置
|
||||||
|
|
||||||
|
## 权限要求
|
||||||
|
|
||||||
|
应用需要配置到「客户联系 > 可调用接口的应用」中,并具有「客户基础信息」权限。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **access_token有效期**: 7200秒,客户端会自动缓存并提前5分钟刷新
|
||||||
|
2. **分页限制**: 当跟进人超过500人时,必须使用cursor参数分页获取
|
||||||
|
3. **字段限制**: 第三方应用无法获取头像、性别、备注手机号、unionid等字段
|
||||||
|
4. **external_userid**: 需要通过其他接口(如获取客户列表)获取
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [企业微信API文档 - 获取客户详情](https://developer.work.weixin.qq.com/document/path/101215)
|
||||||
|
- [企业微信API文档 - 获取access_token](https://developer.work.weixin.qq.com/document/path/91039)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* API错误
|
||||||
|
*/
|
||||||
|
export declare class WeChatError extends Error {
|
||||||
|
errcode: number;
|
||||||
|
errmsg: string;
|
||||||
|
constructor(errcode: number, errmsg: string);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wechat-error.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wechat-error.d.ts","sourceRoot":"","sources":["../../src/common/wechat-error.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAEvB,OAAO,EAAE,MAAM;IACf,MAAM,EAAE,MAAM;gBADd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM;CAI5B"}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* API错误
|
||||||
|
*/
|
||||||
|
export class WeChatError extends Error {
|
||||||
|
errcode;
|
||||||
|
errmsg;
|
||||||
|
constructor(errcode, errmsg) {
|
||||||
|
super(`企业微信API错误 [${errcode}]: ${errmsg}`);
|
||||||
|
this.errcode = errcode;
|
||||||
|
this.errmsg = errmsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wechat-error.js.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wechat-error.js","sourceRoot":"","sources":["../../src/common/wechat-error.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAEvB;IACA;IAFX,YACW,OAAe,EACf,MAAc;QAErB,KAAK,CAAC,cAAc,OAAO,MAAM,MAAM,EAAE,CAAC,CAAC;QAHpC,YAAO,GAAP,OAAO,CAAQ;QACf,WAAM,GAAN,MAAM,CAAQ;IAGzB,CAAC;CACJ"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=main.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { handleServer } from './server.js';
|
||||||
|
/**
|
||||||
|
* 主函数:演示如何使用企业微信客户联系API
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
await handleServer();
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
|
//# sourceMappingURL=main.js.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C;;GAEG;AACH,KAAK,UAAU,IAAI;IACf,MAAM,YAAY,EAAE,CAAC;AACzB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export declare function handleServer(): Promise<void>;
|
||||||
|
//# sourceMappingURL=server.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,wBAAsB,YAAY,kBAsHjC"}
|
||||||
Vendored
+95
@@ -0,0 +1,95 @@
|
|||||||
|
import { serve } from "@hono/node-server";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { getSignature, decrypt, encrypt } from '@wecom/crypto';
|
||||||
|
import { parseStringPromise } from 'xml2js';
|
||||||
|
function validateSignature(msgSignature, timestamp, nonce, encrypt) {
|
||||||
|
const { WEIXIN_EVENT_TOKEN, } = process.env;
|
||||||
|
if (!WEIXIN_EVENT_TOKEN)
|
||||||
|
throw new Error('static param error');
|
||||||
|
const devMsgSignature = getSignature(WEIXIN_EVENT_TOKEN, timestamp, nonce, encrypt);
|
||||||
|
return devMsgSignature === msgSignature;
|
||||||
|
}
|
||||||
|
export async function handleServer() {
|
||||||
|
const app = new Hono();
|
||||||
|
app.get('/we-comp/event', ({ req, ...args }) => {
|
||||||
|
try {
|
||||||
|
const { 'msg_signature': msgSignature, timestamp, nonce, echostr } = req.query();
|
||||||
|
const { WEIXIN_EVENT_TOKEN, WEIXIN_EVENT_ENCODING_AES_KEY, } = process.env;
|
||||||
|
if (!WEIXIN_EVENT_TOKEN
|
||||||
|
|| !WEIXIN_EVENT_ENCODING_AES_KEY
|
||||||
|
|| !msgSignature
|
||||||
|
|| !timestamp
|
||||||
|
|| !nonce
|
||||||
|
|| !echostr) {
|
||||||
|
throw new Error('static param error');
|
||||||
|
}
|
||||||
|
if (!validateSignature(msgSignature, timestamp, nonce, echostr)) {
|
||||||
|
throw new Error('validate error');
|
||||||
|
}
|
||||||
|
const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, echostr);
|
||||||
|
console.log('URL验证成功,解密后的消息:', message);
|
||||||
|
console.log('接收者ID:', id);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return args.text("");
|
||||||
|
});
|
||||||
|
app.post('/we-comp/event', async ({ req, ...args }) => {
|
||||||
|
const { 'msg_signature': msgSignature, timestamp, nonce } = req.query();
|
||||||
|
try {
|
||||||
|
const result = await parseStringPromise(await req.text());
|
||||||
|
const encrypt = result.xml.Encrypt.at(0);
|
||||||
|
const { WEIXIN_EVENT_TOKEN, WEIXIN_EVENT_ENCODING_AES_KEY, } = process.env;
|
||||||
|
if (!WEIXIN_EVENT_TOKEN
|
||||||
|
|| !WEIXIN_EVENT_ENCODING_AES_KEY
|
||||||
|
|| !msgSignature
|
||||||
|
|| !timestamp
|
||||||
|
|| !nonce) {
|
||||||
|
throw new Error('static param error');
|
||||||
|
}
|
||||||
|
// 2. 验证签名
|
||||||
|
if (!validateSignature(msgSignature, timestamp, nonce, encrypt)) {
|
||||||
|
throw new Error('签名验证失败');
|
||||||
|
}
|
||||||
|
const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, encrypt);
|
||||||
|
console.log('解密后的消息:', message);
|
||||||
|
console.log('接收者ID:', id);
|
||||||
|
// 5. 解析明文XML
|
||||||
|
const decryptedXml = await parseStringPromise(message);
|
||||||
|
const msgData = decryptedXml.xml;
|
||||||
|
const msgType = msgData.MsgType?.[0];
|
||||||
|
const fromUserName = msgData.FromUserName?.[0];
|
||||||
|
const toUserName = msgData.ToUserName?.[0];
|
||||||
|
const createTime = msgData.CreateTime?.[0];
|
||||||
|
console.log('========== 消息详情 ==========');
|
||||||
|
console.log('消息类型:', msgType);
|
||||||
|
console.log('发送者:', fromUserName);
|
||||||
|
console.log('接收者:', toUserName);
|
||||||
|
console.log('创建时间:', createTime);
|
||||||
|
console.log('============================');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return args.text('');
|
||||||
|
});
|
||||||
|
const server = serve({
|
||||||
|
fetch: app.fetch,
|
||||||
|
port: 3080,
|
||||||
|
});
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
server.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=server.js.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAG5C,SAAS,iBAAiB,CAAC,YAAoB,EAAE,SAAiB,EAAE,KAAa,EAAE,OAAe;IAC9F,MAAM,EACF,kBAAkB,GACrB,GAAG,OAAO,CAAC,GAAG,CAAC;IAChB,IAAI,CAAC,kBAAkB;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC/D,MAAM,eAAe,GAAG,YAAY,CAAC,kBAAkB,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACpF,OAAO,eAAe,KAAK,YAAY,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAE9B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE;QAE3C,IAAI,CAAC;YACD,MAAM,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC;YAEjF,MAAM,EACF,kBAAkB,EAClB,6BAA6B,GAChC,GAAG,OAAO,CAAC,GAAG,CAAC;YAEhB,IACI,CAAC,kBAAkB;mBAChB,CAAC,6BAA6B;mBAC9B,CAAC,YAAY;mBACb,CAAC,SAAS;mBACV,CAAC,KAAK;mBACN,CAAC,OAAO,EACb,CAAC;gBACC,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;YAC1C,CAAC;YAED,IAAI,CAAC,iBAAiB,CAAC,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC9D,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;YACtC,CAAC;YAED,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,6BAA6B,EAAE,OAAO,CAAC,CAAC;YAExE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE;QAElD,MAAM,EACF,eAAe,EAAE,YAAY,EAC7B,SAAS,EACT,KAAK,EACR,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC;QAEhB,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAEzC,MAAM,EACF,kBAAkB,EAClB,6BAA6B,GAChC,GAAG,OAAO,CAAC,GAAG,CAAC;YAEhB,IACI,CAAC,kBAAkB;mBAChB,CAAC,6BAA6B;mBAC9B,CAAC,YAAY;mBACb,CAAC,SAAS;mBACV,CAAC,KAAK,EACX,CAAC;gBACC,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;YAC1C,CAAC;YAED,UAAU;YACV,IAAI,CAAC,iBAAiB,CAAC,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC9D,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC9B,CAAC;YAED,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,6BAA6B,EAAE,OAAO,CAAC,CAAC;YACxE,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YAG1B,aAAa;YACb,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC;YACvD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC;YAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;YAC/C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;YAE3C,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;YAC1C,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;QAEhD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC;QAGD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzB,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,KAAK,CAAC;QACjB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,IAAI,EAAE,IAAI;KACb,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACtB,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACnB,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACjB,IAAI,GAAG,EAAE,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACnB,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,CAAC,CAAC,CAAA;IACN,CAAC,CAAC,CAAA;AAEN,CAAC"}
|
||||||
Vendored
+343
@@ -0,0 +1,343 @@
|
|||||||
|
/**
|
||||||
|
* 企业微信客户联系 API 类型定义
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 外部联系人详情
|
||||||
|
*/
|
||||||
|
export interface ExternalContact {
|
||||||
|
/** 外部联系人userid */
|
||||||
|
external_userid: string;
|
||||||
|
/** 名称(微信用户返回昵称,企业微信用户返回对外展示名称) */
|
||||||
|
name: string;
|
||||||
|
/** 头像URL(部分应用不可获取) */
|
||||||
|
avatar?: string;
|
||||||
|
/** 1-微信用户,2-企业微信用户 */
|
||||||
|
type: 1 | 2;
|
||||||
|
/** 0-未知,1-男性,2-女性 */
|
||||||
|
gender: 0 | 1 | 2;
|
||||||
|
/** 微信unionid(需绑定微信开发者ID) */
|
||||||
|
unionid?: string;
|
||||||
|
/** 职位(仅企业微信用户) */
|
||||||
|
position?: string;
|
||||||
|
/** 企业简称(仅企业微信用户) */
|
||||||
|
corp_name?: string;
|
||||||
|
/** 企业主体名称(仅企业微信用户) */
|
||||||
|
corp_full_name?: string;
|
||||||
|
/** 自定义展示信息(仅企业微信用户) */
|
||||||
|
external_profile?: ExternalProfile;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 外部联系人自定义展示信息
|
||||||
|
*/
|
||||||
|
export interface ExternalProfile {
|
||||||
|
/** 自定义展示字段列表 */
|
||||||
|
external_attr: ExternalAttr[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 自定义展示字段
|
||||||
|
*/
|
||||||
|
export interface ExternalAttr {
|
||||||
|
/** 0-文本,1-网页,2-小程序 */
|
||||||
|
type: number;
|
||||||
|
/** 属性名称 */
|
||||||
|
name: string;
|
||||||
|
/** 文本内容(type=0时) */
|
||||||
|
text?: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
/** 网页内容(type=1时) */
|
||||||
|
web?: {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
/** 小程序内容(type=2时) */
|
||||||
|
miniprogram?: {
|
||||||
|
appid: string;
|
||||||
|
pagepath: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 标签信息
|
||||||
|
*/
|
||||||
|
export interface Tag {
|
||||||
|
/** 标签分组名称 */
|
||||||
|
group_name: string;
|
||||||
|
/** 标签名称 */
|
||||||
|
tag_name: string;
|
||||||
|
/** 企业标签ID(用户自定义标签不返回) */
|
||||||
|
tag_id?: string;
|
||||||
|
/** 1-企业设置,2-用户自定义,3-规则组标签 */
|
||||||
|
type: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 视频号信息
|
||||||
|
*/
|
||||||
|
export interface WechatChannels {
|
||||||
|
/** 视频号名称 */
|
||||||
|
nickname: string;
|
||||||
|
/** 0-未知,1-主页,2-直播间,3-留资服务 */
|
||||||
|
source: 0 | 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 跟进用户信息
|
||||||
|
*/
|
||||||
|
export interface FollowUser {
|
||||||
|
/** 成员userid */
|
||||||
|
userid: string;
|
||||||
|
/** 该成员对客户的备注 */
|
||||||
|
remark: string;
|
||||||
|
/** 该成员对客户的描述 */
|
||||||
|
description: string;
|
||||||
|
/** 添加时间(Unix时间戳) */
|
||||||
|
createtime: number;
|
||||||
|
/** 标签列表 */
|
||||||
|
tags: Tag[];
|
||||||
|
/** 备注的企业名称 */
|
||||||
|
remark_corp_name: string;
|
||||||
|
/** 备注的手机号(部分应用不可获取) */
|
||||||
|
remark_mobiles?: string[];
|
||||||
|
/** 添加来源 */
|
||||||
|
add_way: number;
|
||||||
|
/** 视频号信息(add_way=10时有此字段) */
|
||||||
|
wechat_channels?: WechatChannels;
|
||||||
|
/** 发起添加的userid */
|
||||||
|
oper_userid: string;
|
||||||
|
/** 自定义state参数(联系我/获客链接) */
|
||||||
|
state?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取客户详情API响应
|
||||||
|
*/
|
||||||
|
export interface GetExternalContactResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 外部联系人详情 */
|
||||||
|
external_contact: ExternalContact;
|
||||||
|
/** 添加了此外部联系人的企业成员列表 */
|
||||||
|
follow_user: FollowUser[];
|
||||||
|
/** 分页cursor(跟进人>500时返回) */
|
||||||
|
next_cursor?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取access_token API响应
|
||||||
|
*/
|
||||||
|
export interface AccessTokenResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 获取到的凭证 */
|
||||||
|
access_token?: string;
|
||||||
|
/** 凭证有效期(秒) */
|
||||||
|
expires_in?: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取配置了客户联系功能的成员列表API响应
|
||||||
|
*/
|
||||||
|
export interface GetFollowUserListResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 配置了客户联系功能的成员userid列表 */
|
||||||
|
follow_user?: string[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取客户列表API响应
|
||||||
|
*/
|
||||||
|
export interface GetExternalContactListResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 外部联系人的userid列表 */
|
||||||
|
external_userid?: string[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情API响应
|
||||||
|
*/
|
||||||
|
export interface BatchGetExternalContactResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 外部联系人详情列表 */
|
||||||
|
external_contact_list?: ExternalContact[];
|
||||||
|
/** 下一次分页的起始位置 */
|
||||||
|
next_cursor?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 结束语文本内容
|
||||||
|
*/
|
||||||
|
export interface ConclusionText {
|
||||||
|
/** 文本内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 结束语
|
||||||
|
*/
|
||||||
|
export interface Conclusion {
|
||||||
|
/** 文本结束语 */
|
||||||
|
text?: ConclusionText;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface AddContactWayRequest {
|
||||||
|
/** 联系方式类型,1-单人,2-多人 */
|
||||||
|
type: 1 | 2;
|
||||||
|
/** 场景,1-在小程序中联系,2-通过二维码联系 */
|
||||||
|
scene: 1 | 2;
|
||||||
|
/** 在小程序中联系时使用的控件样式 */
|
||||||
|
style?: number;
|
||||||
|
/** 联系方式的备注信息,用于助记,不超过30个字符 */
|
||||||
|
remark?: string;
|
||||||
|
/** 外部客户添加时是否无需验证,默认为true */
|
||||||
|
skip_verify?: boolean;
|
||||||
|
/** 企业自定义的state参数,用于区分不同的添加渠道 */
|
||||||
|
state?: string;
|
||||||
|
/** 使用该联系方式的用户userID列表,在type为1时为必填,且只能有一个 */
|
||||||
|
user?: string[];
|
||||||
|
/** 使用该联系方式的部门id列表,只在type为2时有效 */
|
||||||
|
party?: number[];
|
||||||
|
/** 是否临时会话模式 */
|
||||||
|
is_temp?: boolean;
|
||||||
|
/** 临时会话二维码有效期,以秒为单位 */
|
||||||
|
expires_in?: number;
|
||||||
|
/** 临时会话有效期,以秒为单位 */
|
||||||
|
chat_expires_in?: number;
|
||||||
|
/** 可进行临时会话的客户unionid */
|
||||||
|
unionid?: string;
|
||||||
|
/** 是否开启同一外部企业客户只能添加同一个员工 */
|
||||||
|
is_exclusive?: boolean;
|
||||||
|
/** 是否标记客户添加来源 */
|
||||||
|
mark_source?: boolean;
|
||||||
|
/** 结束语 */
|
||||||
|
conclusions?: Conclusion;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface AddContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id?: string;
|
||||||
|
/** 联系方式二维码的URL,开发者需要自行将此二维码转换为图片 */
|
||||||
|
qr_code?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface GetContactWayRequest {
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface GetContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 联系方式配置信息 */
|
||||||
|
contact_way?: ContactWayInfo;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 联系方式配置信息
|
||||||
|
*/
|
||||||
|
export interface ContactWayInfo {
|
||||||
|
/** 联系方式配置id */
|
||||||
|
config_id: string;
|
||||||
|
/** 联系方式类型 */
|
||||||
|
type: 1 | 2;
|
||||||
|
/** 场景 */
|
||||||
|
scene: 1 | 2;
|
||||||
|
/** 在小程序中联系时使用的控件样式 */
|
||||||
|
style?: number;
|
||||||
|
/** 联系方式的备注信息 */
|
||||||
|
remark?: string;
|
||||||
|
/** 是否无需验证 */
|
||||||
|
skip_verify: boolean;
|
||||||
|
/** state参数 */
|
||||||
|
state?: string;
|
||||||
|
/** 使用该联系方式的用户userID列表 */
|
||||||
|
user?: string[];
|
||||||
|
/** 使用该联系方式的部门id列表 */
|
||||||
|
party?: number[];
|
||||||
|
/** 是否临时会话模式 */
|
||||||
|
is_temp: boolean;
|
||||||
|
/** 临时会话二维码有效期 */
|
||||||
|
expires_in?: number;
|
||||||
|
/** 临时会话有效期 */
|
||||||
|
chat_expires_in?: number;
|
||||||
|
/** 临时会话指定的客户unionid */
|
||||||
|
unionid?: string;
|
||||||
|
/** 是否开启同一外部企业客户只能添加同一个员工 */
|
||||||
|
is_exclusive: boolean;
|
||||||
|
/** 结束语 */
|
||||||
|
conclusions?: Conclusion;
|
||||||
|
/** 二维码的URL */
|
||||||
|
qr_code?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface UpdateContactWayRequest {
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id: string;
|
||||||
|
/** 在小程序中联系时使用的控件样式 */
|
||||||
|
style?: number;
|
||||||
|
/** 联系方式的备注信息 */
|
||||||
|
remark?: string;
|
||||||
|
/** 是否无需验证 */
|
||||||
|
skip_verify?: boolean;
|
||||||
|
/** 使用该联系方式的用户userID列表 */
|
||||||
|
user?: string[];
|
||||||
|
/** 使用该联系方式的部门id列表 */
|
||||||
|
party?: number[];
|
||||||
|
/** 是否临时会话模式 */
|
||||||
|
is_temp?: boolean;
|
||||||
|
/** 临时会话二维码有效期 */
|
||||||
|
expires_in?: number;
|
||||||
|
/** 临时会话有效期 */
|
||||||
|
chat_expires_in?: number;
|
||||||
|
/** 临时会话指定的客户unionid */
|
||||||
|
unionid?: string;
|
||||||
|
/** 是否开启同一外部企业客户只能添加同一个员工 */
|
||||||
|
is_exclusive?: boolean;
|
||||||
|
/** 结束语 */
|
||||||
|
conclusions?: Conclusion;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface UpdateContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface DeleteContactWayRequest {
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface DeleteContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wechat.d.ts.map
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* 企业微信客户联系 API 类型定义
|
||||||
|
*/
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=wechat.js.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wechat.js","sourceRoot":"","sources":["../../src/types/wechat.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
||||||
Vendored
+93
@@ -0,0 +1,93 @@
|
|||||||
|
import type { GetExternalContactResponse, GetExternalContactListResponse, GetFollowUserListResponse, BatchGetExternalContactResponse, ExternalContact, AddContactWayRequest, AddContactWayResponse, GetContactWayRequest, GetContactWayResponse, UpdateContactWayRequest, UpdateContactWayResponse, DeleteContactWayRequest, DeleteContactWayResponse } from './types/wechat.js';
|
||||||
|
/**
|
||||||
|
* 企业微信API客户端配置
|
||||||
|
*/
|
||||||
|
export interface WeChatClientConfig {
|
||||||
|
/** 企业ID */
|
||||||
|
corpId: string;
|
||||||
|
/** 应用的secret */
|
||||||
|
corpSecret: string;
|
||||||
|
/** API基础URL */
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 企业微信API客户端
|
||||||
|
*/
|
||||||
|
export declare class WeChatClient {
|
||||||
|
private config;
|
||||||
|
private accessToken;
|
||||||
|
private tokenExpireTime;
|
||||||
|
constructor(config: WeChatClientConfig);
|
||||||
|
/**
|
||||||
|
* 获取access_token
|
||||||
|
* @returns access_token
|
||||||
|
*/
|
||||||
|
getAccessToken(): Promise<string>;
|
||||||
|
/**
|
||||||
|
* 获取客户详情
|
||||||
|
* @param externalUserId 外部联系人的userid
|
||||||
|
* @param cursor 上次请求返回的next_cursor,用于分页
|
||||||
|
* @returns 客户详情
|
||||||
|
*/
|
||||||
|
getExternalContact(externalUserId: string, cursor?: string): Promise<GetExternalContactResponse>;
|
||||||
|
/**
|
||||||
|
* 获取客户详情(支持分页,自动获取所有跟进人)
|
||||||
|
* @param externalUserId 外部联系人的userid
|
||||||
|
* @returns 客户详情(包含所有跟进人)
|
||||||
|
*/
|
||||||
|
getExternalContactFull(externalUserId: string): Promise<GetExternalContactResponse>;
|
||||||
|
/**
|
||||||
|
* 获取配置了客户联系功能的成员列表
|
||||||
|
* @returns 配置了客户联系功能的成员userid列表
|
||||||
|
*/
|
||||||
|
getFollowUserList(): Promise<GetFollowUserListResponse>;
|
||||||
|
/**
|
||||||
|
* 获取客户列表
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @returns 外部联系人的userid列表
|
||||||
|
*/
|
||||||
|
getExternalContactList(userId: string): Promise<GetExternalContactListResponse>;
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @param cursor 上次请求返回的next_cursor,用于分页
|
||||||
|
* @param limit 返回的最大记录数,整型,最大值1000,默认值为100
|
||||||
|
* @returns 客户详情列表
|
||||||
|
*/
|
||||||
|
batchGetExternalContact(userId: string, cursor?: string, limit?: number): Promise<BatchGetExternalContactResponse>;
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情(自动分页,获取所有客户)
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @returns 所有客户详情列表
|
||||||
|
*/
|
||||||
|
batchGetExternalContactFull(userId: string): Promise<ExternalContact[]>;
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式
|
||||||
|
* @param params 联系方式配置参数
|
||||||
|
* @returns 联系方式配置信息(包含config_id和qr_code)
|
||||||
|
*/
|
||||||
|
addContactWay(params: AddContactWayRequest): Promise<AddContactWayResponse>;
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式
|
||||||
|
* @param params 查询参数(包含config_id)
|
||||||
|
* @returns 联系方式配置信息
|
||||||
|
*/
|
||||||
|
getContactWay(params: GetContactWayRequest): Promise<GetContactWayResponse>;
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式
|
||||||
|
* @param params 更新参数(包含config_id和要更新的字段)
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
updateContactWay(params: UpdateContactWayRequest): Promise<UpdateContactWayResponse>;
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式
|
||||||
|
* @param params 删除参数(包含config_id)
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
deleteContactWay(params: DeleteContactWayRequest): Promise<DeleteContactWayResponse>;
|
||||||
|
/**
|
||||||
|
* 重置access_token(强制重新获取)
|
||||||
|
*/
|
||||||
|
resetAccessToken(): void;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wechat-client.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wechat-client.d.ts","sourceRoot":"","sources":["../src/wechat-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,0BAA0B,EAC1B,8BAA8B,EAC9B,yBAAyB,EACzB,+BAA+B,EAC/B,eAAe,EACf,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW;IACX,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,eAAe,CAAqB;gBAEhC,MAAM,EAAE,kBAAkB;IAOtC;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAwBvC;;;;;OAKG;IACG,kBAAkB,CACtB,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,0BAA0B,CAAC;IAwBtC;;;;OAIG;IACG,sBAAsB,CAC1B,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,0BAA0B,CAAC;IAyBtC;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,yBAAyB,CAAC;IAoB7D;;;;OAIG;IACG,sBAAsB,CAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,8BAA8B,CAAC;IAqB1C;;;;;;OAMG;IACG,uBAAuB,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,GAAE,MAAY,GAClB,OAAO,CAAC,+BAA+B,CAAC;IAyB3C;;;;OAIG;IACG,2BAA2B,CAC/B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,EAAE,CAAC;IAe7B;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,qBAAqB,CAAC;IA2BjC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,qBAAqB,CAAC;IA2BjC;;;;OAIG;IACG,gBAAgB,CACpB,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,wBAAwB,CAAC;IA2BpC;;;;OAIG;IACG,gBAAgB,CACpB,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,wBAAwB,CAAC;IA2BpC;;OAEG;IACH,gBAAgB,IAAI,IAAI;CAIzB"}
|
||||||
Vendored
+287
@@ -0,0 +1,287 @@
|
|||||||
|
/**
|
||||||
|
* 企业微信 API 客户端
|
||||||
|
*/
|
||||||
|
import { WeChatError } from './common/wechat-error.js';
|
||||||
|
/**
|
||||||
|
* 企业微信API客户端
|
||||||
|
*/
|
||||||
|
export class WeChatClient {
|
||||||
|
config;
|
||||||
|
accessToken;
|
||||||
|
tokenExpireTime;
|
||||||
|
constructor(config) {
|
||||||
|
this.config = {
|
||||||
|
baseUrl: process.env.WEIXIN_CORP_URL ?? 'https://qyapi.weixin.qq.com',
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取access_token
|
||||||
|
* @returns access_token
|
||||||
|
*/
|
||||||
|
async getAccessToken() {
|
||||||
|
// 检查token是否有效
|
||||||
|
if (this.accessToken && this.tokenExpireTime && Date.now() < this.tokenExpireTime) {
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
const url = new URL('/cgi-bin/gettoken', this.config.baseUrl);
|
||||||
|
url.searchParams.set('corpid', this.config.corpId);
|
||||||
|
url.searchParams.set('corpsecret', this.config.corpSecret);
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.errcode !== 0 || !data.access_token) {
|
||||||
|
throw new Error(`获取access_token失败: ${data.errmsg}`);
|
||||||
|
}
|
||||||
|
this.accessToken = data.access_token;
|
||||||
|
// 提前5分钟过期,避免临界点问题
|
||||||
|
this.tokenExpireTime = Date.now() + (data.expires_in || 7200) * 1000 - 5 * 60 * 1000;
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取客户详情
|
||||||
|
* @param externalUserId 外部联系人的userid
|
||||||
|
* @param cursor 上次请求返回的next_cursor,用于分页
|
||||||
|
* @returns 客户详情
|
||||||
|
*/
|
||||||
|
async getExternalContact(externalUserId, cursor) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/get', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
url.searchParams.set('external_userid', externalUserId);
|
||||||
|
if (cursor) {
|
||||||
|
url.searchParams.set('cursor', cursor);
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取客户详情(支持分页,自动获取所有跟进人)
|
||||||
|
* @param externalUserId 外部联系人的userid
|
||||||
|
* @returns 客户详情(包含所有跟进人)
|
||||||
|
*/
|
||||||
|
async getExternalContactFull(externalUserId) {
|
||||||
|
let result = null;
|
||||||
|
let cursor;
|
||||||
|
do {
|
||||||
|
const data = await this.getExternalContact(externalUserId, cursor);
|
||||||
|
if (!result) {
|
||||||
|
// 第一次请求,直接使用返回的数据
|
||||||
|
result = data;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 后续请求,合并follow_user列表
|
||||||
|
result.follow_user.push(...data.follow_user);
|
||||||
|
// 更新next_cursor
|
||||||
|
if (data.next_cursor !== undefined) {
|
||||||
|
result.next_cursor = data.next_cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor = data.next_cursor;
|
||||||
|
} while (cursor);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取配置了客户联系功能的成员列表
|
||||||
|
* @returns 配置了客户联系功能的成员userid列表
|
||||||
|
*/
|
||||||
|
async getFollowUserList() {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/get_follow_user_list', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取客户列表
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @returns 外部联系人的userid列表
|
||||||
|
*/
|
||||||
|
async getExternalContactList(userId) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/list', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
url.searchParams.set('userid', userId);
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @param cursor 上次请求返回的next_cursor,用于分页
|
||||||
|
* @param limit 返回的最大记录数,整型,最大值1000,默认值为100
|
||||||
|
* @returns 客户详情列表
|
||||||
|
*/
|
||||||
|
async batchGetExternalContact(userId, cursor, limit = 100) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/batch/get_by_user', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
url.searchParams.set('userid', userId);
|
||||||
|
if (cursor) {
|
||||||
|
url.searchParams.set('cursor', cursor);
|
||||||
|
}
|
||||||
|
url.searchParams.set('limit', limit.toString());
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情(自动分页,获取所有客户)
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @returns 所有客户详情列表
|
||||||
|
*/
|
||||||
|
async batchGetExternalContactFull(userId) {
|
||||||
|
const allContacts = [];
|
||||||
|
let cursor;
|
||||||
|
do {
|
||||||
|
const data = await this.batchGetExternalContact(userId, cursor);
|
||||||
|
if (data.external_contact_list) {
|
||||||
|
allContacts.push(...data.external_contact_list);
|
||||||
|
}
|
||||||
|
cursor = data.next_cursor;
|
||||||
|
} while (cursor);
|
||||||
|
return allContacts;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式
|
||||||
|
* @param params 联系方式配置参数
|
||||||
|
* @returns 联系方式配置信息(包含config_id和qr_code)
|
||||||
|
*/
|
||||||
|
async addContactWay(params) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/add_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式
|
||||||
|
* @param params 查询参数(包含config_id)
|
||||||
|
* @returns 联系方式配置信息
|
||||||
|
*/
|
||||||
|
async getContactWay(params) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/get_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式
|
||||||
|
* @param params 更新参数(包含config_id和要更新的字段)
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateContactWay(params) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/update_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式
|
||||||
|
* @param params 删除参数(包含config_id)
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteContactWay(params) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/del_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重置access_token(强制重新获取)
|
||||||
|
*/
|
||||||
|
resetAccessToken() {
|
||||||
|
this.accessToken = undefined;
|
||||||
|
this.tokenExpireTime = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wechat-client.js.map
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export declare function handleWecomp(): Promise<void>;
|
||||||
|
//# sourceMappingURL=wecomp.d.ts.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wecomp.d.ts","sourceRoot":"","sources":["../src/wecomp.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,kBA2DjC"}
|
||||||
Vendored
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import { WeChatClient } from "./wechat-client.js";
|
||||||
|
export async function handleWecomp() {
|
||||||
|
// 检查环境变量
|
||||||
|
if (process.env.WEIXIN_CORP_ID == null ||
|
||||||
|
process.env.WEIXIN_CORP_SECRET == null) {
|
||||||
|
throw new Error('环境变量错误:请设置 WEIXIN_CORP_ID 和 WEIXIN_CORP_SECRET');
|
||||||
|
}
|
||||||
|
// 创建企业微信客户端
|
||||||
|
const client = new WeChatClient({
|
||||||
|
corpId: process.env.WEIXIN_CORP_ID,
|
||||||
|
corpSecret: process.env.WEIXIN_CORP_SECRET,
|
||||||
|
baseUrl: process.env.WEIXIN_CORP_URL || 'https://qyapi.weixin.qq.com',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// 示例:获取配置了客户联系功能的成员列表
|
||||||
|
console.log('\n=== 获取配置了客户联系功能的成员列表 ===');
|
||||||
|
const followUserList = await client.getFollowUserList();
|
||||||
|
console.log('\n--- 成员列表 ---');
|
||||||
|
console.log('成员数量:', followUserList.follow_user?.length || 0);
|
||||||
|
if (!followUserList.follow_user || followUserList.follow_user.length === 0) {
|
||||||
|
console.log('没有配置了客户联系功能的成员');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('成员ID列表:');
|
||||||
|
followUserList.follow_user.forEach((id, index) => {
|
||||||
|
console.log(` ${index + 1}. ${id}`);
|
||||||
|
});
|
||||||
|
let userId = 'YeDeLiang';
|
||||||
|
if (!userId) {
|
||||||
|
console.log('错误:无法获取有效的成员userid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await client.getContactWay({
|
||||||
|
config_id: 'd5990127d833c10d9db8ba7f192e7b19'
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error('发生错误:', error.message);
|
||||||
|
const err = error;
|
||||||
|
if (err.errcode !== undefined) {
|
||||||
|
console.error('错误码:', err.errcode);
|
||||||
|
}
|
||||||
|
if (err.errmsg !== undefined) {
|
||||||
|
console.error('错误信息:', err.errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=wecomp.js.map
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"wecomp.js","sourceRoot":"","sources":["../src/wecomp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,CAAC,KAAK,UAAU,YAAY;IAC9B,SAAS;IACT,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI;QAClC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,EACxC,CAAC;QACC,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACtE,CAAC;IAED,YAAY;IACZ,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;QAC5B,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;QAClC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAC1C,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,6BAA6B;KACxE,CAAC,CAAC;IAEH,IAAI,CAAC;QACD,sBAAsB;QACtB,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC1C,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,iBAAiB,EAAE,CAAC;QAExD,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,WAAW,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC;QAE9D,IAAI,CAAC,cAAc,CAAC,WAAW,IAAI,cAAc,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzE,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC9B,OAAO;QACX,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvB,cAAc,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE;YAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,WAAW,CAAC;QAEzB,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YAClC,OAAO;QACX,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC;YACnC,SAAS,EAAE,kCAAkC;SAChD,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YACzB,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,GAAG,GAAG,KAAyD,CAAC;YACtE,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC3B,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;YACvC,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;AACL,CAAC"}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
|
||||||
|
# 企业微信外部客户引流与识别系统 PRD
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
实现一个基于企业微信的外部客户引流与识别系统,根据用户与企业的关系状态(是否已添加为外部联系人),引导用户进入不同的业务流程。
|
||||||
|
|
||||||
|
## 核心需求点
|
||||||
|
|
||||||
|
1. 用户的入口(二维码) 二维码是哪里生成的? 需要带什么参数?
|
||||||
|
2. 用户扫码后, 判断当前用户是否已添加该企业微信的外部用户
|
||||||
|
3. 如果是该企业的外部客户, 跳转到该企业的具体应用页面, 如果不是该企业的外部客户则生成添加企业的二维码页面(这里的逻辑该如何处理)
|
||||||
|
|
||||||
|
## 技术方案
|
||||||
|
|
||||||
|
### 1. 二维码生成方案
|
||||||
|
|
||||||
|
#### 1.1 使用企业微信「联系我」接口
|
||||||
|
|
||||||
|
api doc: [客户联系「联系我」管理](https://developer.work.weixin.qq.com/document/path/92228)
|
||||||
|
|
||||||
|
**接口地址**: `POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_contact_way`
|
||||||
|
|
||||||
|
**推荐配置**(临时会话模式):
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: 1, // 单人联系
|
||||||
|
scene: 2, // 通过二维码联系
|
||||||
|
user: ["service_staff_id"], // 接待人员UserID
|
||||||
|
is_temp: true, // 开启临时会话
|
||||||
|
expires_in: 86400, // 二维码有效期(秒)
|
||||||
|
chat_expires_in: 86400, // 会话有效期(秒)
|
||||||
|
state: "channel_source_001", // 渠道标识参数
|
||||||
|
skip_verify: true, // 无需验证直接添加
|
||||||
|
conclusions: {
|
||||||
|
text: {
|
||||||
|
content: "感谢咨询,如需长期服务请添加我们"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 必须 | 说明 |
|
||||||
|
| --------------- | ---- | ------------------------------------------------------------------------------------------------------ |
|
||||||
|
| access_token | 是 | 调用接口凭证 |
|
||||||
|
| type | 是 | 联系方式类型,1-单人,2-多人 |
|
||||||
|
| scene | 是 | 场景,1-在小程序中联系,2-通过二维码联系 |
|
||||||
|
| style | 否 | 在小程序中联系时使用的控件样式,详见附表 |
|
||||||
|
| remark | 否 | 联系方式的备注信息,用于助记,不超过30个字符 |
|
||||||
|
| skip_verify | 否 | 外部客户添加时是否无需验证,默认为true |
|
||||||
|
| state | 否 | 企业自定义的state参数,用于区分不同的添加渠道,在调用"获取客户详情"时会返回该参数值,不超过30个字符 |
|
||||||
|
| user | 否 | 使用该联系方式的用户userID列表,在type为1时为必填,且只能有一个 |
|
||||||
|
| party | 否 | 使用该联系方式的部门id列表,只在type为2时有效 |
|
||||||
|
| is_temp | 否 | 是否临时会话模式,true表示使用临时会话模式,默认为false |
|
||||||
|
| expires_in | 否 | 临时会话二维码有效期,以秒为单位。该参数仅在is_temp为true时有效,默认7天,最多为14天 |
|
||||||
|
| chat_expires_in | 否 | 临时会话有效期,以秒为单位。该参数仅在is_temp为true时有效,默认为添加好友后24小时,最多为14天 |
|
||||||
|
| unionid | 否 | 可进行临时会话的客户unionid,该参数仅在is_temp为true时有效,如不指定则不进行限制 |
|
||||||
|
| is_exclusive | 否 | 是否开启同一外部企业客户只能添加同一个员工,默认为否,开启后,同一个企业的客户会优先添加到同一个跟进人 |
|
||||||
|
| mark_source | 否 | 是否标记客户添加来源为该应用创建的「联系我」,默认为true;仅对「营销获客」应用生效 |
|
||||||
|
| conclusions | 否 | 结束语,会话结束时自动发送给客户,可参考"结束语定义",仅在is_temp为true时有效 |
|
||||||
|
|
||||||
|
**关键参数说明**:
|
||||||
|
- `state`: 自定义状态参数,用于识别不同引流渠道,建议格式:`channel_{渠道ID}_{场景ID}`
|
||||||
|
- `is_temp`: 临时会话模式,不占用企业「联系我」配额,适合首次接触
|
||||||
|
- `user`: 指定接待人员,可以是固定客服或根据业务规则分配
|
||||||
|
|
||||||
|
#### 1.2 二维码参数设计
|
||||||
|
|
||||||
|
**必需参数**:
|
||||||
|
- `state`: 渠道标识(如:`channel_website_homepage`)
|
||||||
|
- `user`: 接待人员ID(可配置轮询规则)
|
||||||
|
|
||||||
|
**可选参数**:
|
||||||
|
- `expires_in`: 二维码有效期(建议7天内)
|
||||||
|
- `chat_expires_in`: 会话有效期(建议24小时)
|
||||||
|
- `conclusions`: 会话结束语(引导正式添加)
|
||||||
|
|
||||||
|
### 2. 客户识别与分流逻辑
|
||||||
|
|
||||||
|
#### 2.1 客户状态判断流程
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 用户扫码后的处理流程
|
||||||
|
async function handleCustomerScan(qrCodeState, externalUserId) {
|
||||||
|
// Step 1: 解析渠道信息
|
||||||
|
const channelInfo = parseChannelState(qrCodeState);
|
||||||
|
|
||||||
|
// Step 2: 获取客户详情
|
||||||
|
const customerDetail = await getExternalContactDetail(externalUserId);
|
||||||
|
|
||||||
|
// Step 3: 判断客户状态
|
||||||
|
if (customerDetail.external_contact) {
|
||||||
|
// 已是外部客户 -> 跳转应用
|
||||||
|
return {
|
||||||
|
action: "redirect_to_app",
|
||||||
|
url: `https://your-app.com/dashboard?channel=${channelInfo.source}&user=${externalUserId}`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 非外部客户 -> 生成添加二维码
|
||||||
|
const addContactWay = await generatePermanentContactWay(channelInfo);
|
||||||
|
return {
|
||||||
|
action: "show_add_qrcode",
|
||||||
|
qrCode: addContactWay.qr_code,
|
||||||
|
message: "请添加我们以获得完整服务",
|
||||||
|
expiresIn: addContactWay.expires_in
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 相关接口
|
||||||
|
|
||||||
|
**获取客户详情**:
|
||||||
|
- 接口: `POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get`
|
||||||
|
- 参数: `external_userid`(从扫码回调中获取)
|
||||||
|
- 返回: 客户基本信息、添加状态、标签等
|
||||||
|
|
||||||
|
**批量获取客户详情**:
|
||||||
|
- 接口: `POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user`
|
||||||
|
- 用于验证客户是否已添加
|
||||||
|
|
||||||
|
### 3. 分流处理方案
|
||||||
|
|
||||||
|
#### 3.1 已添加客户流程
|
||||||
|
|
||||||
|
1. **触发条件**: 客户已在企业外部联系人列表中
|
||||||
|
2. **处理动作**:
|
||||||
|
- 直接跳转到企业应用页面
|
||||||
|
- 传递客户标识和渠道参数
|
||||||
|
- 可根据客户标签进行个性化页面展示
|
||||||
|
3. **示例URL**: `https://app.yourcompany.com/home?external_userid=xxx&source=channel_001`
|
||||||
|
|
||||||
|
#### 3.2 未添加客户流程
|
||||||
|
|
||||||
|
1. **触发条件**: 客户不在企业外部联系人列表中
|
||||||
|
2. **处理动作**:
|
||||||
|
- 生成永久「联系我」二维码(非临时会话)
|
||||||
|
- 展示引导页面,说明添加后的服务内容
|
||||||
|
- 记录引流渠道,用于后续数据分析
|
||||||
|
3. **二维码配置**:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
scene: 2,
|
||||||
|
user: ["service_staff_id"],
|
||||||
|
is_temp: false, // 永久添加
|
||||||
|
state: "permanent_add_xxx", // 记录来源
|
||||||
|
skip_verify: true,
|
||||||
|
remark: "官网引流转化"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 接口调用流程
|
||||||
|
|
||||||
|
#### 4.1 初始化阶段
|
||||||
|
|
||||||
|
1. 调用 `add_contact_way` 接口生成引流二维码
|
||||||
|
2. 保存返回的 `config_id` 和 `qr_code` URL
|
||||||
|
3. 将二维码部署到各引流渠道
|
||||||
|
|
||||||
|
#### 4.2 用户扫码阶段
|
||||||
|
|
||||||
|
1. 用户扫码后,企业微信推送事件到回调URL
|
||||||
|
2. 回调事件包含 `external_userid` 和 `state` 参数
|
||||||
|
3. 后端接收事件,调用 `get_external_contact` 获取客户详情
|
||||||
|
4. 根据客户状态执行分流逻辑
|
||||||
|
|
||||||
|
#### 4.3 事件回调处理
|
||||||
|
|
||||||
|
**回调事件类型**:
|
||||||
|
- `change_external_contact`: 外部联系人变更事件
|
||||||
|
- `add_external_contact`: 添加外部联系人事件
|
||||||
|
- `del_external_contact`: 删除外部联系人事件
|
||||||
|
|
||||||
|
**关键参数**:
|
||||||
|
- `UserID`: 接待人员的企业微信ID
|
||||||
|
- `ExternalUserID`: 外部联系人ID
|
||||||
|
- `State`: 二维码中的state参数
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "pg-demo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "tsx ./src/main.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.27.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.19.5",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"tsx": "latest",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.19.8",
|
||||||
|
"@wecom/crypto": "^1.0.1",
|
||||||
|
"@wecom/jssdk": "^2.3.3",
|
||||||
|
"hono": "^4.11.3",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+415
@@ -0,0 +1,415 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@hono/node-server':
|
||||||
|
specifier: ^1.19.8
|
||||||
|
version: 1.19.8(hono@4.11.3)
|
||||||
|
'@wecom/crypto':
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1
|
||||||
|
'@wecom/jssdk':
|
||||||
|
specifier: ^2.3.3
|
||||||
|
version: 2.3.3
|
||||||
|
hono:
|
||||||
|
specifier: ^4.11.3
|
||||||
|
version: 4.11.3
|
||||||
|
xml2js:
|
||||||
|
specifier: ^0.6.2
|
||||||
|
version: 0.6.2
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.19.5
|
||||||
|
version: 22.19.5
|
||||||
|
'@types/xml2js':
|
||||||
|
specifier: ^0.4.14
|
||||||
|
version: 0.4.14
|
||||||
|
tsx:
|
||||||
|
specifier: latest
|
||||||
|
version: 4.21.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.9.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.2':
|
||||||
|
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.2':
|
||||||
|
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.2':
|
||||||
|
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.2':
|
||||||
|
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.2':
|
||||||
|
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.2':
|
||||||
|
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.2':
|
||||||
|
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@hono/node-server@1.19.8':
|
||||||
|
resolution: {integrity: sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==}
|
||||||
|
engines: {node: '>=18.14.1'}
|
||||||
|
peerDependencies:
|
||||||
|
hono: ^4
|
||||||
|
|
||||||
|
'@types/node@22.19.5':
|
||||||
|
resolution: {integrity: sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==}
|
||||||
|
|
||||||
|
'@types/xml2js@0.4.14':
|
||||||
|
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
||||||
|
|
||||||
|
'@wecom/crypto@1.0.1':
|
||||||
|
resolution: {integrity: sha512-K4Ilkl1l64ceJDbj/kflx8ND/J88pcl8tKx4Ivp7IiCrshRJU+Uo5uWCjAa+PjUiLIdcQSZ4m4d0t1npMPCX5A==}
|
||||||
|
|
||||||
|
'@wecom/jssdk@2.3.3':
|
||||||
|
resolution: {integrity: sha512-bazYSD39MoiaUV1xMVxrOPsTFa37ARY0xXAqDgth4kU/xk8n61o1YiAcdn4dPM+kBnJKOfukFYPjOPQv8K5l+w==}
|
||||||
|
|
||||||
|
esbuild@0.27.2:
|
||||||
|
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
get-tsconfig@4.13.0:
|
||||||
|
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||||
|
|
||||||
|
hono@4.11.3:
|
||||||
|
resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==}
|
||||||
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0:
|
||||||
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
|
sax@1.4.4:
|
||||||
|
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
||||||
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@6.21.0:
|
||||||
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1:
|
||||||
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@hono/node-server@1.19.8(hono@4.11.3)':
|
||||||
|
dependencies:
|
||||||
|
hono: 4.11.3
|
||||||
|
|
||||||
|
'@types/node@22.19.5':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/xml2js@0.4.14':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.5
|
||||||
|
|
||||||
|
'@wecom/crypto@1.0.1': {}
|
||||||
|
|
||||||
|
'@wecom/jssdk@2.3.3': {}
|
||||||
|
|
||||||
|
esbuild@0.27.2:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.27.2
|
||||||
|
'@esbuild/android-arm': 0.27.2
|
||||||
|
'@esbuild/android-arm64': 0.27.2
|
||||||
|
'@esbuild/android-x64': 0.27.2
|
||||||
|
'@esbuild/darwin-arm64': 0.27.2
|
||||||
|
'@esbuild/darwin-x64': 0.27.2
|
||||||
|
'@esbuild/freebsd-arm64': 0.27.2
|
||||||
|
'@esbuild/freebsd-x64': 0.27.2
|
||||||
|
'@esbuild/linux-arm': 0.27.2
|
||||||
|
'@esbuild/linux-arm64': 0.27.2
|
||||||
|
'@esbuild/linux-ia32': 0.27.2
|
||||||
|
'@esbuild/linux-loong64': 0.27.2
|
||||||
|
'@esbuild/linux-mips64el': 0.27.2
|
||||||
|
'@esbuild/linux-ppc64': 0.27.2
|
||||||
|
'@esbuild/linux-riscv64': 0.27.2
|
||||||
|
'@esbuild/linux-s390x': 0.27.2
|
||||||
|
'@esbuild/linux-x64': 0.27.2
|
||||||
|
'@esbuild/netbsd-arm64': 0.27.2
|
||||||
|
'@esbuild/netbsd-x64': 0.27.2
|
||||||
|
'@esbuild/openbsd-arm64': 0.27.2
|
||||||
|
'@esbuild/openbsd-x64': 0.27.2
|
||||||
|
'@esbuild/openharmony-arm64': 0.27.2
|
||||||
|
'@esbuild/sunos-x64': 0.27.2
|
||||||
|
'@esbuild/win32-arm64': 0.27.2
|
||||||
|
'@esbuild/win32-ia32': 0.27.2
|
||||||
|
'@esbuild/win32-x64': 0.27.2
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
get-tsconfig@4.13.0:
|
||||||
|
dependencies:
|
||||||
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
hono@4.11.3: {}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
|
sax@1.4.4: {}
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.2
|
||||||
|
get-tsconfig: 4.13.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.4.4
|
||||||
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1: {}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* API错误
|
||||||
|
*/
|
||||||
|
export class WeChatError extends Error {
|
||||||
|
constructor(
|
||||||
|
public errcode: number,
|
||||||
|
public errmsg: string
|
||||||
|
) {
|
||||||
|
super(`企业微信API错误 [${errcode}]: ${errmsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import { handleServer } from './server.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主函数:演示如何使用企业微信客户联系API
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
await handleServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
import { serve } from "@hono/node-server";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { getSignature, decrypt, encrypt } from '@wecom/crypto';
|
||||||
|
import { parseStringPromise } from 'xml2js';
|
||||||
|
|
||||||
|
|
||||||
|
function validateSignature(msgSignature: string, timestamp: string, nonce: string, encrypt: string) {
|
||||||
|
const {
|
||||||
|
WEIXIN_EVENT_TOKEN,
|
||||||
|
} = process.env;
|
||||||
|
if (!WEIXIN_EVENT_TOKEN) throw new Error('static param error');
|
||||||
|
const devMsgSignature = getSignature(WEIXIN_EVENT_TOKEN, timestamp, nonce, encrypt);
|
||||||
|
return devMsgSignature === msgSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleServer() {
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
app.get('/we-comp/event', ({ req, ...args }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { 'msg_signature': msgSignature, timestamp, nonce, echostr } = req.query();
|
||||||
|
|
||||||
|
const {
|
||||||
|
WEIXIN_EVENT_TOKEN,
|
||||||
|
WEIXIN_EVENT_ENCODING_AES_KEY,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!WEIXIN_EVENT_TOKEN
|
||||||
|
|| !WEIXIN_EVENT_ENCODING_AES_KEY
|
||||||
|
|| !msgSignature
|
||||||
|
|| !timestamp
|
||||||
|
|| !nonce
|
||||||
|
|| !echostr
|
||||||
|
) {
|
||||||
|
throw new Error('static param error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateSignature(msgSignature, timestamp, nonce, echostr)) {
|
||||||
|
throw new Error('validate error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, echostr);
|
||||||
|
|
||||||
|
console.log('URL验证成功,解密后的消息:', message);
|
||||||
|
console.log('接收者ID:', id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return args.text("");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/we-comp/event', async ({ req, ...args }) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
'msg_signature': msgSignature,
|
||||||
|
timestamp,
|
||||||
|
nonce
|
||||||
|
} = req.query();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await parseStringPromise(await req.text());
|
||||||
|
const encrypt = result.xml.Encrypt.at(0);
|
||||||
|
|
||||||
|
const {
|
||||||
|
WEIXIN_EVENT_TOKEN,
|
||||||
|
WEIXIN_EVENT_ENCODING_AES_KEY,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!WEIXIN_EVENT_TOKEN
|
||||||
|
|| !WEIXIN_EVENT_ENCODING_AES_KEY
|
||||||
|
|| !msgSignature
|
||||||
|
|| !timestamp
|
||||||
|
|| !nonce
|
||||||
|
) {
|
||||||
|
throw new Error('static param error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证签名
|
||||||
|
if (!validateSignature(msgSignature, timestamp, nonce, encrypt)) {
|
||||||
|
throw new Error('签名验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, encrypt);
|
||||||
|
console.log('解密后的消息:', message);
|
||||||
|
console.log('接收者ID:', id);
|
||||||
|
|
||||||
|
|
||||||
|
// 5. 解析明文XML
|
||||||
|
const decryptedXml = await parseStringPromise(message);
|
||||||
|
const msgData = decryptedXml.xml;
|
||||||
|
|
||||||
|
const msgType = msgData.MsgType?.[0];
|
||||||
|
const fromUserName = msgData.FromUserName?.[0];
|
||||||
|
const toUserName = msgData.ToUserName?.[0];
|
||||||
|
const createTime = msgData.CreateTime?.[0];
|
||||||
|
|
||||||
|
console.log('========== 消息详情 ==========');
|
||||||
|
console.log('消息类型:', msgType);
|
||||||
|
console.log('发送者:', fromUserName);
|
||||||
|
console.log('接收者:', toUserName);
|
||||||
|
console.log('创建时间:', createTime);
|
||||||
|
console.log('============================');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return args.text('');
|
||||||
|
})
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
fetch: app.fetch,
|
||||||
|
port: 3080,
|
||||||
|
});
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
server.close()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
/**
|
||||||
|
* 企业微信客户联系 API 类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部联系人详情
|
||||||
|
*/
|
||||||
|
export interface ExternalContact {
|
||||||
|
/** 外部联系人userid */
|
||||||
|
external_userid: string;
|
||||||
|
/** 名称(微信用户返回昵称,企业微信用户返回对外展示名称) */
|
||||||
|
name: string;
|
||||||
|
/** 头像URL(部分应用不可获取) */
|
||||||
|
avatar?: string;
|
||||||
|
/** 1-微信用户,2-企业微信用户 */
|
||||||
|
type: 1 | 2;
|
||||||
|
/** 0-未知,1-男性,2-女性 */
|
||||||
|
gender: 0 | 1 | 2;
|
||||||
|
/** 微信unionid(需绑定微信开发者ID) */
|
||||||
|
unionid?: string;
|
||||||
|
/** 职位(仅企业微信用户) */
|
||||||
|
position?: string;
|
||||||
|
/** 企业简称(仅企业微信用户) */
|
||||||
|
corp_name?: string;
|
||||||
|
/** 企业主体名称(仅企业微信用户) */
|
||||||
|
corp_full_name?: string;
|
||||||
|
/** 自定义展示信息(仅企业微信用户) */
|
||||||
|
external_profile?: ExternalProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部联系人自定义展示信息
|
||||||
|
*/
|
||||||
|
export interface ExternalProfile {
|
||||||
|
/** 自定义展示字段列表 */
|
||||||
|
external_attr: ExternalAttr[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义展示字段
|
||||||
|
*/
|
||||||
|
export interface ExternalAttr {
|
||||||
|
/** 0-文本,1-网页,2-小程序 */
|
||||||
|
type: number;
|
||||||
|
/** 属性名称 */
|
||||||
|
name: string;
|
||||||
|
/** 文本内容(type=0时) */
|
||||||
|
text?: { value: string };
|
||||||
|
/** 网页内容(type=1时) */
|
||||||
|
web?: { url: string; title: string };
|
||||||
|
/** 小程序内容(type=2时) */
|
||||||
|
miniprogram?: { appid: string; pagepath: string; title: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签信息
|
||||||
|
*/
|
||||||
|
export interface Tag {
|
||||||
|
/** 标签分组名称 */
|
||||||
|
group_name: string;
|
||||||
|
/** 标签名称 */
|
||||||
|
tag_name: string;
|
||||||
|
/** 企业标签ID(用户自定义标签不返回) */
|
||||||
|
tag_id?: string;
|
||||||
|
/** 1-企业设置,2-用户自定义,3-规则组标签 */
|
||||||
|
type: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频号信息
|
||||||
|
*/
|
||||||
|
export interface WechatChannels {
|
||||||
|
/** 视频号名称 */
|
||||||
|
nickname: string;
|
||||||
|
/** 0-未知,1-主页,2-直播间,3-留资服务 */
|
||||||
|
source: 0 | 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟进用户信息
|
||||||
|
*/
|
||||||
|
export interface FollowUser {
|
||||||
|
/** 成员userid */
|
||||||
|
userid: string;
|
||||||
|
/** 该成员对客户的备注 */
|
||||||
|
remark: string;
|
||||||
|
/** 该成员对客户的描述 */
|
||||||
|
description: string;
|
||||||
|
/** 添加时间(Unix时间戳) */
|
||||||
|
createtime: number;
|
||||||
|
/** 标签列表 */
|
||||||
|
tags: Tag[];
|
||||||
|
/** 备注的企业名称 */
|
||||||
|
remark_corp_name: string;
|
||||||
|
/** 备注的手机号(部分应用不可获取) */
|
||||||
|
remark_mobiles?: string[];
|
||||||
|
/** 添加来源 */
|
||||||
|
add_way: number;
|
||||||
|
/** 视频号信息(add_way=10时有此字段) */
|
||||||
|
wechat_channels?: WechatChannels;
|
||||||
|
/** 发起添加的userid */
|
||||||
|
oper_userid: string;
|
||||||
|
/** 自定义state参数(联系我/获客链接) */
|
||||||
|
state?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户详情API响应
|
||||||
|
*/
|
||||||
|
export interface GetExternalContactResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 外部联系人详情 */
|
||||||
|
external_contact: ExternalContact;
|
||||||
|
/** 添加了此外部联系人的企业成员列表 */
|
||||||
|
follow_user: FollowUser[];
|
||||||
|
/** 分页cursor(跟进人>500时返回) */
|
||||||
|
next_cursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取access_token API响应
|
||||||
|
*/
|
||||||
|
export interface AccessTokenResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 获取到的凭证 */
|
||||||
|
access_token?: string;
|
||||||
|
/** 凭证有效期(秒) */
|
||||||
|
expires_in?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置了客户联系功能的成员列表API响应
|
||||||
|
*/
|
||||||
|
export interface GetFollowUserListResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 配置了客户联系功能的成员userid列表 */
|
||||||
|
follow_user?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户列表API响应
|
||||||
|
*/
|
||||||
|
export interface GetExternalContactListResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 外部联系人的userid列表 */
|
||||||
|
external_userid?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情API响应
|
||||||
|
*/
|
||||||
|
export interface BatchGetExternalContactResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 外部联系人详情列表 */
|
||||||
|
external_contact_list?: ExternalContact[];
|
||||||
|
/** 下一次分页的起始位置 */
|
||||||
|
next_cursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束语文本内容
|
||||||
|
*/
|
||||||
|
export interface ConclusionText {
|
||||||
|
/** 文本内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束语
|
||||||
|
*/
|
||||||
|
export interface Conclusion {
|
||||||
|
/** 文本结束语 */
|
||||||
|
text?: ConclusionText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface AddContactWayRequest {
|
||||||
|
/** 联系方式类型,1-单人,2-多人 */
|
||||||
|
type: 1 | 2;
|
||||||
|
/** 场景,1-在小程序中联系,2-通过二维码联系 */
|
||||||
|
scene: 1 | 2;
|
||||||
|
/** 在小程序中联系时使用的控件样式 */
|
||||||
|
style?: number;
|
||||||
|
/** 联系方式的备注信息,用于助记,不超过30个字符 */
|
||||||
|
remark?: string;
|
||||||
|
/** 外部客户添加时是否无需验证,默认为true */
|
||||||
|
skip_verify?: boolean;
|
||||||
|
/** 企业自定义的state参数,用于区分不同的添加渠道 */
|
||||||
|
state?: string;
|
||||||
|
/** 使用该联系方式的用户userID列表,在type为1时为必填,且只能有一个 */
|
||||||
|
user?: string[];
|
||||||
|
/** 使用该联系方式的部门id列表,只在type为2时有效 */
|
||||||
|
party?: number[];
|
||||||
|
/** 是否临时会话模式 */
|
||||||
|
is_temp?: boolean;
|
||||||
|
/** 临时会话二维码有效期,以秒为单位 */
|
||||||
|
expires_in?: number;
|
||||||
|
/** 临时会话有效期,以秒为单位 */
|
||||||
|
chat_expires_in?: number;
|
||||||
|
/** 可进行临时会话的客户unionid */
|
||||||
|
unionid?: string;
|
||||||
|
/** 是否开启同一外部企业客户只能添加同一个员工 */
|
||||||
|
is_exclusive?: boolean;
|
||||||
|
/** 是否标记客户添加来源 */
|
||||||
|
mark_source?: boolean;
|
||||||
|
/** 结束语 */
|
||||||
|
conclusions?: Conclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface AddContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id?: string;
|
||||||
|
/** 联系方式二维码的URL,开发者需要自行将此二维码转换为图片 */
|
||||||
|
qr_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface GetContactWayRequest {
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface GetContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
/** 联系方式配置信息 */
|
||||||
|
contact_way?: ContactWayInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系方式配置信息
|
||||||
|
*/
|
||||||
|
export interface ContactWayInfo {
|
||||||
|
/** 联系方式配置id */
|
||||||
|
config_id: string;
|
||||||
|
/** 联系方式类型 */
|
||||||
|
type: 1 | 2;
|
||||||
|
/** 场景 */
|
||||||
|
scene: 1 | 2;
|
||||||
|
/** 在小程序中联系时使用的控件样式 */
|
||||||
|
style?: number;
|
||||||
|
/** 联系方式的备注信息 */
|
||||||
|
remark?: string;
|
||||||
|
/** 是否无需验证 */
|
||||||
|
skip_verify: boolean;
|
||||||
|
/** state参数 */
|
||||||
|
state?: string;
|
||||||
|
/** 使用该联系方式的用户userID列表 */
|
||||||
|
user?: string[];
|
||||||
|
/** 使用该联系方式的部门id列表 */
|
||||||
|
party?: number[];
|
||||||
|
/** 是否临时会话模式 */
|
||||||
|
is_temp: boolean;
|
||||||
|
/** 临时会话二维码有效期 */
|
||||||
|
expires_in?: number;
|
||||||
|
/** 临时会话有效期 */
|
||||||
|
chat_expires_in?: number;
|
||||||
|
/** 临时会话指定的客户unionid */
|
||||||
|
unionid?: string;
|
||||||
|
/** 是否开启同一外部企业客户只能添加同一个员工 */
|
||||||
|
is_exclusive: boolean;
|
||||||
|
/** 结束语 */
|
||||||
|
conclusions?: Conclusion;
|
||||||
|
/** 二维码的URL */
|
||||||
|
qr_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface UpdateContactWayRequest {
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id: string;
|
||||||
|
/** 在小程序中联系时使用的控件样式 */
|
||||||
|
style?: number;
|
||||||
|
/** 联系方式的备注信息 */
|
||||||
|
remark?: string;
|
||||||
|
/** 是否无需验证 */
|
||||||
|
skip_verify?: boolean;
|
||||||
|
/** 使用该联系方式的用户userID列表 */
|
||||||
|
user?: string[];
|
||||||
|
/** 使用该联系方式的部门id列表 */
|
||||||
|
party?: number[];
|
||||||
|
/** 是否临时会话模式 */
|
||||||
|
is_temp?: boolean;
|
||||||
|
/** 临时会话二维码有效期 */
|
||||||
|
expires_in?: number;
|
||||||
|
/** 临时会话有效期 */
|
||||||
|
chat_expires_in?: number;
|
||||||
|
/** 临时会话指定的客户unionid */
|
||||||
|
unionid?: string;
|
||||||
|
/** 是否开启同一外部企业客户只能添加同一个员工 */
|
||||||
|
is_exclusive?: boolean;
|
||||||
|
/** 结束语 */
|
||||||
|
conclusions?: Conclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface UpdateContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式请求参数
|
||||||
|
*/
|
||||||
|
export interface DeleteContactWayRequest {
|
||||||
|
/** 联系方式的配置id */
|
||||||
|
config_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式响应
|
||||||
|
*/
|
||||||
|
export interface DeleteContactWayResponse {
|
||||||
|
/** 返回码 */
|
||||||
|
errcode: number;
|
||||||
|
/** 返回信息 */
|
||||||
|
errmsg: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
/**
|
||||||
|
* 企业微信 API 客户端
|
||||||
|
*/
|
||||||
|
import { WeChatError } from './common/wechat-error.js';
|
||||||
|
import type {
|
||||||
|
AccessTokenResponse,
|
||||||
|
GetExternalContactResponse,
|
||||||
|
GetExternalContactListResponse,
|
||||||
|
GetFollowUserListResponse,
|
||||||
|
BatchGetExternalContactResponse,
|
||||||
|
ExternalContact,
|
||||||
|
AddContactWayRequest,
|
||||||
|
AddContactWayResponse,
|
||||||
|
GetContactWayRequest,
|
||||||
|
GetContactWayResponse,
|
||||||
|
UpdateContactWayRequest,
|
||||||
|
UpdateContactWayResponse,
|
||||||
|
DeleteContactWayRequest,
|
||||||
|
DeleteContactWayResponse,
|
||||||
|
} from './types/wechat.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信API客户端配置
|
||||||
|
*/
|
||||||
|
export interface WeChatClientConfig {
|
||||||
|
/** 企业ID */
|
||||||
|
corpId: string;
|
||||||
|
/** 应用的secret */
|
||||||
|
corpSecret: string;
|
||||||
|
/** API基础URL */
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信API客户端
|
||||||
|
*/
|
||||||
|
export class WeChatClient {
|
||||||
|
private config: Required<WeChatClientConfig>;
|
||||||
|
private accessToken: string | undefined;
|
||||||
|
private tokenExpireTime: number | undefined;
|
||||||
|
|
||||||
|
constructor(config: WeChatClientConfig) {
|
||||||
|
this.config = {
|
||||||
|
baseUrl: process.env.WEIXIN_CORP_URL ?? 'https://qyapi.weixin.qq.com',
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取access_token
|
||||||
|
* @returns access_token
|
||||||
|
*/
|
||||||
|
async getAccessToken(): Promise<string> {
|
||||||
|
// 检查token是否有效
|
||||||
|
if (this.accessToken && this.tokenExpireTime && Date.now() < this.tokenExpireTime) {
|
||||||
|
return this.accessToken as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/gettoken', this.config.baseUrl);
|
||||||
|
url.searchParams.set('corpid', this.config.corpId);
|
||||||
|
url.searchParams.set('corpsecret', this.config.corpSecret);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json() as AccessTokenResponse;
|
||||||
|
|
||||||
|
if (data.errcode !== 0 || !data.access_token) {
|
||||||
|
throw new Error(`获取access_token失败: ${data.errmsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accessToken = data.access_token;
|
||||||
|
// 提前5分钟过期,避免临界点问题
|
||||||
|
this.tokenExpireTime = Date.now() + (data.expires_in || 7200) * 1000 - 5 * 60 * 1000;
|
||||||
|
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户详情
|
||||||
|
* @param externalUserId 外部联系人的userid
|
||||||
|
* @param cursor 上次请求返回的next_cursor,用于分页
|
||||||
|
* @returns 客户详情
|
||||||
|
*/
|
||||||
|
async getExternalContact(
|
||||||
|
externalUserId: string,
|
||||||
|
cursor?: string
|
||||||
|
): Promise<GetExternalContactResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/get', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
url.searchParams.set('external_userid', externalUserId);
|
||||||
|
if (cursor) {
|
||||||
|
url.searchParams.set('cursor', cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json() as GetExternalContactResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户详情(支持分页,自动获取所有跟进人)
|
||||||
|
* @param externalUserId 外部联系人的userid
|
||||||
|
* @returns 客户详情(包含所有跟进人)
|
||||||
|
*/
|
||||||
|
async getExternalContactFull(
|
||||||
|
externalUserId: string
|
||||||
|
): Promise<GetExternalContactResponse> {
|
||||||
|
let result: GetExternalContactResponse | null = null;
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const data = await this.getExternalContact(externalUserId, cursor);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
// 第一次请求,直接使用返回的数据
|
||||||
|
result = data;
|
||||||
|
} else {
|
||||||
|
// 后续请求,合并follow_user列表
|
||||||
|
result.follow_user.push(...data.follow_user);
|
||||||
|
// 更新next_cursor
|
||||||
|
if (data.next_cursor !== undefined) {
|
||||||
|
result.next_cursor = data.next_cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = data.next_cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置了客户联系功能的成员列表
|
||||||
|
* @returns 配置了客户联系功能的成员userid列表
|
||||||
|
*/
|
||||||
|
async getFollowUserList(): Promise<GetFollowUserListResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/get_follow_user_list', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json() as GetFollowUserListResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户列表
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @returns 外部联系人的userid列表
|
||||||
|
*/
|
||||||
|
async getExternalContactList(
|
||||||
|
userId: string
|
||||||
|
): Promise<GetExternalContactListResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/list', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
url.searchParams.set('userid', userId);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json() as GetExternalContactListResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @param cursor 上次请求返回的next_cursor,用于分页
|
||||||
|
* @param limit 返回的最大记录数,整型,最大值1000,默认值为100
|
||||||
|
* @returns 客户详情列表
|
||||||
|
*/
|
||||||
|
async batchGetExternalContact(
|
||||||
|
userId: string,
|
||||||
|
cursor?: string,
|
||||||
|
limit: number = 100
|
||||||
|
): Promise<BatchGetExternalContactResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/batch/get_by_user', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
url.searchParams.set('userid', userId);
|
||||||
|
if (cursor) {
|
||||||
|
url.searchParams.set('cursor', cursor);
|
||||||
|
}
|
||||||
|
url.searchParams.set('limit', limit.toString());
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json() as BatchGetExternalContactResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取客户详情(自动分页,获取所有客户)
|
||||||
|
* @param userId 企业成员的userid
|
||||||
|
* @returns 所有客户详情列表
|
||||||
|
*/
|
||||||
|
async batchGetExternalContactFull(
|
||||||
|
userId: string
|
||||||
|
): Promise<ExternalContact[]> {
|
||||||
|
const allContacts: ExternalContact[] = [];
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const data = await this.batchGetExternalContact(userId, cursor);
|
||||||
|
if (data.external_contact_list) {
|
||||||
|
allContacts.push(...data.external_contact_list);
|
||||||
|
}
|
||||||
|
cursor = data.next_cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return allContacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加「联系我」方式
|
||||||
|
* @param params 联系方式配置参数
|
||||||
|
* @returns 联系方式配置信息(包含config_id和qr_code)
|
||||||
|
*/
|
||||||
|
async addContactWay(
|
||||||
|
params: AddContactWayRequest
|
||||||
|
): Promise<AddContactWayResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/add_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as AddContactWayResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取「联系我」方式
|
||||||
|
* @param params 查询参数(包含config_id)
|
||||||
|
* @returns 联系方式配置信息
|
||||||
|
*/
|
||||||
|
async getContactWay(
|
||||||
|
params: GetContactWayRequest
|
||||||
|
): Promise<GetContactWayResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/get_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as GetContactWayResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新「联系我」方式
|
||||||
|
* @param params 更新参数(包含config_id和要更新的字段)
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateContactWay(
|
||||||
|
params: UpdateContactWayRequest
|
||||||
|
): Promise<UpdateContactWayResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/update_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as UpdateContactWayResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除「联系我」方式
|
||||||
|
* @param params 删除参数(包含config_id)
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteContactWay(
|
||||||
|
params: DeleteContactWayRequest
|
||||||
|
): Promise<DeleteContactWayResponse> {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
const url = new URL('/cgi-bin/externalcontact/del_contact_way', this.config.baseUrl);
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as DeleteContactWayResponse;
|
||||||
|
|
||||||
|
// 检查API错误
|
||||||
|
if (data.errcode !== 0) {
|
||||||
|
const error = new WeChatError(data.errcode, data.errmsg);
|
||||||
|
error.errcode = data.errcode;
|
||||||
|
error.errmsg = data.errmsg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置access_token(强制重新获取)
|
||||||
|
*/
|
||||||
|
resetAccessToken(): void {
|
||||||
|
this.accessToken = undefined as string | undefined;
|
||||||
|
this.tokenExpireTime = undefined as number | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { WeChatClient } from "./wechat-client.js";
|
||||||
|
|
||||||
|
export async function handleWecomp() {
|
||||||
|
// 检查环境变量
|
||||||
|
if (process.env.WEIXIN_CORP_ID == null ||
|
||||||
|
process.env.WEIXIN_CORP_SECRET == null
|
||||||
|
) {
|
||||||
|
throw new Error('环境变量错误:请设置 WEIXIN_CORP_ID 和 WEIXIN_CORP_SECRET');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建企业微信客户端
|
||||||
|
const client = new WeChatClient({
|
||||||
|
corpId: process.env.WEIXIN_CORP_ID,
|
||||||
|
corpSecret: process.env.WEIXIN_CORP_SECRET,
|
||||||
|
baseUrl: process.env.WEIXIN_CORP_URL || 'https://qyapi.weixin.qq.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 示例:获取配置了客户联系功能的成员列表
|
||||||
|
console.log('\n=== 获取配置了客户联系功能的成员列表 ===');
|
||||||
|
const followUserList = await client.getFollowUserList();
|
||||||
|
|
||||||
|
console.log('\n--- 成员列表 ---');
|
||||||
|
console.log('成员数量:', followUserList.follow_user?.length || 0);
|
||||||
|
|
||||||
|
if (!followUserList.follow_user || followUserList.follow_user.length === 0) {
|
||||||
|
console.log('没有配置了客户联系功能的成员');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('成员ID列表:');
|
||||||
|
followUserList.follow_user.forEach((id, index) => {
|
||||||
|
console.log(` ${index + 1}. ${id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let userId = 'YeDeLiang';
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.log('错误:无法获取有效的成员userid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await client.getContactWay({
|
||||||
|
config_id: 'd5990127d833c10d9db8ba7f192e7b19'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(res);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error('发生错误:', error.message);
|
||||||
|
const err = error as unknown as { errcode?: number; errmsg?: string };
|
||||||
|
if (err.errcode !== undefined) {
|
||||||
|
console.error('错误码:', err.errcode);
|
||||||
|
}
|
||||||
|
if (err.errmsg !== undefined) {
|
||||||
|
console.error('错误信息:', err.errmsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
// Visit https://aka.ms/tsconfig to read more about this file
|
||||||
|
"compilerOptions": {
|
||||||
|
// File Layout
|
||||||
|
// "rootDir": "./src",
|
||||||
|
// "outDir": "./dist",
|
||||||
|
// Environment Settings
|
||||||
|
// See also https://aka.ms/tsconfig/module
|
||||||
|
"module": "nodenext",
|
||||||
|
"target": "esnext",
|
||||||
|
// "types": [],
|
||||||
|
// For nodejs:
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
// and npm install -D @types/node
|
||||||
|
// Other Outputs
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
// Stricter Typechecking Options
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
// Style Options
|
||||||
|
// "noImplicitReturns": true,
|
||||||
|
// "noImplicitOverride": true,
|
||||||
|
// "noUnusedLocals": true,
|
||||||
|
// "noUnusedParameters": true,
|
||||||
|
// "noFallthroughCasesInSwitch": true,
|
||||||
|
// "noPropertyAccessFromIndexSignature": true,
|
||||||
|
// Recommended Options
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"paths": {
|
||||||
|
"~/*": [
|
||||||
|
"./*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user