feishu-user-plugin 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/.claude-plugin/plugin.json +9 -0
- package/.env.example +18 -0
- package/.mcp.json.example +13 -0
- package/CHANGELOG.md +62 -0
- package/LICENSE +21 -0
- package/README.md +473 -0
- package/package.json +57 -0
- package/proto/lark.proto +317 -0
- package/skills/feishu-user-plugin/SKILL.md +103 -0
- package/skills/feishu-user-plugin/references/CLAUDE.md +94 -0
- package/skills/feishu-user-plugin/references/digest.md +26 -0
- package/skills/feishu-user-plugin/references/doc.md +27 -0
- package/skills/feishu-user-plugin/references/drive.md +24 -0
- package/skills/feishu-user-plugin/references/reply.md +23 -0
- package/skills/feishu-user-plugin/references/search.md +22 -0
- package/skills/feishu-user-plugin/references/send.md +28 -0
- package/skills/feishu-user-plugin/references/status.md +22 -0
- package/skills/feishu-user-plugin/references/table.md +32 -0
- package/skills/feishu-user-plugin/references/wiki.md +26 -0
- package/src/client.js +364 -0
- package/src/index.js +697 -0
- package/src/oauth-auto.js +196 -0
- package/src/oauth.js +215 -0
- package/src/official.js +365 -0
- package/src/test-all.js +324 -0
- package/src/test-comprehensive.js +301 -0
- package/src/test-send.js +67 -0
- package/src/utils.js +39 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
操作飞书多维表格(Bitable)。
|
|
2
|
+
|
|
3
|
+
## 参数
|
|
4
|
+
- $ARGUMENTS:操作类型 + 表格标识
|
|
5
|
+
|
|
6
|
+
## 执行步骤
|
|
7
|
+
|
|
8
|
+
### 查询数据
|
|
9
|
+
1. 用 `list_bitable_tables` 获取表格列表(传入 app_token)
|
|
10
|
+
2. 用 `list_bitable_fields` 获取字段结构(传入 app_token + table_id)
|
|
11
|
+
3. 用 `search_bitable_records` 查询记录(支持 filter 和 sort)
|
|
12
|
+
4. 格式化展示查询结果
|
|
13
|
+
|
|
14
|
+
### 写入数据
|
|
15
|
+
1. 先用 `list_bitable_fields` 确认字段结构
|
|
16
|
+
2. 用 `create_bitable_record` 创建新记录
|
|
17
|
+
```
|
|
18
|
+
create_bitable_record({ app_token, table_id, fields: {"状态":"进行中","标题":"新任务"} })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 更新数据
|
|
22
|
+
1. 先用 `search_bitable_records` 定位目标记录的 record_id
|
|
23
|
+
2. 用 `update_bitable_record` 更新指定字段
|
|
24
|
+
|
|
25
|
+
## 示例
|
|
26
|
+
- `/table query appXxx` — 列出所有表格
|
|
27
|
+
- `/table query appXxx tblXxx` — 查询表格记录
|
|
28
|
+
- `/table create appXxx tblXxx {"状态":"进行中"}` — 创建记录
|
|
29
|
+
|
|
30
|
+
## 注意
|
|
31
|
+
- 需要知道 app_token(从多维表格 URL 中获取)
|
|
32
|
+
- 字段名必须与表格中的字段名完全匹配
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
搜索和管理飞书知识库。
|
|
2
|
+
|
|
3
|
+
## 参数
|
|
4
|
+
- $ARGUMENTS:操作类型 + 关键词或节点标识
|
|
5
|
+
|
|
6
|
+
## 执行步骤
|
|
7
|
+
|
|
8
|
+
### 列出空间
|
|
9
|
+
1. 用 `list_wiki_spaces` 列出所有可访问的知识库空间
|
|
10
|
+
|
|
11
|
+
### 搜索内容
|
|
12
|
+
1. 用 `search_wiki` 搜索知识库内容
|
|
13
|
+
2. 找到节点后可用 `read_doc` 读取其文档内容
|
|
14
|
+
|
|
15
|
+
### 浏览节点
|
|
16
|
+
1. 用 `list_wiki_nodes` 列出指定空间的节点树
|
|
17
|
+
2. 可传 `parent_node_token` 浏览子节点
|
|
18
|
+
|
|
19
|
+
## 示例
|
|
20
|
+
- `/wiki list` — 列出所有知识库空间
|
|
21
|
+
- `/wiki search MCP 协议` — 搜索知识库
|
|
22
|
+
- `/wiki browse spaceXxx` — 浏览空间节点树
|
|
23
|
+
|
|
24
|
+
## 注意
|
|
25
|
+
- 使用 Official API,需要 LARK_APP_ID
|
|
26
|
+
- 搜索结果受机器人权限范围限制
|
package/src/client.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const protobuf = require('protobufjs');
|
|
3
|
+
const { generateRequestId, generateCid, parseCookie, formatCookie } = require('./utils');
|
|
4
|
+
|
|
5
|
+
const GATEWAY_URL = 'https://internal-api-lark-api.feishu.cn/im/gateway/';
|
|
6
|
+
const CSRF_URL = 'https://internal-api-lark-api.feishu.cn/accounts/csrf';
|
|
7
|
+
const USER_INFO_URL = 'https://internal-api-lark-api.feishu.cn/accounts/web/user';
|
|
8
|
+
|
|
9
|
+
// Message type enum (matches proto)
|
|
10
|
+
const MsgType = { POST: 2, FILE: 3, TEXT: 4, IMAGE: 5, AUDIO: 7, STICKER: 10, MEDIA: 15 };
|
|
11
|
+
|
|
12
|
+
class LarkUserClient {
|
|
13
|
+
constructor(cookieStr) {
|
|
14
|
+
// Validate cookie: HTTP headers require all characters ≤ 255 (ByteString).
|
|
15
|
+
// Users sometimes accidentally include Chinese text when copying cookies from browser.
|
|
16
|
+
const nonAsciiMatch = cookieStr.match(/[^\x00-\xff]/);
|
|
17
|
+
if (nonAsciiMatch) {
|
|
18
|
+
const idx = cookieStr.indexOf(nonAsciiMatch[0]);
|
|
19
|
+
const context = cookieStr.substring(Math.max(0, idx - 20), idx + 20);
|
|
20
|
+
throw new Error(
|
|
21
|
+
`LARK_COOKIE contains non-ASCII character "${nonAsciiMatch[0]}" (U+${nonAsciiMatch[0].charCodeAt(0).toString(16).toUpperCase()}) at index ${idx}.\n` +
|
|
22
|
+
`Context: ...${context}...\n` +
|
|
23
|
+
'This usually means extra text was accidentally copied with the cookie.\n' +
|
|
24
|
+
'Fix: In DevTools Network tab → first request → Request Headers → Cookie → copy ONLY the cookie value.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this.cookieObj = parseCookie(cookieStr);
|
|
28
|
+
this.cookieStr = cookieStr;
|
|
29
|
+
this.csrfToken = null;
|
|
30
|
+
this.userId = null;
|
|
31
|
+
this.userName = null;
|
|
32
|
+
this.proto = null;
|
|
33
|
+
this._heartbeatTimer = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async init() {
|
|
37
|
+
this.proto = await protobuf.load(path.join(__dirname, '..', 'proto', 'lark.proto'));
|
|
38
|
+
await this._getCsrfToken();
|
|
39
|
+
await this._getUserInfo();
|
|
40
|
+
if (!this.userId) {
|
|
41
|
+
throw new Error('Failed to authenticate. Cookie may be expired — re-login at feishu.cn and update LARK_COOKIE.');
|
|
42
|
+
}
|
|
43
|
+
console.error(`[feishu-user-plugin] Initialized as user: ${this.userName || this.userId}`);
|
|
44
|
+
this._startHeartbeat();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Auth ---
|
|
48
|
+
|
|
49
|
+
async _getCsrfToken() {
|
|
50
|
+
const res = await fetch(`${CSRF_URL}?_t=${Date.now()}`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
...this._jsonHeaders(),
|
|
54
|
+
'x-request-id': generateRequestId(),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const setCookie = res.headers.getSetCookie?.() || [];
|
|
58
|
+
for (const c of setCookie) {
|
|
59
|
+
const csrf = c.match(/swp_csrf_token=([^;]+)/);
|
|
60
|
+
if (csrf) { this.csrfToken = csrf[1]; this.cookieObj['swp_csrf_token'] = csrf[1]; }
|
|
61
|
+
const sl = c.match(/sl_session=([^;]+)/);
|
|
62
|
+
if (sl) { this.cookieObj['sl_session'] = sl[1]; }
|
|
63
|
+
}
|
|
64
|
+
this.cookieStr = formatCookie(this.cookieObj);
|
|
65
|
+
if (!this.csrfToken) {
|
|
66
|
+
console.error('[feishu-user-plugin] Warning: Could not obtain CSRF token');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _getUserInfo() {
|
|
71
|
+
const res = await fetch(`${USER_INFO_URL}?app_id=12&_t=${Date.now()}`, {
|
|
72
|
+
headers: {
|
|
73
|
+
...this._jsonHeaders(),
|
|
74
|
+
'x-csrf-token': this.csrfToken || '',
|
|
75
|
+
'x-request-id': generateRequestId(),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const body = await res.json().catch(() => null);
|
|
79
|
+
if (body?.data?.user?.id) {
|
|
80
|
+
this.userId = String(body.data.user.id);
|
|
81
|
+
this.userName = body.data.user.name || null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Cookie Heartbeat ---
|
|
86
|
+
|
|
87
|
+
_startHeartbeat() {
|
|
88
|
+
// Refresh CSRF token every 4 hours to keep session alive
|
|
89
|
+
// Feishu sl_session has 12h max-age; CSRF refresh also refreshes sl_session
|
|
90
|
+
this._heartbeatTimer = setInterval(async () => {
|
|
91
|
+
try {
|
|
92
|
+
await this._getCsrfToken();
|
|
93
|
+
console.error('[feishu-user-plugin] Cookie heartbeat: session refreshed');
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error('[feishu-user-plugin] Cookie heartbeat failed:', e.message);
|
|
96
|
+
}
|
|
97
|
+
}, 4 * 60 * 60 * 1000); // 4 hours
|
|
98
|
+
// Don't keep the process alive just for heartbeat
|
|
99
|
+
if (this._heartbeatTimer.unref) this._heartbeatTimer.unref();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async checkSession() {
|
|
103
|
+
try {
|
|
104
|
+
await this._getCsrfToken();
|
|
105
|
+
await this._getUserInfo();
|
|
106
|
+
return {
|
|
107
|
+
valid: !!this.userId,
|
|
108
|
+
userId: this.userId,
|
|
109
|
+
userName: this.userName,
|
|
110
|
+
message: this.userId ? 'Session active' : 'Session expired — re-login required',
|
|
111
|
+
};
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { valid: false, message: `Session check failed: ${e.message}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Headers ---
|
|
118
|
+
|
|
119
|
+
_baseHeaders() {
|
|
120
|
+
return {
|
|
121
|
+
'accept-language': 'zh-CN,zh;q=0.9',
|
|
122
|
+
'cookie': this.cookieStr,
|
|
123
|
+
'origin': 'https://www.feishu.cn',
|
|
124
|
+
'referer': 'https://www.feishu.cn/',
|
|
125
|
+
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_jsonHeaders() {
|
|
130
|
+
return {
|
|
131
|
+
...this._baseHeaders(),
|
|
132
|
+
'accept': 'application/json, text/plain, */*',
|
|
133
|
+
'x-app-id': '12',
|
|
134
|
+
'x-api-version': '2',
|
|
135
|
+
'x-device-info': 'platform=websdk',
|
|
136
|
+
'x-lgw-os-type': '1',
|
|
137
|
+
'x-lgw-terminal-type': '2',
|
|
138
|
+
'x-terminal-type': '2',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_protoHeaders(cmd, cmdVersion = '2.7.0') {
|
|
143
|
+
return {
|
|
144
|
+
...this._baseHeaders(),
|
|
145
|
+
'accept': '*/*',
|
|
146
|
+
'content-type': 'application/x-protobuf',
|
|
147
|
+
'locale': 'zh_CN',
|
|
148
|
+
'x-appid': '161471',
|
|
149
|
+
'x-command': String(cmd),
|
|
150
|
+
'x-command-version': cmdVersion,
|
|
151
|
+
'x-lgw-os-type': '1',
|
|
152
|
+
'x-lgw-terminal-type': '2',
|
|
153
|
+
'x-request-id': generateRequestId(),
|
|
154
|
+
'x-source': 'web',
|
|
155
|
+
'x-web-version': '3.9.32',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Protobuf Helpers ---
|
|
160
|
+
|
|
161
|
+
_encode(typeName, data) {
|
|
162
|
+
const Type = this.proto.lookupType(typeName);
|
|
163
|
+
return Type.encode(Type.create(data)).finish();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_decode(typeName, buffer) {
|
|
167
|
+
const Type = this.proto.lookupType(typeName);
|
|
168
|
+
return Type.decode(buffer);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async _gateway(cmd, reqType, reqData, cmdVersion) {
|
|
172
|
+
const reqBuf = this._encode(reqType, reqData);
|
|
173
|
+
const packetBuf = this._encode('Packet', {
|
|
174
|
+
payloadType: 1,
|
|
175
|
+
cmd,
|
|
176
|
+
cid: generateRequestId(),
|
|
177
|
+
payload: reqBuf,
|
|
178
|
+
});
|
|
179
|
+
const res = await fetch(GATEWAY_URL, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: this._protoHeaders(cmd, cmdVersion),
|
|
182
|
+
body: packetBuf,
|
|
183
|
+
});
|
|
184
|
+
const resBuf = Buffer.from(await res.arrayBuffer());
|
|
185
|
+
return { packet: this._decode('Packet', resBuf), ok: res.ok };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Generic Send (cmd=5) ---
|
|
189
|
+
|
|
190
|
+
async _sendMsg(type, chatId, content, { rootId, parentId } = {}) {
|
|
191
|
+
const req = { type, chatId, cid: generateCid(), isNotified: true, version: 1, content };
|
|
192
|
+
if (rootId) req.rootId = rootId;
|
|
193
|
+
if (parentId) req.parentId = parentId;
|
|
194
|
+
const { packet, ok } = await this._gateway(5, 'PutMessageRequest', req, '5.7.0');
|
|
195
|
+
return { success: ok && (packet.status === 0 || packet.status == null), status: packet.status };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Send Text Message ---
|
|
199
|
+
|
|
200
|
+
async sendMessage(chatId, text, opts = {}) {
|
|
201
|
+
const elemId = generateCid();
|
|
202
|
+
const textPropBuf = this._encode('TextProperty', { content: text });
|
|
203
|
+
return this._sendMsg(MsgType.TEXT, chatId, {
|
|
204
|
+
richText: {
|
|
205
|
+
elementIds: [elemId],
|
|
206
|
+
innerText: text,
|
|
207
|
+
elements: { dictionary: { [elemId]: { tag: 1, property: textPropBuf } } },
|
|
208
|
+
},
|
|
209
|
+
}, opts);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Send Image ---
|
|
213
|
+
|
|
214
|
+
async sendImage(chatId, imageKey, opts = {}) {
|
|
215
|
+
return this._sendMsg(MsgType.IMAGE, chatId, { imageKey }, opts);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Send File ---
|
|
219
|
+
|
|
220
|
+
async sendFile(chatId, fileKey, fileName, opts = {}) {
|
|
221
|
+
return this._sendMsg(MsgType.FILE, chatId, { fileKey, fileName }, opts);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- Send Audio ---
|
|
225
|
+
|
|
226
|
+
async sendAudio(chatId, audioKey, opts = {}) {
|
|
227
|
+
return this._sendMsg(MsgType.AUDIO, chatId, { audioKey }, opts);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Send Sticker ---
|
|
231
|
+
|
|
232
|
+
async sendSticker(chatId, stickerId, stickerSetId, opts = {}) {
|
|
233
|
+
return this._sendMsg(MsgType.STICKER, chatId, { stickerId, stickerSetId }, opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Send Rich Text / POST ---
|
|
237
|
+
|
|
238
|
+
async sendPost(chatId, title, paragraphs, opts = {}) {
|
|
239
|
+
const elementIds = [];
|
|
240
|
+
const dictionary = {};
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
243
|
+
const para = paragraphs[i];
|
|
244
|
+
for (const elem of para) {
|
|
245
|
+
const elemId = generateCid();
|
|
246
|
+
elementIds.push(elemId);
|
|
247
|
+
|
|
248
|
+
if (elem.tag === 'text') {
|
|
249
|
+
const propBuf = this._encode('TextProperty', { content: elem.text });
|
|
250
|
+
dictionary[elemId] = { tag: 1, property: propBuf };
|
|
251
|
+
} else if (elem.tag === 'at') {
|
|
252
|
+
const propBuf = this._encode('TextProperty', { content: elem.userId });
|
|
253
|
+
dictionary[elemId] = { tag: 5, property: propBuf };
|
|
254
|
+
} else if (elem.tag === 'a') {
|
|
255
|
+
const propBuf = this._encode('TextProperty', { content: elem.text || elem.href });
|
|
256
|
+
dictionary[elemId] = { tag: 6, property: propBuf };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Insert newline element between paragraphs
|
|
260
|
+
if (i < paragraphs.length - 1) {
|
|
261
|
+
const nlId = generateCid();
|
|
262
|
+
elementIds.push(nlId);
|
|
263
|
+
const propBuf = this._encode('TextProperty', { content: '\n' });
|
|
264
|
+
dictionary[nlId] = { tag: 1, property: propBuf };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const innerText = paragraphs.map(p => p.map(e => e.text || '').join('')).join('\n');
|
|
269
|
+
return this._sendMsg(MsgType.POST, chatId, {
|
|
270
|
+
title: title || '',
|
|
271
|
+
richText: { elementIds, innerText, elements: { dictionary } },
|
|
272
|
+
}, opts);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- Search (cmd=11021) ---
|
|
276
|
+
|
|
277
|
+
async search(query) {
|
|
278
|
+
const { packet } = await this._gateway(11021, 'UniversalSearchRequest', {
|
|
279
|
+
header: {
|
|
280
|
+
searchSession: generateCid(),
|
|
281
|
+
sessionSeqId: 1,
|
|
282
|
+
query,
|
|
283
|
+
locale: 'zh_CN',
|
|
284
|
+
searchContext: {
|
|
285
|
+
tagName: 'SMART_SEARCH',
|
|
286
|
+
entityItems: [
|
|
287
|
+
{ type: 1 },
|
|
288
|
+
{ type: 2 },
|
|
289
|
+
{ type: 3, filter: { groupChatFilter: {} } },
|
|
290
|
+
],
|
|
291
|
+
commonFilter: { includeOuterTenant: true },
|
|
292
|
+
sourceKey: 'messenger',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!packet.payload) return [];
|
|
298
|
+
const searchRes = this._decode('UniversalSearchResponse', packet.payload);
|
|
299
|
+
const items = (searchRes.results || []).map((r) => ({
|
|
300
|
+
id: r.id,
|
|
301
|
+
type: r.type === 1 ? 'user' : r.type === 3 ? 'group' : 'bot',
|
|
302
|
+
title: r.titleHighlighted?.replace(/<[^>]+>/g, '') || '',
|
|
303
|
+
summary: r.summaryHighlighted?.replace(/<[^>]+>/g, '') || '',
|
|
304
|
+
}));
|
|
305
|
+
// Cache names for getUserName lookups
|
|
306
|
+
for (const item of items) {
|
|
307
|
+
if (item.title) this._nameCache.set(String(item.id), item.title);
|
|
308
|
+
}
|
|
309
|
+
return items;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// --- Create P2P Chat (cmd=13) ---
|
|
313
|
+
|
|
314
|
+
async createChat(userId) {
|
|
315
|
+
const { packet } = await this._gateway(13, 'PutChatRequest', {
|
|
316
|
+
type: 1,
|
|
317
|
+
chatterIds: [userId],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!packet.payload) return null;
|
|
321
|
+
const chatRes = this._decode('PutChatResponse', packet.payload);
|
|
322
|
+
return chatRes.chat?.id || null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Get Group Info (cmd=64) ---
|
|
326
|
+
|
|
327
|
+
async getGroupInfo(chatId) {
|
|
328
|
+
const { packet } = await this._gateway(64, 'GetGroupInfoRequest', { chatId });
|
|
329
|
+
|
|
330
|
+
if (!packet.payload) return null;
|
|
331
|
+
const res = this._decode('GetGroupInfoResponse', packet.payload);
|
|
332
|
+
const chat = res.chat;
|
|
333
|
+
if (!chat) return null;
|
|
334
|
+
return {
|
|
335
|
+
id: chat.id,
|
|
336
|
+
name: chat.name || '',
|
|
337
|
+
description: chat.description || '',
|
|
338
|
+
type: chat.type === 1 ? 'p2p' : chat.type === 2 ? 'group' : chat.type === 3 ? 'topic_group' : 'unknown',
|
|
339
|
+
memberCount: chat.memberCount || chat.userCount || 0,
|
|
340
|
+
ownerId: chat.ownerId || '',
|
|
341
|
+
isPublic: !!chat.isPublic,
|
|
342
|
+
isDissolved: !!chat.isDissolved,
|
|
343
|
+
createTime: chat.createTime ? Number(chat.createTime) : null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// --- Get User Name ---
|
|
348
|
+
|
|
349
|
+
// Name cache populated by search() and init()
|
|
350
|
+
_nameCache = new Map();
|
|
351
|
+
|
|
352
|
+
async getUserName(userId) {
|
|
353
|
+
// Check cache first (populated by search, init, and previous lookups)
|
|
354
|
+
if (this._nameCache.has(String(userId))) return this._nameCache.get(String(userId));
|
|
355
|
+
// Self
|
|
356
|
+
if (String(userId) === String(this.userId) && this.userName) {
|
|
357
|
+
this._nameCache.set(String(userId), this.userName);
|
|
358
|
+
return this.userName;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = { LarkUserClient, MsgType };
|