chatboty 0.1.0 → 0.2.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/README.md +31 -11
- package/dist/cjs/src/chatboty.d.ts +15 -6
- package/dist/cjs/src/chatboty.d.ts.map +1 -1
- package/dist/cjs/src/chatboty.js +37 -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 +15 -6
- package/dist/esm/src/chatboty.d.ts.map +1 -1
- package/dist/esm/src/chatboty.js +37 -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 +42 -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/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:..
|