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.
Files changed (72) hide show
  1. package/README.md +31 -11
  2. package/dist/cjs/src/chatboty.d.ts +15 -6
  3. package/dist/cjs/src/chatboty.d.ts.map +1 -1
  4. package/dist/cjs/src/chatboty.js +37 -5
  5. package/dist/cjs/src/chatboty.js.map +1 -1
  6. package/dist/cjs/src/entity/contact.d.ts +10 -2
  7. package/dist/cjs/src/entity/contact.d.ts.map +1 -1
  8. package/dist/cjs/src/entity/contact.js +11 -4
  9. package/dist/cjs/src/entity/contact.js.map +1 -1
  10. package/dist/cjs/src/entity/friendship.d.ts +6 -1
  11. package/dist/cjs/src/entity/friendship.d.ts.map +1 -1
  12. package/dist/cjs/src/entity/friendship.js +6 -1
  13. package/dist/cjs/src/entity/friendship.js.map +1 -1
  14. package/dist/cjs/src/entity/message.d.ts +21 -16
  15. package/dist/cjs/src/entity/message.d.ts.map +1 -1
  16. package/dist/cjs/src/entity/message.js +23 -20
  17. package/dist/cjs/src/entity/message.js.map +1 -1
  18. package/dist/cjs/src/entity/received-file.d.ts +25 -0
  19. package/dist/cjs/src/entity/received-file.d.ts.map +1 -0
  20. package/dist/cjs/src/entity/received-file.js +35 -0
  21. package/dist/cjs/src/entity/received-file.js.map +1 -0
  22. package/dist/cjs/src/entity/room.d.ts +18 -8
  23. package/dist/cjs/src/entity/room.d.ts.map +1 -1
  24. package/dist/cjs/src/entity/room.js +20 -15
  25. package/dist/cjs/src/entity/room.js.map +1 -1
  26. package/dist/cjs/src/mod.d.ts +1 -0
  27. package/dist/cjs/src/mod.d.ts.map +1 -1
  28. package/dist/cjs/src/mod.js +3 -1
  29. package/dist/cjs/src/mod.js.map +1 -1
  30. package/dist/cjs/src/types.d.ts +6 -1
  31. package/dist/cjs/src/types.d.ts.map +1 -1
  32. package/dist/cjs/src/types.js.map +1 -1
  33. package/dist/esm/src/chatboty.d.ts +15 -6
  34. package/dist/esm/src/chatboty.d.ts.map +1 -1
  35. package/dist/esm/src/chatboty.js +37 -5
  36. package/dist/esm/src/chatboty.js.map +1 -1
  37. package/dist/esm/src/entity/contact.d.ts +10 -2
  38. package/dist/esm/src/entity/contact.d.ts.map +1 -1
  39. package/dist/esm/src/entity/contact.js +11 -4
  40. package/dist/esm/src/entity/contact.js.map +1 -1
  41. package/dist/esm/src/entity/friendship.d.ts +6 -1
  42. package/dist/esm/src/entity/friendship.d.ts.map +1 -1
  43. package/dist/esm/src/entity/friendship.js +6 -1
  44. package/dist/esm/src/entity/friendship.js.map +1 -1
  45. package/dist/esm/src/entity/message.d.ts +21 -16
  46. package/dist/esm/src/entity/message.d.ts.map +1 -1
  47. package/dist/esm/src/entity/message.js +23 -20
  48. package/dist/esm/src/entity/message.js.map +1 -1
  49. package/dist/esm/src/entity/received-file.d.ts +25 -0
  50. package/dist/esm/src/entity/received-file.d.ts.map +1 -0
  51. package/dist/esm/src/entity/received-file.js +31 -0
  52. package/dist/esm/src/entity/received-file.js.map +1 -0
  53. package/dist/esm/src/entity/room.d.ts +18 -8
  54. package/dist/esm/src/entity/room.d.ts.map +1 -1
  55. package/dist/esm/src/entity/room.js +20 -15
  56. package/dist/esm/src/entity/room.js.map +1 -1
  57. package/dist/esm/src/mod.d.ts +1 -0
  58. package/dist/esm/src/mod.d.ts.map +1 -1
  59. package/dist/esm/src/mod.js +1 -0
  60. package/dist/esm/src/mod.js.map +1 -1
  61. package/dist/esm/src/types.d.ts +6 -1
  62. package/dist/esm/src/types.d.ts.map +1 -1
  63. package/dist/esm/src/types.js.map +1 -1
  64. package/package.json +2 -1
  65. package/src/chatboty.ts +42 -9
  66. package/src/entity/contact.ts +16 -5
  67. package/src/entity/friendship.ts +6 -1
  68. package/src/entity/message.ts +32 -27
  69. package/src/entity/received-file.ts +44 -0
  70. package/src/entity/room.ts +26 -16
  71. package/src/mod.ts +1 -0
  72. package/src/types.ts +6 -1
@@ -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
 
@@ -1,18 +1,33 @@
1
- import type { BotApi, MessagePayload, Sayable, FileBox } from '../types.js'
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
- constructor (private readonly bot: BotApi, readonly payload: MessagePayload) {}
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
- file (): FileBox | null | undefined { return this.payload.file }
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 toFileBoxFromResult (r: any): FileBox | null {
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 FileBox}(含 base64 data URL)。
121
+ * 取本条消息的媒体/文件字节(图片/文件/音视频), 返回 {@link ReceivedFile}(含 base64 data URL,
122
+ * 并带 toBuffer()/save() 便捷方法)。
107
123
  * - 收到的图片/文件事件通常已附带 file(探针后台已下载解密) → 直接返回。
108
124
  * - 否则按类型冷调探针下载: 图片走 downloadImage, 文件/音视频走 downloadFile(按 msgid+会话+文件名定位缓存)。
109
- * @returns FileBox(dataUrl/mime/name/size); 非媒体消息或下载失败返回 null。
125
+ * @returns ReceivedFile; 非媒体消息或下载失败返回 null。
110
126
  */
111
- async toFileBox (): Promise<FileBox | null> {
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.toFileBoxFromResult(r)
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.toFileBoxFromResult(r)
143
+ return this.toFileFromResult(r)
128
144
  }
129
145
  return null
130
146
  }
131
147
 
132
148
  /** {@link toFileBox} 的别名(下载本条消息的媒体字节)。 */
133
- async download (): Promise<FileBox | null> { return this.toFileBox() }
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
+ }
@@ -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
- constructor (private readonly bot: BotApi, readonly payload: RoomPayload) {}
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
- /** Contact.find/findAll 查询条件: 关键词字符串, 或按字段精确匹配。 */
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:..