chatboty 0.1.0 → 0.2.1
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/README.md +31 -11
- package/dist/cjs/src/chatboty.d.ts +19 -6
- package/dist/cjs/src/chatboty.d.ts.map +1 -1
- package/dist/cjs/src/chatboty.js +43 -5
- package/dist/cjs/src/chatboty.js.map +1 -1
- package/dist/cjs/src/entity/contact.d.ts +10 -2
- package/dist/cjs/src/entity/contact.d.ts.map +1 -1
- package/dist/cjs/src/entity/contact.js +11 -4
- package/dist/cjs/src/entity/contact.js.map +1 -1
- package/dist/cjs/src/entity/friendship.d.ts +6 -1
- package/dist/cjs/src/entity/friendship.d.ts.map +1 -1
- package/dist/cjs/src/entity/friendship.js +6 -1
- package/dist/cjs/src/entity/friendship.js.map +1 -1
- package/dist/cjs/src/entity/message.d.ts +21 -16
- package/dist/cjs/src/entity/message.d.ts.map +1 -1
- package/dist/cjs/src/entity/message.js +23 -20
- package/dist/cjs/src/entity/message.js.map +1 -1
- package/dist/cjs/src/entity/received-file.d.ts +25 -0
- package/dist/cjs/src/entity/received-file.d.ts.map +1 -0
- package/dist/cjs/src/entity/received-file.js +35 -0
- package/dist/cjs/src/entity/received-file.js.map +1 -0
- package/dist/cjs/src/entity/room.d.ts +18 -8
- package/dist/cjs/src/entity/room.d.ts.map +1 -1
- package/dist/cjs/src/entity/room.js +20 -15
- package/dist/cjs/src/entity/room.js.map +1 -1
- package/dist/cjs/src/mod.d.ts +1 -0
- package/dist/cjs/src/mod.d.ts.map +1 -1
- package/dist/cjs/src/mod.js +3 -1
- package/dist/cjs/src/mod.js.map +1 -1
- package/dist/cjs/src/types.d.ts +6 -1
- package/dist/cjs/src/types.d.ts.map +1 -1
- package/dist/cjs/src/types.js.map +1 -1
- package/dist/esm/src/chatboty.d.ts +19 -6
- package/dist/esm/src/chatboty.d.ts.map +1 -1
- package/dist/esm/src/chatboty.js +43 -5
- package/dist/esm/src/chatboty.js.map +1 -1
- package/dist/esm/src/entity/contact.d.ts +10 -2
- package/dist/esm/src/entity/contact.d.ts.map +1 -1
- package/dist/esm/src/entity/contact.js +11 -4
- package/dist/esm/src/entity/contact.js.map +1 -1
- package/dist/esm/src/entity/friendship.d.ts +6 -1
- package/dist/esm/src/entity/friendship.d.ts.map +1 -1
- package/dist/esm/src/entity/friendship.js +6 -1
- package/dist/esm/src/entity/friendship.js.map +1 -1
- package/dist/esm/src/entity/message.d.ts +21 -16
- package/dist/esm/src/entity/message.d.ts.map +1 -1
- package/dist/esm/src/entity/message.js +23 -20
- package/dist/esm/src/entity/message.js.map +1 -1
- package/dist/esm/src/entity/received-file.d.ts +25 -0
- package/dist/esm/src/entity/received-file.d.ts.map +1 -0
- package/dist/esm/src/entity/received-file.js +31 -0
- package/dist/esm/src/entity/received-file.js.map +1 -0
- package/dist/esm/src/entity/room.d.ts +18 -8
- package/dist/esm/src/entity/room.d.ts.map +1 -1
- package/dist/esm/src/entity/room.js +20 -15
- package/dist/esm/src/entity/room.js.map +1 -1
- package/dist/esm/src/mod.d.ts +1 -0
- package/dist/esm/src/mod.d.ts.map +1 -1
- package/dist/esm/src/mod.js +1 -0
- package/dist/esm/src/mod.js.map +1 -1
- package/dist/esm/src/types.d.ts +6 -1
- package/dist/esm/src/types.d.ts.map +1 -1
- package/dist/esm/src/types.js.map +1 -1
- package/package.json +2 -1
- package/src/chatboty.ts +50 -9
- package/src/entity/contact.ts +16 -5
- package/src/entity/friendship.ts +6 -1
- package/src/entity/message.ts +32 -27
- package/src/entity/received-file.ts +44 -0
- package/src/entity/room.ts +26 -16
- package/src/mod.ts +1 -0
- package/src/types.ts +6 -1
package/src/chatboty.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ChatbotyClient, type ChatbotyClientOptions } from './client.js'
|
|
7
7
|
import {
|
|
8
|
-
type BotApi, type Sayable, type MessagePayload, type ContactPayload,
|
|
8
|
+
type BotApi, type Sayable, type MessagePayload, type ContactPayload,
|
|
9
|
+
type ContactQueryFilter, type RoomQueryFilter,
|
|
9
10
|
toMessageType, isRoomId,
|
|
10
11
|
} from './types.js'
|
|
11
12
|
import { Message } from './entity/message.js'
|
|
@@ -32,6 +33,9 @@ export class Chatboty implements BotApi {
|
|
|
32
33
|
/** 当前登录者(self)。start 后异步热身; 未就绪时 undefined(用 {@link currentUser} 主动拉取)。 */
|
|
33
34
|
get self (): Contact | undefined { return this._self }
|
|
34
35
|
|
|
36
|
+
/** 是否已登录(在线)。等价于 self 已就绪。 */
|
|
37
|
+
get isLoggedIn (): boolean { return this._self !== undefined }
|
|
38
|
+
|
|
35
39
|
async start (): Promise<void> {
|
|
36
40
|
this.wire()
|
|
37
41
|
await this.client.start()
|
|
@@ -47,12 +51,13 @@ export class Chatboty implements BotApi {
|
|
|
47
51
|
on (event: 'friendship', l: (f: Friendship) => void): this
|
|
48
52
|
on (event: 'room-join' | 'room-leave', l: (p: { room: Room, contactList: Contact[], idList: string[], timestamp?: number }) => void): this
|
|
49
53
|
on (event: 'room-topic', l: (p: { room: Room, topic?: string, oldTopic?: string, timestamp?: number }) => void): this
|
|
50
|
-
|
|
54
|
+
// 登录成功: 回调收到 typed 的当前登录者 Contact(已从 payload 构造并缓存到 bot.self)。
|
|
55
|
+
on (event: 'login', l: (self: Contact) => void): this
|
|
51
56
|
on (event: 'scan', l: (p: { qrcode?: string, status?: any }) => void): this
|
|
52
57
|
// 登录阶段(扫码/验证码全流程): phase = scan|scanned|need-verify|verifying|verify-ok|verify-fail|online...
|
|
53
58
|
on (event: 'login-phase', l: (p: { phase: string, label?: string }) => void): this
|
|
54
|
-
// 探针/代理侧错误信号(不中断运行, 仅上报):
|
|
55
|
-
on (event: 'error', l: (
|
|
59
|
+
// 探针/代理侧错误信号(不中断运行, 仅上报): 回调收到一个真正的 Error 对象。
|
|
60
|
+
on (event: 'error', l: (err: Error) => void): this
|
|
56
61
|
// 探针心跳(~30s 一次), 供上层做存活/掉线判定: { state }。
|
|
57
62
|
on (event: 'heartbeat', l: (p: { state?: string }) => void): this
|
|
58
63
|
on (event: 'logout' | 'ready' | 'stop' | 'disconnected', l: () => void): this
|
|
@@ -74,12 +79,17 @@ export class Chatboty implements BotApi {
|
|
|
74
79
|
room: new Room(this, { id: w.roomId, topic: w.topic }), topic: w.topic, oldTopic: w.oldTopic, timestamp: w.timestamp,
|
|
75
80
|
}))
|
|
76
81
|
this.client.on('friendship', (w) => this.emit('friendship', this.toFriendship(w)))
|
|
77
|
-
this.client.on('login', (w) =>
|
|
82
|
+
this.client.on('login', (w) => {
|
|
83
|
+
// 把 payload 构造成 typed self Contact, 缓存并向上抛(供 message.mentionSelf/self 判定)。
|
|
84
|
+
const self = new Contact(this, this.toContactPayload(w.self ?? w))
|
|
85
|
+
this._self = self
|
|
86
|
+
this.emit('login', self)
|
|
87
|
+
})
|
|
78
88
|
this.client.on('logout', () => { this._self = undefined; this.emit('logout') })
|
|
79
89
|
this.client.on('scan', (w) => this.emit('scan', { qrcode: w.qrcode, status: w.status }))
|
|
80
90
|
this.client.on('login-phase', (w) => this.emit('login-phase', { phase: w.phase, label: w.label }))
|
|
81
91
|
this.client.on('disconnected', () => this.emit('disconnected'))
|
|
82
|
-
this.client.on('error', (w) => this.emit('error',
|
|
92
|
+
this.client.on('error', (w) => this.emit('error', new Error(w.error ?? '未知错误')))
|
|
83
93
|
this.client.on('heartbeat', (w) => this.emit('heartbeat', { state: w.state }))
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -143,6 +153,11 @@ export class Chatboty implements BotApi {
|
|
|
143
153
|
return this.client.command('enterVerifyCode', { code })
|
|
144
154
|
}
|
|
145
155
|
|
|
156
|
+
/** 刷新登录二维码(仅未登录/扫码态有效): 令探针重新取一张新码, 旧码作废。 */
|
|
157
|
+
async refreshQrCode (): Promise<any> {
|
|
158
|
+
return this.command('refreshQr')
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
/** BotApi: 当前登录者 payload(供实体做本机 self 判定, 如 Message.mentionSelf)。 */
|
|
147
162
|
selfContact (): ContactPayload | undefined { return this._self?.payload }
|
|
148
163
|
|
|
@@ -177,10 +192,36 @@ export class Chatboty implements BotApi {
|
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
/* ---------------- 查询/工厂 ---------------- */
|
|
180
|
-
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 查找群列表。无 query 时返回全部已加载群; 传 query 则在其上本地过滤。
|
|
198
|
+
* @param query 关键词字符串(匹配 topic/id 子串), 或 { topic?, id? } 精确匹配。
|
|
199
|
+
* @returns 命中的 Room 列表(可能为空)。
|
|
200
|
+
*/
|
|
201
|
+
async roomFindAll (query?: RoomQueryFilter): Promise<Room[]> {
|
|
181
202
|
const r = await this.command('roomList')
|
|
182
203
|
const arr = (r && (r.rooms || r.list || (Array.isArray(r) ? r : []))) || []
|
|
183
|
-
|
|
204
|
+
let list: Room[] = arr.map((x: any) => new Room(this, { id: x.conversationId || x.id, topic: x.name || x.topic }))
|
|
205
|
+
if (query !== undefined) {
|
|
206
|
+
if (typeof query === 'string') {
|
|
207
|
+
const kw = query.toLowerCase()
|
|
208
|
+
list = list.filter((rm) => (rm.topic() ?? '').toLowerCase().includes(kw) || rm.id.toLowerCase().includes(kw))
|
|
209
|
+
} else {
|
|
210
|
+
if (query.id) list = list.filter((rm) => rm.id === query.id)
|
|
211
|
+
if (query.topic) list = list.filter((rm) => rm.topic() === query.topic)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return list
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 查找单个群(取 {@link roomFindAll} 第一个命中)。
|
|
219
|
+
* @param query 关键词字符串或 { topic?, id? }。
|
|
220
|
+
* @returns 命中的第一个 Room; 无则 undefined。
|
|
221
|
+
*/
|
|
222
|
+
async roomFind (query: RoomQueryFilter): Promise<Room | undefined> {
|
|
223
|
+
const all = await this.roomFindAll(query)
|
|
224
|
+
return all[0]
|
|
184
225
|
}
|
|
185
226
|
|
|
186
227
|
async contactList (): Promise<Contact[]> {
|
|
@@ -202,7 +243,7 @@ export class Chatboty implements BotApi {
|
|
|
202
243
|
const arr = (r && (r.contacts || r.list || (Array.isArray(r) ? r : []))) || []
|
|
203
244
|
let list = arr.map((u: any) => new Contact(this, this.toContactPayload(u)))
|
|
204
245
|
if (query && typeof query !== 'string') { // 对象条件: 在子串命中基础上再做精确收窄
|
|
205
|
-
if (query.id) list = list.filter((c: Contact) => c.id
|
|
246
|
+
if (query.id) list = list.filter((c: Contact) => c.id === query.id)
|
|
206
247
|
if (query.alias) list = list.filter((c: Contact) => c.alias() === query.alias)
|
|
207
248
|
if (query.name) list = list.filter((c: Contact) => c.name() === query.name)
|
|
208
249
|
}
|
package/src/entity/contact.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { BotApi, ContactPayload, Sayable } from '../types.js'
|
|
2
2
|
import { ContactType } from '../types.js'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* 联系人实体(含外部客户)。
|
|
6
|
+
*
|
|
7
|
+
* 【同步 vs 异步约定】实体已持有的数据(来自事件 payload / 缓存)用【同步】读取;
|
|
8
|
+
* 需要回连接器/企微的操作一律 async。
|
|
9
|
+
* - 同步(缓存): id / name() / alias() / avatar() / phone() / title() / type() / conversationId()
|
|
10
|
+
* - 异步(动作): say()
|
|
11
|
+
*/
|
|
5
12
|
export class Contact {
|
|
6
|
-
|
|
13
|
+
/** 联系人 id(vid; 同步只读属性)。 */
|
|
14
|
+
readonly id: string
|
|
15
|
+
|
|
16
|
+
constructor (private readonly bot: BotApi, readonly payload: ContactPayload) {
|
|
17
|
+
this.id = payload.id
|
|
18
|
+
}
|
|
7
19
|
|
|
8
|
-
id (): string { return this.payload.id }
|
|
9
20
|
name (): string { return this.payload.name ?? '' }
|
|
10
21
|
alias (): string | undefined { return this.payload.alias }
|
|
11
22
|
avatar (): string | undefined { return this.payload.avatar }
|
|
@@ -24,9 +35,9 @@ export class Contact {
|
|
|
24
35
|
/** 给该联系人发消息(单聊)。 */
|
|
25
36
|
async say (sayable: Sayable): Promise<any> {
|
|
26
37
|
const conv = this.payload.conversationId
|
|
27
|
-
if (!conv) throw new Error(`Contact ${this.id
|
|
38
|
+
if (!conv) throw new Error(`Contact ${this.id} 无单聊会话 id, 无法发送`)
|
|
28
39
|
return this.bot.sayTo(conv, sayable)
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
toString (): string { return `Contact<${this.name() || this.id
|
|
42
|
+
toString (): string { return `Contact<${this.name() || this.id}>` }
|
|
32
43
|
}
|
package/src/entity/friendship.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { BotApi, FriendshipPayload, FriendshipKind } from '../types.js'
|
|
2
2
|
import { Contact } from './contact.js'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* 好友关系事件实体。
|
|
6
|
+
*
|
|
7
|
+
* 【同步 vs 异步约定】本实体全部数据来自事件 payload, 故 kind()/hello()/contact() 均为同步。
|
|
8
|
+
* (Friendship 无自身 id; 相关联系人 id 见 contact().id。)
|
|
9
|
+
*/
|
|
5
10
|
export class Friendship {
|
|
6
11
|
constructor (private readonly bot: BotApi, readonly payload: FriendshipPayload) {}
|
|
7
12
|
|
package/src/entity/message.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import type { BotApi, MessagePayload, Sayable
|
|
1
|
+
import type { BotApi, MessagePayload, Sayable } from '../types.js'
|
|
2
2
|
import { MessageType } from '../types.js'
|
|
3
3
|
import { Contact } from './contact.js'
|
|
4
4
|
import { Room } from './room.js'
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { ReceivedFile } from './received-file.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 消息实体。
|
|
9
|
+
*
|
|
10
|
+
* 【同步 vs 异步约定】实体已持有的数据(来自事件 payload / 缓存)用【同步】方法直接读取;
|
|
11
|
+
* 任何需要回连接器/企微(下载、拉通讯录、发送)的操作一律 async。
|
|
12
|
+
* - 同步(缓存): id / type() / text() / self() / talker() / room() / mentionSelf() / timestamp()
|
|
13
|
+
* - 异步(round-trip/动作): say() / toFileBox() / download() / mentionList()
|
|
14
|
+
*/
|
|
7
15
|
export class Message {
|
|
8
|
-
|
|
16
|
+
/** 消息 id(来自事件 payload; 同步只读属性)。未知时为空串。 */
|
|
17
|
+
readonly id: string
|
|
18
|
+
|
|
19
|
+
constructor (private readonly bot: BotApi, readonly payload: MessagePayload) {
|
|
20
|
+
this.id = payload.id ?? ''
|
|
21
|
+
}
|
|
9
22
|
|
|
10
|
-
id (): string | undefined { return this.payload.id }
|
|
11
23
|
type (): MessageType { return this.payload.type }
|
|
12
24
|
text (): string { return this.payload.text ?? '' }
|
|
13
25
|
self (): boolean { return !!this.payload.self }
|
|
14
26
|
timestamp (): number | undefined { return this.payload.timestamp }
|
|
15
|
-
|
|
27
|
+
/** 随事件附带的媒体(若探针已下载); 无则 undefined。取字节请用 {@link toFileBox}。 */
|
|
28
|
+
file (): ReceivedFile | null | undefined {
|
|
29
|
+
return this.payload.file ? new ReceivedFile(this.payload.file) : this.payload.file
|
|
30
|
+
}
|
|
16
31
|
miniProgram (): Record<string, any> | null | undefined { return this.payload.miniProgram }
|
|
17
32
|
urlLink (): Record<string, any> | null | undefined { return this.payload.urlLink }
|
|
18
33
|
|
|
@@ -57,7 +72,7 @@ export class Message {
|
|
|
57
72
|
/**
|
|
58
73
|
* 消息中被 @ 的联系人列表(best-effort)。
|
|
59
74
|
* 先从正文解析出 @显示名, 再用一次 contactList 拉全量通讯录按 name/alias 就近解析成完整 Contact;
|
|
60
|
-
* 未在已加载通讯录中的成员回退为仅含名字的 Contact(其 id
|
|
75
|
+
* 未在已加载通讯录中的成员回退为仅含名字的 Contact(其 id 为空串)。
|
|
61
76
|
* @returns 被 @ 的 Contact 数组(无 @ 时为空数组)。
|
|
62
77
|
*/
|
|
63
78
|
async mentionList (): Promise<Contact[]> {
|
|
@@ -95,27 +110,28 @@ export class Message {
|
|
|
95
110
|
|
|
96
111
|
/* ---------------- 媒体下载 ---------------- */
|
|
97
112
|
|
|
98
|
-
private
|
|
113
|
+
private toFileFromResult (r: any): ReceivedFile | null {
|
|
99
114
|
if (!r) return null
|
|
100
115
|
const dataUrl = r.dataUrl || r.base64
|
|
101
116
|
if (!dataUrl) return null
|
|
102
|
-
return { dataUrl, mime: r.mime, name: r.name, size: r.size }
|
|
117
|
+
return new ReceivedFile({ dataUrl, mime: r.mime, name: r.name, size: r.size })
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/**
|
|
106
|
-
* 取本条消息的媒体/文件字节(图片/文件/音视频), 返回 {@link
|
|
121
|
+
* 取本条消息的媒体/文件字节(图片/文件/音视频), 返回 {@link ReceivedFile}(含 base64 data URL,
|
|
122
|
+
* 并带 toBuffer()/save() 便捷方法)。
|
|
107
123
|
* - 收到的图片/文件事件通常已附带 file(探针后台已下载解密) → 直接返回。
|
|
108
124
|
* - 否则按类型冷调探针下载: 图片走 downloadImage, 文件/音视频走 downloadFile(按 msgid+会话+文件名定位缓存)。
|
|
109
|
-
* @returns
|
|
125
|
+
* @returns ReceivedFile; 非媒体消息或下载失败返回 null。
|
|
110
126
|
*/
|
|
111
|
-
async toFileBox (): Promise<
|
|
112
|
-
if (this.payload.file) return this.payload.file
|
|
127
|
+
async toFileBox (): Promise<ReceivedFile | null> {
|
|
128
|
+
if (this.payload.file) return new ReceivedFile(this.payload.file)
|
|
113
129
|
const msgid = this.payload.id
|
|
114
130
|
if (!msgid) return null
|
|
115
131
|
const t = this.payload.type
|
|
116
132
|
if (t === MessageType.Image) {
|
|
117
133
|
const r = await this.bot.command('downloadImage', { msgid })
|
|
118
|
-
return this.
|
|
134
|
+
return this.toFileFromResult(r)
|
|
119
135
|
}
|
|
120
136
|
if (t === MessageType.File || t === MessageType.Video || t === MessageType.Audio) {
|
|
121
137
|
const r = await this.bot.command('downloadFile', {
|
|
@@ -124,24 +140,13 @@ export class Message {
|
|
|
124
140
|
timestamp: this.payload.timestamp,
|
|
125
141
|
name: this.payload.text,
|
|
126
142
|
})
|
|
127
|
-
return this.
|
|
143
|
+
return this.toFileFromResult(r)
|
|
128
144
|
}
|
|
129
145
|
return null
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
/** {@link toFileBox} 的别名(下载本条消息的媒体字节)。 */
|
|
133
|
-
async download (): Promise<
|
|
134
|
-
|
|
135
|
-
/* ---------------- 转发(RE 待定) ---------------- */
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 转发本条消息到另一会话(群/单聊 conversationId, 或 Room/Contact)。
|
|
139
|
-
* ⚠️ 未实现: 企微转发需逆向定位并冷调"转发消息"服务(见 puppet-wework 交付说明的 RE 计划)。
|
|
140
|
-
* 当前会抛错, 以免静默失败。
|
|
141
|
-
*/
|
|
142
|
-
async forward (_to: string | Room | Contact): Promise<never> {
|
|
143
|
-
throw new Error('Message.forward 未实现: 需逆向定位企微转发消息服务(ForwardMessage), 见 RE 计划')
|
|
144
|
-
}
|
|
149
|
+
async download (): Promise<ReceivedFile | null> { return this.toFileBox() }
|
|
145
150
|
|
|
146
151
|
toString (): string { return `Message<${MessageType[this.type()]}:${this.text().slice(0, 30)}>` }
|
|
147
152
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises'
|
|
2
|
+
import type { FileBox } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 收到的媒体/文件(图片/文件/音视频)。自包含, 零外部依赖。
|
|
6
|
+
*
|
|
7
|
+
* 除原始字段(dataUrl/mime/name/size)外, 额外提供便捷方法:
|
|
8
|
+
* - toBuffer(): 同步, 把 dataUrl/base64 解码成原始字节(Buffer)。
|
|
9
|
+
* - save(path): 异步, 把字节写到磁盘。
|
|
10
|
+
*/
|
|
11
|
+
export class ReceivedFile {
|
|
12
|
+
/** base64 data URL(形如 `data:<mime>;base64,<...>`)。 */
|
|
13
|
+
readonly dataUrl?: string
|
|
14
|
+
/** MIME 类型(如 image/png)。 */
|
|
15
|
+
readonly mime?: string
|
|
16
|
+
/** 文件名(若探针可解析)。 */
|
|
17
|
+
readonly name?: string
|
|
18
|
+
/** 字节数(若已知)。 */
|
|
19
|
+
readonly size?: number
|
|
20
|
+
|
|
21
|
+
constructor (init: FileBox) {
|
|
22
|
+
this.dataUrl = init.dataUrl
|
|
23
|
+
this.mime = init.mime
|
|
24
|
+
this.name = init.name
|
|
25
|
+
this.size = init.size
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 解出原始字节(从 dataUrl / 裸 base64 解码)。同步。 */
|
|
29
|
+
toBuffer (): Buffer {
|
|
30
|
+
const s = this.dataUrl ?? ''
|
|
31
|
+
const comma = s.indexOf(',')
|
|
32
|
+
const b64 = s.startsWith('data:') && comma >= 0 ? s.slice(comma + 1) : s
|
|
33
|
+
return Buffer.from(b64, 'base64')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 写盘到指定路径(需 round-trip 到磁盘, 故为 async)。 */
|
|
37
|
+
async save (path: string): Promise<void> {
|
|
38
|
+
// 用 Uint8Array 包一层: 新版 @types/node 的 writeFile 形参要求 Uint8Array<ArrayBuffer>,
|
|
39
|
+
// 而 Buffer 的底层可能是 SharedArrayBuffer(方差不兼容); 复制成纯 ArrayBuffer 视图即可。
|
|
40
|
+
await writeFile(path, new Uint8Array(this.toBuffer()))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toString (): string { return `ReceivedFile<${this.name ?? this.mime ?? 'unknown'}>` }
|
|
44
|
+
}
|
package/src/entity/room.ts
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
import type { BotApi, RoomPayload, Sayable } from '../types.js'
|
|
2
2
|
import { Contact } from './contact.js'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* 群实体。
|
|
6
|
+
*
|
|
7
|
+
* 【同步 vs 异步约定】实体已持有的数据(来自事件 payload / 缓存)用【同步】读取;
|
|
8
|
+
* 需要回连接器/企微(拉成员、改名、发送)的操作一律 async。
|
|
9
|
+
* - 同步(缓存): id / topic() / owner()
|
|
10
|
+
* - 异步(round-trip/动作): say() / rename() / memberAll()
|
|
11
|
+
*/
|
|
5
12
|
export class Room {
|
|
6
|
-
|
|
13
|
+
/** 群 id(R:..; 同步只读属性)。 */
|
|
14
|
+
readonly id: string
|
|
15
|
+
|
|
16
|
+
constructor (private readonly bot: BotApi, readonly payload: RoomPayload) {
|
|
17
|
+
this.id = payload.id
|
|
18
|
+
}
|
|
7
19
|
|
|
8
|
-
id (): string { return this.payload.id }
|
|
9
20
|
topic (): string | undefined { return this.payload.topic }
|
|
10
|
-
ownerId (): string | undefined { return this.payload.ownerId }
|
|
11
21
|
|
|
12
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* 群主(同步; 由缓存的 ownerId 构造)。
|
|
24
|
+
* 事件/列表未携带 ownerId 时返回 undefined; 需完整群主资料请用 {@link memberAll} 定位。
|
|
25
|
+
*/
|
|
26
|
+
owner (): Contact | undefined {
|
|
27
|
+
return this.payload.ownerId ? new Contact(this.bot, { id: this.payload.ownerId }) : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 群发消息(动作)。 */
|
|
13
31
|
async say (sayable: Sayable): Promise<any> {
|
|
14
32
|
return this.bot.sayTo(this.payload.id, sayable)
|
|
15
33
|
}
|
|
16
34
|
|
|
17
|
-
/** 改群名(冷改,
|
|
35
|
+
/** 改群名(冷改, 内外群通用; round-trip)。 */
|
|
18
36
|
async rename (topic: string): Promise<any> {
|
|
19
37
|
return this.bot.command('roomTopic', { conversationId: this.payload.id, topic })
|
|
20
38
|
}
|
|
21
39
|
|
|
22
|
-
/** 群成员(完整 Contact 对象;
|
|
40
|
+
/** 群成员(完整 Contact 对象; 受探针成员枚举局限; round-trip)。 */
|
|
23
41
|
async memberAll (): Promise<Contact[]> {
|
|
24
42
|
const r = await this.bot.command('roomMembers', { conversationId: this.payload.id })
|
|
25
43
|
const list = (r && (r.memberList || [])) as any[]
|
|
@@ -30,13 +48,5 @@ export class Room {
|
|
|
30
48
|
}))
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
async owner (): Promise<Contact | undefined> {
|
|
35
|
-
const r = await this.bot.command('roomMembers', { conversationId: this.payload.id })
|
|
36
|
-
const u = r && r.ownerUser
|
|
37
|
-
if (!u || !u.userId) return undefined
|
|
38
|
-
return new Contact(this.bot, { id: u.userId, name: u.name, alias: u.alias, avatar: u.avatar, conversationId: u.conversationId })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
toString (): string { return `Room<${this.topic() || this.id()}>` }
|
|
51
|
+
toString (): string { return `Room<${this.topic() || this.id}>` }
|
|
42
52
|
}
|
package/src/mod.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { Message } from './entity/message.js'
|
|
|
13
13
|
export { Contact } from './entity/contact.js'
|
|
14
14
|
export { Room } from './entity/room.js'
|
|
15
15
|
export { Friendship } from './entity/friendship.js'
|
|
16
|
+
export { ReceivedFile } from './entity/received-file.js'
|
|
16
17
|
|
|
17
18
|
export * from './types.js'
|
|
18
19
|
|
package/src/types.ts
CHANGED
|
@@ -42,11 +42,16 @@ export interface ContactPayload {
|
|
|
42
42
|
// 注: 企微企业通讯录未采集"性别"字段(探针 dump 的 User 结构无此项), 故 SDK 不暴露 gender()。
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** contactFind/contactFindAll 查询条件: 关键词字符串, 或按字段精确匹配。 */
|
|
46
46
|
export type ContactQueryFilter =
|
|
47
47
|
| string
|
|
48
48
|
| { name?: string, alias?: string, id?: string }
|
|
49
49
|
|
|
50
|
+
/** roomFind/roomFindAll 查询条件: 关键词字符串(匹配 topic/id 子串), 或按字段精确匹配。 */
|
|
51
|
+
export type RoomQueryFilter =
|
|
52
|
+
| string
|
|
53
|
+
| { topic?: string, id?: string }
|
|
54
|
+
|
|
50
55
|
/** 群。 */
|
|
51
56
|
export interface RoomPayload {
|
|
52
57
|
id: string // R:..
|