lansenger-sdk-ts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.fr.md +501 -0
- package/README.md +504 -0
- package/README.zhHans.md +501 -0
- package/README.zhHant.md +501 -0
- package/README.zhHantHK.md +501 -0
- package/dist/accountMessages.d.ts +12 -0
- package/dist/accountMessages.js +41 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +70 -0
- package/dist/calendars.d.ts +84 -0
- package/dist/calendars.js +278 -0
- package/dist/callbacks.d.ts +384 -0
- package/dist/callbacks.js +712 -0
- package/dist/chats.d.ts +22 -0
- package/dist/chats.js +88 -0
- package/dist/client.d.ts +439 -0
- package/dist/client.js +712 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +42 -0
- package/dist/constants.d.ts +30 -0
- package/dist/constants.js +187 -0
- package/dist/contacts.d.ts +38 -0
- package/dist/contacts.js +161 -0
- package/dist/departments.d.ts +18 -0
- package/dist/departments.js +69 -0
- package/dist/exceptions.d.ts +20 -0
- package/dist/exceptions.js +42 -0
- package/dist/groupMessages.d.ts +11 -0
- package/dist/groupMessages.js +39 -0
- package/dist/groups.d.ts +66 -0
- package/dist/groups.js +218 -0
- package/dist/http.d.ts +7 -0
- package/dist/http.js +67 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +189 -0
- package/dist/media.d.ts +16 -0
- package/dist/media.js +178 -0
- package/dist/models.d.ts +925 -0
- package/dist/models.js +991 -0
- package/dist/oauth.d.ts +17 -0
- package/dist/oauth.js +107 -0
- package/dist/persistence.d.ts +26 -0
- package/dist/persistence.js +210 -0
- package/dist/reminders.d.ts +10 -0
- package/dist/reminders.js +31 -0
- package/dist/streaming.d.ts +9 -0
- package/dist/streaming.js +40 -0
- package/dist/todos.d.ts +75 -0
- package/dist/todos.js +282 -0
- package/dist/urlHelpers.d.ts +7 -0
- package/dist/urlHelpers.js +22 -0
- package/dist/userMessages.d.ts +8 -0
- package/dist/userMessages.js +34 -0
- package/dist/users.d.ts +6 -0
- package/dist/users.js +32 -0
- package/package.json +33 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
[English](README.md) | [简体中文](README.zhHans.md) | [繁体中文](README.zhHant.md) | [繁体中文香港](README.zhHantHK.md) | [Français](README.fr.md)
|
|
2
|
+
|
|
3
|
+
# lansenger-sdk-ts
|
|
4
|
+
|
|
5
|
+
藍信(Lansenger)平臺的 TypeScript SDK——支援藍信應用、組織機械人及個人機械人。
|
|
6
|
+
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
> 零框架依賴——僅依賴 `node-fetch` (v2,CommonJS 相容)。可適配任何 Node.js 專案。
|
|
12
|
+
|
|
13
|
+
## 支援的機械人類型
|
|
14
|
+
|
|
15
|
+
| 機械人類型 | 認證 | WebSocket 入站 | 全部 API |
|
|
16
|
+
|------------|------|-----------------|----------|
|
|
17
|
+
| **藍信應用** | appToken + userToken | ✗(使用 webhook) | ✓ |
|
|
18
|
+
| **組織機械人** | appToken + userToken | ✗(使用 webhook) | ✓ |
|
|
19
|
+
| **個人機械人** | appToken | ✓(WebSocket) | ✓(非機械人 API 有部分限制) |
|
|
20
|
+
|
|
21
|
+
三種機械人類型使用相同的認證機制:`appToken` 為所有 API 呼叫所必需;`userToken` 僅在特定使用者級操作時需要(使用者資訊、員工搜尋、日曆等)。
|
|
22
|
+
|
|
23
|
+
## 功能特色
|
|
24
|
+
|
|
25
|
+
- **非同步客戶端** — `LansengerClient` 提供 Promise 化 API
|
|
26
|
+
- **憑證與令牌持久化** — `CredentialStore` 將 app_id、app_secret、URL、appToken、userToken 保存至檔案(重啟不丟失)
|
|
27
|
+
- **OAuth2 使用者認證** — 建構授權 URL、換取 userToken、更新令牌
|
|
28
|
+
- **組織與部門** — 組織資訊、部門詳情/子部門/員工列表
|
|
29
|
+
- **員工與通訊錄** — 基本/詳細資訊、ID 映射、部門祖先鏈、搜尋
|
|
30
|
+
- **訊息傳遞** — 3 種私聊通道(機械人、公眾號、人→人)+ 群聊,支援所有訊息類型,含 @提及和真人/機械人發送身分,加急提醒
|
|
31
|
+
- **富卡片** — appCard(支援動態狀態更新)、oacard、linkCard、appArticles
|
|
32
|
+
- **串流訊息** — SSE 即時投遞,專為 AI Agent 設計
|
|
33
|
+
- **媒體上傳/下載** — 檔案、圖片、影片,自動偵測類型,媒體路徑取得
|
|
34
|
+
- **訊息管理** — 撤回、動態卡片更新
|
|
35
|
+
- **群組** — 建立、查詢資訊/成員/列表、成員檢查、更新設定與成員、解散
|
|
36
|
+
- **日曆日程** — 主日曆、日程 CRUD + 更新、參會人管理 + 參會人元資料
|
|
37
|
+
- **統一待辦** — 建立、更新、刪除、查詢、執行人管理、狀態統計
|
|
38
|
+
- **回呼事件** — 25 種事件類型、結構化訊息解析、AES 解密(4.10.1.4)、SHA1 簽名驗證
|
|
39
|
+
- **聊天讀取** — 取得聊天列表、取得聊天訊息(4.24 MCP)
|
|
40
|
+
|
|
41
|
+
## 快速安裝
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install lansenger-sdk-ts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
開發模式:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/lansenger-pm/lansenger-sdk-ts.git
|
|
51
|
+
cd lansenger-sdk-ts
|
|
52
|
+
npm install
|
|
53
|
+
npm run build
|
|
54
|
+
npm test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 1. 認證
|
|
58
|
+
|
|
59
|
+
### appToken — 所有 API 呼叫所必需
|
|
60
|
+
|
|
61
|
+
每個 SDK 方法都需要 `appToken`。客戶端使用 `app_id` + `app_secret` 自動取得並更新 appToken,你無需手動管理——`TokenManager` 處理整個生命週期:
|
|
62
|
+
|
|
63
|
+
1. **首次呼叫** → 使用 app_id + app_secret 請求 `GET /v1/apptoken/create` → 回傳 `appToken`(有效期 2 小時)
|
|
64
|
+
2. **後續呼叫** → 重用已緩存的 appToken 直到過期
|
|
65
|
+
3. **Token 過期** → 透過同一端點自動更新
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { LansengerClient } from "lansenger-sdk-ts";
|
|
69
|
+
|
|
70
|
+
const client = new LansengerClient("你的-appid", "你的-secret");
|
|
71
|
+
|
|
72
|
+
// 你也可以手動取得/撤銷 token
|
|
73
|
+
const token = await client.getToken();
|
|
74
|
+
client.invalidateToken(); // 強制下次呼叫時更新
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### userToken — 僅特定端點需要
|
|
78
|
+
|
|
79
|
+
`userToken` 代表特定藍信使用者的授權(透過 OAuth2 取得),僅在以下情況需要:
|
|
80
|
+
- 使用者級資訊(fetchUserInfo、fetchStaffDetail、searchStaff)
|
|
81
|
+
- 日曆日程操作(fetchPrimaryCalendar、createSchedule 等)
|
|
82
|
+
- 以真人發送身分進行群組操作
|
|
83
|
+
|
|
84
|
+
### 取得憑證
|
|
85
|
+
|
|
86
|
+
| 機械人類型 | 如何取得 app_id + app_secret |
|
|
87
|
+
|------------|------------------------------|
|
|
88
|
+
| **個人機械人** | 藍信桌面端 → 通訊錄 → 智慧機械人 → 個人機械人 → 點擊右側 ℹ️ 圖標(行動端不支援查看憑證) |
|
|
89
|
+
| **藍信應用** | 在藍信開發者中心建立,可能需要向組織管理員申請 |
|
|
90
|
+
| **組織機械人** | 在藍信開發者中心建立,可能需要向組織管理員申請 |
|
|
91
|
+
|
|
92
|
+
### OAuth2 使用者級認證
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// 建構授權 URL——將使用者重定向到藍信通行證頁面
|
|
96
|
+
const url = client.buildAuthorizeUrl("https://myapp.com/callback");
|
|
97
|
+
|
|
98
|
+
// 使用者授權後,用 code 换取 userToken + refreshToken
|
|
99
|
+
const tokenResult = await client.exchangeCode("回呼中的授權碼");
|
|
100
|
+
|
|
101
|
+
// 更新過期的 userToken
|
|
102
|
+
const newToken = await client.refreshUserToken(tokenResult.refresh_token!);
|
|
103
|
+
|
|
104
|
+
// 取得使用者資料
|
|
105
|
+
const userInfo = await client.fetchUserInfo(tokenResult.user_token!);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 工廠方法
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// 從環境變數(LANSENGER_APP_ID、LANSENGER_APP_SECRET 等)
|
|
112
|
+
const client = LansengerClient.fromEnv();
|
|
113
|
+
|
|
114
|
+
// 從 LansengerConfig 物件
|
|
115
|
+
const config = new LansengerConfig("appid", "secret", "https://open.e.lanxin.cn/open/apigw");
|
|
116
|
+
const client = LansengerClient.fromConfig(config);
|
|
117
|
+
|
|
118
|
+
// 從 CredentialStore(讀取持久化憑證)
|
|
119
|
+
const client = LansengerClient.fromStore();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 2. 組織與部門
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// 組織資訊
|
|
126
|
+
const org = await client.fetchOrgInfo("orgId");
|
|
127
|
+
|
|
128
|
+
// 部門層級
|
|
129
|
+
const detail = await client.fetchDepartmentDetail("deptId");
|
|
130
|
+
const children = await client.fetchDepartmentChildren("deptId");
|
|
131
|
+
const staffs = await client.fetchDepartmentStaffs("deptId");
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 3. 員工與通訊錄
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// 基本員工資訊
|
|
138
|
+
const staff = await client.fetchStaffBasicInfo("staffOpenId");
|
|
139
|
+
|
|
140
|
+
// 詳細資料(建議使用 userToken)
|
|
141
|
+
const detail = await client.fetchStaffDetail("staffOpenId", { user_token: "ut" });
|
|
142
|
+
|
|
143
|
+
// 手機號 → staffId 映射
|
|
144
|
+
const mapping = await client.fetchStaffIdMapping("orgId", "mobile", "13800138000");
|
|
145
|
+
|
|
146
|
+
// 員工的部門祖先鏈
|
|
147
|
+
const ancestors = await client.fetchDepartmentAncestors("staffOpenId");
|
|
148
|
+
|
|
149
|
+
// 搜尋員工(需要 userToken)
|
|
150
|
+
const results = await client.searchStaff("張三", { user_token: "ut" });
|
|
151
|
+
|
|
152
|
+
// 組織額外欄位 ID
|
|
153
|
+
const fields = await client.fetchOrgExtraFieldIds("orgId");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 4. 訊息與媒體
|
|
157
|
+
|
|
158
|
+
#### 機械人私聊——最常用
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const result = await client.sendText("staff123", "你好!");
|
|
162
|
+
const result = await client.sendMarkdown("staff123", "**加粗**");
|
|
163
|
+
const result = await client.sendFile("staff123", "/path/to/report.pdf");
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### 公眾號通道
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const result = await client.sendAccountMessage(
|
|
170
|
+
"text", { text: { content: "系統通知" } },
|
|
171
|
+
["staff1", "staff2"], undefined,
|
|
172
|
+
{ account_id: "524288-xxxx" },
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 人→人通道(需要 userToken)
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const result = await client.sendUserMessage(
|
|
180
|
+
"staff456", "text", { text: { content: "你好" } },
|
|
181
|
+
{ user_token: "ut" },
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### 群聊
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// 機械人 → 群組
|
|
189
|
+
const result = await client.sendText("group123", "通知", { is_group: true });
|
|
190
|
+
|
|
191
|
+
// 真人 → 群組(使用 userToken)
|
|
192
|
+
const result = await client.sendGroupMessage(
|
|
193
|
+
"group123", "text", { text: { content: "我來處理" } },
|
|
194
|
+
{ user_token: "ut" },
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// 群聊 @提及
|
|
198
|
+
const result = await client.sendText("group123", "重要!", { is_group: true, reminder_all: true });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### 富卡片
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const result = await client.sendAppCard("staff123", "審批", { is_dynamic: true });
|
|
205
|
+
const result = await client.sendLinkCard("staff123", "文章", "https://...");
|
|
206
|
+
const result = await client.sendAppArticles("staff123", [{ title: "文章1", link: "..." }]);
|
|
207
|
+
|
|
208
|
+
// 更新動態卡片狀態
|
|
209
|
+
const result = await client.updateDynamicCard("msg123", { is_last_update: true });
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### 串流訊息(專為 AI Agent 設計)
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const result = await client.createStreamMessage("staff1", "staff", "stream-id-1");
|
|
216
|
+
const result = await client.fetchStreamMessage("msg123");
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### 媒體
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// 上傳
|
|
223
|
+
const upload = await client.uploadMediaFile("/path/to/file.pdf");
|
|
224
|
+
|
|
225
|
+
// 下載
|
|
226
|
+
const download = await client.downloadMediaFile("media123");
|
|
227
|
+
|
|
228
|
+
// 保存至檔案
|
|
229
|
+
const filePath = await client.downloadMediaToFile("media123", { target_path: "/tmp/file.pdf" });
|
|
230
|
+
|
|
231
|
+
// 取得下載 URL 路徑 (4.5.3)
|
|
232
|
+
const pathResult = await client.fetchMediaPathInfo("media123");
|
|
233
|
+
|
|
234
|
+
// 撤回訊息
|
|
235
|
+
const result = await client.revokeMessage(["msg1", "msg2"]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### 加急提醒 (4.6.14)
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { REMINDER_TYPE_POPUP, REMINDER_TYPE_SMS } from "lansenger-sdk-ts";
|
|
242
|
+
|
|
243
|
+
const result = await client.sendReminderMsg(
|
|
244
|
+
"msg123",
|
|
245
|
+
[REMINDER_TYPE_POPUP, REMINDER_TYPE_SMS],
|
|
246
|
+
["staff1", "staff2"],
|
|
247
|
+
);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 5. 群組
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// 建立群組
|
|
254
|
+
const group = await client.createGroup("專案討論", "orgId", { staff_id_list: ["s1", "s2", "s3"] });
|
|
255
|
+
|
|
256
|
+
// 查詢資訊與成員
|
|
257
|
+
const info = await client.fetchGroupInfo("groupOpenId");
|
|
258
|
+
const members = await client.fetchGroupMembers("groupOpenId");
|
|
259
|
+
const groups = await client.fetchGroupList();
|
|
260
|
+
|
|
261
|
+
// 成員檢查
|
|
262
|
+
const result = await client.checkIsInGroup("groupOpenId", { staff_id: "staff1" });
|
|
263
|
+
|
|
264
|
+
// 更新設定
|
|
265
|
+
await client.updateGroupInfo("groupId", { name: "新名稱", manage_mode: 1 });
|
|
266
|
+
|
|
267
|
+
// 新增/移除成員
|
|
268
|
+
await client.updateGroupMembers("groupId", { add_user_list: ["staff4"], del_user_list: ["staff3"] });
|
|
269
|
+
|
|
270
|
+
// 解散群組(僅群主,4.28.6)
|
|
271
|
+
await client.dismissGroup("groupId");
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## 6. 日曆日程
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// 取得主日曆(需要 userToken 或 userId)
|
|
278
|
+
const cal = await client.fetchPrimaryCalendar({ user_token: "ut" });
|
|
279
|
+
|
|
280
|
+
// 建立日程
|
|
281
|
+
const schedule = await client.createSchedule(
|
|
282
|
+
cal.calendar_id!, "團隊會議",
|
|
283
|
+
{ date: "2024-01-15", time: "10:00", timeZone: "Asia/Shanghai" },
|
|
284
|
+
{ date: "2024-01-15", time: "11:00", timeZone: "Asia/Shanghai" },
|
|
285
|
+
[{ staffId: "staff1", attendeeFlag: "required" }],
|
|
286
|
+
{ user_token: "ut" },
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// 取得/刪除日程
|
|
290
|
+
const info = await client.fetchSchedule("cal1", "sch1", { user_token: "ut" });
|
|
291
|
+
await client.deleteSchedule("cal1", "sch1", { user_token: "ut" });
|
|
292
|
+
|
|
293
|
+
// 時間段內的日程列表(最多 42 天)
|
|
294
|
+
const schedules = await client.fetchScheduleList("cal1", 1705276800000, 1707940800000, { user_token: "ut" });
|
|
295
|
+
|
|
296
|
+
// 參會人管理
|
|
297
|
+
const attendees = await client.fetchScheduleAttendees("cal1", "sch1", { user_token: "ut" });
|
|
298
|
+
await client.addScheduleAttendees("cal1", "sch1", ["staff2"], { user_token: "ut" });
|
|
299
|
+
await client.deleteScheduleAttendees("cal1", "sch1", ["staff2"], { user_token: "ut" });
|
|
300
|
+
|
|
301
|
+
// 更新日程 (4.23.12)
|
|
302
|
+
await client.updateSchedule("cal1", "sch1", { summary: "更新的會議", user_token: "ut" });
|
|
303
|
+
|
|
304
|
+
// 更新參會人元資料 (4.23.17) — RSVP、顏色、忙/閒、提醒
|
|
305
|
+
await client.updateScheduleAttendeeMeta("cal1", "sch1", {
|
|
306
|
+
rsvp_status: "accept", busy_free_state: "busy", remind_times: [5, 15], user_token: "ut",
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## 7. 統一待辦
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { TODO_TYPE_APPROVAL, TODO_TODO_STATUS_DONE } from "lansenger-sdk-ts";
|
|
314
|
+
|
|
315
|
+
// 建立待辦任務
|
|
316
|
+
const todo = await client.createTodoTask(
|
|
317
|
+
"審批請求", "https://app.com/a/1", "https://pc.app.com/a/1",
|
|
318
|
+
["staff1"], "org1", TODO_TYPE_APPROVAL,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// 更新狀態(11=待閱, 12=已閱, 21=待辦, 22=已辦)
|
|
322
|
+
await client.updateTodoTaskStatus("taskId", TODO_TODO_STATUS_DONE, "org1");
|
|
323
|
+
|
|
324
|
+
// 更新內容
|
|
325
|
+
await client.updateTodoTask("taskId", "已更新", "l", "p", "org1");
|
|
326
|
+
|
|
327
|
+
// 刪除(僅發送者可操作)
|
|
328
|
+
await client.deleteTodoTask("taskId", "org1");
|
|
329
|
+
|
|
330
|
+
// 查詢
|
|
331
|
+
const listResult = await client.fetchTodoTaskList("org1");
|
|
332
|
+
const task = await client.fetchTodoTaskById("taskId", "org1");
|
|
333
|
+
const task = await client.fetchTodoTaskBySourceId("src1", "org1");
|
|
334
|
+
const counts = await client.fetchTodoTaskStatusCounts("staff1", "org1");
|
|
335
|
+
|
|
336
|
+
// 執行人管理
|
|
337
|
+
await client.addExecutors(["staff2"], "org1", { todotask_id: "taskId" });
|
|
338
|
+
await client.deleteExecutors(["staff2"], "org1", { todotask_id: "taskId" });
|
|
339
|
+
const executors = await client.fetchExecutorList("taskId", "org1");
|
|
340
|
+
await client.updateExecutorStatus(
|
|
341
|
+
[{ executorId: "staff1", todotaskId: "taskId", status: "22" }],
|
|
342
|
+
"org1",
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## 8. 聊天讀取 (4.24 MCP)
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// 取得聊天列表(私聊 + 羣聊)
|
|
350
|
+
const chatList = await client.fetchChatList({ user_token: "ut" });
|
|
351
|
+
|
|
352
|
+
// 取得聊天訊息
|
|
353
|
+
const messages = await client.fetchChatMessages({
|
|
354
|
+
staff_id: "staff1", // 或 group_id: "group1"
|
|
355
|
+
user_token: "ut",
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## 9. 回呼事件
|
|
360
|
+
|
|
361
|
+
SDK 同時支援純 JSON 和 AES 加密回呼載荷(藍信 API 規範 4.10.1.4)。
|
|
362
|
+
|
|
363
|
+
### 配置
|
|
364
|
+
|
|
365
|
+
設定 `encoding_key` 和 `callback_token`(來自藍信開發者中心回呼設定):
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
const client = new LansengerClient("appid", "secret", undefined, undefined, undefined, undefined, "BASE64_AES_KEY", "CALLBACK_TOKEN");
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
也可透過環境變數:`LANSENGER_ENCODING_KEY`、`LANSENGER_CALLBACK_TOKEN`。
|
|
372
|
+
|
|
373
|
+
### 解析回呼載荷(自動辨識加密/明文)
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { parseCallbackPayload } from "lansenger-sdk-ts";
|
|
377
|
+
|
|
378
|
+
// 純 JSON webhook
|
|
379
|
+
const events = parseCallbackPayload('{"events": [...]}');
|
|
380
|
+
|
|
381
|
+
// AES 加密載荷(使用 encodingKey 自動解密)
|
|
382
|
+
const events = parseCallbackPayload(encryptedData, {
|
|
383
|
+
encoding_key: "BASE64_AES_KEY",
|
|
384
|
+
known_app_id: "your-appid",
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 驗證簽名
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { verifyCallbackSignature } from "lansenger-sdk-ts";
|
|
392
|
+
|
|
393
|
+
// sha1(sort(token, timestamp, nonce, dataEncrypt))
|
|
394
|
+
const isValid = verifyCallbackSignature(
|
|
395
|
+
timestamp, nonce, signature, encodingKey,
|
|
396
|
+
{ data_encrypt: encryptedData, callback_token: "CALLBACK_TOKEN" },
|
|
397
|
+
);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### 事件類型
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const types = LansengerClient.getCallbackEventTypes(); // 25 種事件類型,13 個類別
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## 訊息類型能力矩陣
|
|
407
|
+
|
|
408
|
+
| msgType | Markdown | @提及 | 附件 | 私聊通道 | 羣聊 | 備註 |
|
|
409
|
+
|---------|----------|-------|------|----------|------|------|
|
|
410
|
+
| `text` | ✗ | ✓ (羣) | ✓ | 機械人、公眾號、人→人 | ✓ | 上限 6000 字節 |
|
|
411
|
+
| `formatText` | ✓ | ✗ | ✗ | 僅人→人 | ✓ | formatType=1 支援 Markdown |
|
|
412
|
+
| `oacard` | ✗ | ✗ | ✗ | 機械人、公眾號、人→人 | ✓ | 簡單卡片含欄位 |
|
|
413
|
+
| `appCard` | ✓ (div) | ✗ | ✗ | 機械人、公眾號、人→人 | ✓ | 富卡片,支援動態更新 |
|
|
414
|
+
| `linkCard` | ✗ | ✗ | ✗ | 機械人、公眾號 | ✓ | 連結預覽卡片 |
|
|
415
|
+
| `appArticles` | ✗ | ✗ | ✗ | 僅機械人私聊 | ✓ | 文章列表(1+ 篇) |
|
|
416
|
+
|
|
417
|
+
**羣聊**支援所有訊息類型。只有羣聊支援 @提及。
|
|
418
|
+
|
|
419
|
+
## 配置
|
|
420
|
+
|
|
421
|
+
### 環境變數
|
|
422
|
+
|
|
423
|
+
| 變數 | 必填 | 說明 | 預設值 |
|
|
424
|
+
|------|------|------|--------|
|
|
425
|
+
| `LANSENGER_APP_ID` | ✓ | 應用/機械人 ID | — |
|
|
426
|
+
| `LANSENGER_APP_SECRET` | ✓ | 應用/機械人 Secret | — |
|
|
427
|
+
| `LANSENGER_API_GATEWAY_URL` | ✗ | API 网關 URL | `https://open.e.lanxin.cn/open/apigw` |
|
|
428
|
+
| `LANSENGER_PASSPORT_URL` | ✗ | 通行證 URL(OAuth2) | — |
|
|
429
|
+
| `LANSENGER_ENCODING_KEY` | ✗ | 回呼 AES 加密密鑰(Base64) | — |
|
|
430
|
+
| `LANSENGER_CALLBACK_TOKEN` | ✗ | 回呼簽名令牌 | — |
|
|
431
|
+
|
|
432
|
+
### 憑證與令牌持久化
|
|
433
|
+
|
|
434
|
+
預設情況下,憑證和令牌僅保留在記憶體中(程式退出即消失)。啟用檔案持久化使用 `storePath`:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { LansengerClient, CredentialStore } from "lansenger-sdk-ts";
|
|
438
|
+
|
|
439
|
+
// 自動持久化至 ~/.lansenger/sdk_state.json
|
|
440
|
+
const client = new LansengerClient("appid", "secret", undefined, undefined, undefined, "~/.lansenger/sdk_state.json", "BASE64_AES_KEY", "CALLBACK_TOKEN");
|
|
441
|
+
|
|
442
|
+
// 或從環境變數加持久化
|
|
443
|
+
const client = LansengerClient.fromEnv("~/.lansenger/sdk_state.json");
|
|
444
|
+
|
|
445
|
+
// 手動操作儲存
|
|
446
|
+
const store = new CredentialStore("~/.lansenger/sdk_state.json");
|
|
447
|
+
store.saveCredentials("app_id", "app_secret", "https://open.e.lanxin.cn/open/apigw");
|
|
448
|
+
store.saveUserToken("user_token", "refresh_token");
|
|
449
|
+
const token = store.loadAppToken(); // 過期則回傳 null
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## 專案結構
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
lansenger-sdk-ts/
|
|
456
|
+
├── src/
|
|
457
|
+
│ ├── index.ts # 全部导出
|
|
458
|
+
│ ├── client.ts # LansengerClient(非同步)
|
|
459
|
+
│ ├── config.ts # LansengerConfig
|
|
460
|
+
│ ├── auth.ts # TokenManager — appToken 生命週期管理
|
|
461
|
+
│ ├── oauth.ts # OAuth2 輔助
|
|
462
|
+
│ ├── constants.ts # API 端點、媒體類型、OAuth 範圍
|
|
463
|
+
│ ├── exceptions.ts # LansengerError 異常層級
|
|
464
|
+
│ ├── models.ts # 38+ 結果類型
|
|
465
|
+
│ ├── http.ts # doGet、doPost、doPostMultipart、parseApiResponse
|
|
466
|
+
│ ├── urlHelpers.ts # buildApiUrl (支援 pathVars)
|
|
467
|
+
│ ├── contacts.ts # 員工與組織資訊 API
|
|
468
|
+
│ ├── departments.ts # 部門 API
|
|
469
|
+
│ ├── accountMessages.ts # 公眾號通道
|
|
470
|
+
│ ├── userMessages.ts # 人→人通道
|
|
471
|
+
│ ├── groupMessages.ts # 羣聊通道
|
|
472
|
+
│ ├── media.ts # 上傳/下載
|
|
473
|
+
│ ├── streaming.ts # SSE 串流
|
|
474
|
+
│ ├── persistence.ts # CredentialStore — 檔案持久化
|
|
475
|
+
│ ├── callbacks.ts # 回呼事件 — 25 種事件、AES 解密、SHA1 簽名驗證
|
|
476
|
+
│ ├── groups.ts # 羣組 API(含解散 4.28.6)
|
|
477
|
+
│ ├── todos.ts # 統一待辦
|
|
478
|
+
│ ├── calendars.ts # 日曆日程
|
|
479
|
+
│ ├── reminders.ts # 加急提醒 (4.6.14)
|
|
480
|
+
│ ├── chats.ts # 聊天讀取 (4.24 MCP)
|
|
481
|
+
│ └── users.ts # 使用者資訊
|
|
482
|
+
├── tests/ # 單元測試
|
|
483
|
+
├── package.json
|
|
484
|
+
├── tsconfig.json
|
|
485
|
+
├── jest.config.js
|
|
486
|
+
├── LICENSE
|
|
487
|
+
└── README*.md # 5 種語言 README
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## 開發
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
npm install
|
|
494
|
+
npm run build # 編譯 TypeScript → dist/
|
|
495
|
+
npm test # 執行 Jest 測試
|
|
496
|
+
npm run lint # 僅類型檢查 (tsc --noEmit)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## 授權
|
|
500
|
+
|
|
501
|
+
MIT — 見 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LansengerConfig } from "./config";
|
|
2
|
+
import { FetchFn } from "./http";
|
|
3
|
+
import { AccountMessageResult } from "./models";
|
|
4
|
+
export declare function sendAccountMessage(config: LansengerConfig, appToken: string, msgType: string, msgData: Record<string, any>, opts?: {
|
|
5
|
+
chat_ids?: string[];
|
|
6
|
+
department_ids?: string[];
|
|
7
|
+
account_id?: string;
|
|
8
|
+
entry_id?: string;
|
|
9
|
+
attach?: string;
|
|
10
|
+
user_token?: string;
|
|
11
|
+
fetchFn?: FetchFn;
|
|
12
|
+
}): Promise<AccountMessageResult>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendAccountMessage = sendAccountMessage;
|
|
4
|
+
const urlHelpers_1 = require("./urlHelpers");
|
|
5
|
+
const http_1 = require("./http");
|
|
6
|
+
const models_1 = require("./models");
|
|
7
|
+
async function sendAccountMessage(config, appToken, msgType, msgData, opts) {
|
|
8
|
+
if (!msgType)
|
|
9
|
+
return new models_1.AccountMessageResult({ success: false, error: "msg_type is required" });
|
|
10
|
+
if (!opts?.chat_ids && !opts?.department_ids)
|
|
11
|
+
return new models_1.AccountMessageResult({ success: false, error: "at least one of chat_ids or department_ids is required" });
|
|
12
|
+
if (!msgData)
|
|
13
|
+
return new models_1.AccountMessageResult({ success: false, error: "msg_data is required" });
|
|
14
|
+
const userToken = opts?.user_token || "";
|
|
15
|
+
const url = (0, urlHelpers_1.buildApiUrl)(config, "account_message", "create", appToken, { userToken });
|
|
16
|
+
const payload = {
|
|
17
|
+
userIdList: opts?.chat_ids || [], departmentIdList: opts?.department_ids || [],
|
|
18
|
+
msgType, msgData,
|
|
19
|
+
};
|
|
20
|
+
if (opts?.account_id)
|
|
21
|
+
payload.accountId = opts.account_id;
|
|
22
|
+
if (opts?.entry_id)
|
|
23
|
+
payload.entryId = opts.entry_id;
|
|
24
|
+
if (opts?.attach)
|
|
25
|
+
payload.attach = opts.attach;
|
|
26
|
+
const [data, httpErr] = await (0, http_1.doPost)(url, payload, opts?.fetchFn);
|
|
27
|
+
if (httpErr)
|
|
28
|
+
return new models_1.AccountMessageResult({ success: false, error: httpErr });
|
|
29
|
+
const errCode = data.errCode ?? -1;
|
|
30
|
+
if (errCode !== 0) {
|
|
31
|
+
const msg = data.errMsg || "Unknown error";
|
|
32
|
+
return new models_1.AccountMessageResult({ success: false, error: `API error (errCode=${errCode}): ${msg}` });
|
|
33
|
+
}
|
|
34
|
+
const d = data.data || {};
|
|
35
|
+
return new models_1.AccountMessageResult({
|
|
36
|
+
success: true, message_id: d.msgId,
|
|
37
|
+
invalid_staff: d.invalidStaff, invalid_department: d.invalidDepartment,
|
|
38
|
+
raw_response: data,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=accountMessages.js.map
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LansengerConfig } from "./config";
|
|
2
|
+
import { FetchFn } from "./http";
|
|
3
|
+
import { CredentialStore } from "./persistence";
|
|
4
|
+
export declare class TokenManager {
|
|
5
|
+
private config;
|
|
6
|
+
private fetchFn;
|
|
7
|
+
private token;
|
|
8
|
+
private tokenExpiry;
|
|
9
|
+
private store;
|
|
10
|
+
constructor(config: LansengerConfig, fetchFn: FetchFn, store?: CredentialStore | null);
|
|
11
|
+
getToken(): Promise<string>;
|
|
12
|
+
invalidate(): void;
|
|
13
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenManager = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
const exceptions_1 = require("./exceptions");
|
|
6
|
+
const TOKEN_REFRESH_MARGIN = 300;
|
|
7
|
+
class TokenManager {
|
|
8
|
+
constructor(config, fetchFn, store) {
|
|
9
|
+
this.token = null;
|
|
10
|
+
this.tokenExpiry = 0;
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.fetchFn = fetchFn;
|
|
13
|
+
this.store = store || null;
|
|
14
|
+
if (this.store) {
|
|
15
|
+
const cached = this.store.loadAppToken();
|
|
16
|
+
if (cached) {
|
|
17
|
+
this.token = cached;
|
|
18
|
+
const state = this.store.load();
|
|
19
|
+
this.tokenExpiry = state.app_token_expiry || 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async getToken() {
|
|
24
|
+
const now = Math.floor(Date.now() / 1000);
|
|
25
|
+
if (this.token && now < this.tokenExpiry) {
|
|
26
|
+
return this.token;
|
|
27
|
+
}
|
|
28
|
+
const url = `${this.config.api_gateway_url}${constants_1.API_ENDPOINTS.app_token.create}`;
|
|
29
|
+
const params = new URLSearchParams({
|
|
30
|
+
grant_type: "client_credential",
|
|
31
|
+
appid: this.config.app_id,
|
|
32
|
+
secret: this.config.app_secret,
|
|
33
|
+
});
|
|
34
|
+
const fullUrl = `${url}?${params.toString()}`;
|
|
35
|
+
try {
|
|
36
|
+
const response = await this.fetchFn(fullUrl);
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new exceptions_1.LansengerNetworkError(`Token request failed: HTTP ${response.status}`);
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
const errCode = data.errCode ?? -1;
|
|
42
|
+
if (errCode !== 0) {
|
|
43
|
+
const msg = data.errMsg || "Unknown token error";
|
|
44
|
+
throw new exceptions_1.LansengerAuthError(`Token error (errCode=${errCode}): ${msg}`, errCode);
|
|
45
|
+
}
|
|
46
|
+
const tokenData = data.data || {};
|
|
47
|
+
this.token = tokenData.appToken;
|
|
48
|
+
const expiresIn = tokenData.expiresIn || 7200;
|
|
49
|
+
this.tokenExpiry = Math.floor(Date.now() / 1000) + expiresIn - TOKEN_REFRESH_MARGIN;
|
|
50
|
+
if (!this.token) {
|
|
51
|
+
throw new exceptions_1.LansengerAuthError("Token response missing appToken field");
|
|
52
|
+
}
|
|
53
|
+
if (this.store) {
|
|
54
|
+
this.store.saveAppToken(this.token, expiresIn, TOKEN_REFRESH_MARGIN);
|
|
55
|
+
}
|
|
56
|
+
return this.token;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e instanceof exceptions_1.LansengerAuthError || e instanceof exceptions_1.LansengerNetworkError)
|
|
60
|
+
throw e;
|
|
61
|
+
throw new exceptions_1.LansengerNetworkError(`Token request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
invalidate() {
|
|
65
|
+
this.token = null;
|
|
66
|
+
this.tokenExpiry = 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.TokenManager = TokenManager;
|
|
70
|
+
//# sourceMappingURL=auth.js.map
|