@tencent-rtc/trtc-agent-skills 0.1.4 → 0.1.6
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/CODEBUDDY.md +0 -24
- package/README.md +12 -0
- package/README.zh.md +12 -0
- package/bin/cli.js +18 -5
- package/knowledge-base/chat/web/index.yaml +168 -0
- package/knowledge-base/chat/web/path-d-signals.yaml +4 -0
- package/knowledge-base/docs/chat/debug/GenerateTestUserSig.js +23 -0
- package/knowledge-base/docs/chat/debug/lib-generate-test-usersig.min.js +1 -0
- package/knowledge-base/docs/chat/gen-usersig.md +129 -0
- package/knowledge-base/docs/chat/product.md +61 -0
- package/knowledge-base/docs/chat/restapi.md +227 -0
- package/knowledge-base/docs/chat/sdk/android/faq.md +7 -0
- package/knowledge-base/docs/chat/sdk/android/index.md +94 -0
- package/knowledge-base/docs/chat/sdk/flutter/faq.md +7 -0
- package/knowledge-base/docs/chat/sdk/flutter/index.md +83 -0
- package/knowledge-base/docs/chat/sdk/ios/faq.md +7 -0
- package/knowledge-base/docs/chat/sdk/ios/index.md +94 -0
- package/knowledge-base/docs/chat/sdk/web/faq.md +8 -0
- package/knowledge-base/docs/chat/sdk/web/index.md +97 -0
- package/knowledge-base/docs/chat/uikit/android/index.md +28 -0
- package/knowledge-base/docs/chat/uikit/flutter/index.md +17 -0
- package/knowledge-base/docs/chat/uikit/ios/index.md +28 -0
- package/knowledge-base/docs/chat/uikit/react/index.md +6 -0
- package/knowledge-base/docs/chat/uikit/uniapp/index.md +6 -0
- package/knowledge-base/docs/chat/uikit/vue3/index.md +6 -0
- package/knowledge-base/docs/chat/webhook.md +73 -0
- package/knowledge-base/products.yaml +2 -2
- package/knowledge-base/slices/chat/web/at-mention.md +417 -0
- package/knowledge-base/slices/chat/web/conversation-actions.md +178 -0
- package/knowledge-base/slices/chat/web/conversation-list.md +368 -0
- package/knowledge-base/slices/chat/web/detect-integration.md +36 -0
- package/knowledge-base/slices/chat/web/detect-style.md +88 -0
- package/knowledge-base/slices/chat/web/direct-chat-entry.md +210 -0
- package/knowledge-base/slices/chat/web/login-auth.md +244 -0
- package/knowledge-base/slices/chat/web/message-base-actions.md +273 -0
- package/knowledge-base/slices/chat/web/message-input.md +209 -0
- package/knowledge-base/slices/chat/web/message-list.md +532 -0
- package/knowledge-base/slices/chat/web/send-custom-message.md +189 -0
- package/knowledge-base/slices/chat/web/send-media.md +214 -0
- package/knowledge-base/slices/chat/web/state-api-skeleton.md +60 -0
- package/knowledge-base/slices/chat/web/style-guide.md +210 -0
- package/package.json +5 -1
- package/skills/trtc/SKILL.md +57 -23
- package/skills/trtc/hooks/topic_phase_gate.py +5 -0
- package/skills/trtc/tests/test_reporting_v2_docs_query.py +175 -0
- package/skills/trtc/tools/kb.py +130 -0
- package/skills/trtc/tools/reporting.py +23 -2
- package/skills/trtc/tools/reporting_v2.py +501 -0
- package/skills/trtc-ai-service/README.ja.md +209 -0
- package/skills/trtc-ai-service/README.md +23 -9
- package/skills/trtc-ai-service/README.zh-CN.md +23 -9
- package/skills/trtc-ai-service/SKILL.md +4 -4
- package/skills/trtc-chat/.docs-query.yaml +9 -0
- package/skills/trtc-chat/SKILL.md +186 -0
- package/skills/trtc-chat/docs/SKILL.md +31 -0
- package/skills/trtc-chat/flows/maintenance.md +19 -0
- package/skills/trtc-chat/flows/onboarding.md +79 -0
- package/skills/trtc-chat/references/01-detect-project.md +136 -0
- package/skills/trtc-chat/references/02-path-a-questions.md +182 -0
- package/skills/trtc-chat/references/02-path-a-scaffold-template.md +59 -0
- package/skills/trtc-chat/references/02-path-a-script.md +266 -0
- package/skills/trtc-chat/references/02-path-a-templates.md +159 -0
- package/skills/trtc-chat/references/03-path-b-script.md +226 -0
- package/skills/trtc-chat/references/04-path-c-script.md +95 -0
- package/skills/trtc-chat/references/04-uikit-redirect.md +32 -0
- package/skills/trtc-chat/references/05-path-d-script.md +735 -0
- package/skills/trtc-chat/references/05-slice-loading.md +139 -0
- package/skills/trtc-chat/references/06-a-defensive-coding.md +139 -0
- package/skills/trtc-chat/references/06-hard-rules.md +190 -0
- package/skills/trtc-chat/references/08-state-config.md +231 -0
- package/skills/trtc-chat/references/09-troubleshoot.md +91 -0
- package/skills/trtc-chat/references/10-references-index.md +63 -0
- package/skills/trtc-chat/references/11-what-to-do-next-template.md +338 -0
- package/skills/trtc-chat/references/12-page-composition.md +73 -0
- package/skills/trtc-chat/references/13-reporting.md +166 -0
- package/skills/trtc-chat/references/14-official-docs.md +169 -0
- package/skills/trtc-chat/references/execution-units.yaml +27 -0
- package/skills/trtc-chat/references/vue3.md +104 -0
- package/skills/trtc-chat/tests/test_chat_bundle_contract.py +330 -0
- package/skills/trtc-chat/tools/__init__.py +5 -0
- package/skills/trtc-chat/tools/_delegate.py +32 -0
- package/skills/trtc-chat/tools/flow.py +8 -0
- package/skills/trtc-chat/tools/kb.py +8 -0
- package/skills/trtc-chat/tools/reporting_v2.py +8 -0
- package/skills/trtc-chat/tools/session.py +8 -0
- package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
- package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: chat/message-base-actions
|
|
3
|
+
name: 消息基础操作
|
|
4
|
+
product: chat
|
|
5
|
+
platform: web
|
|
6
|
+
description: 消息基础操作(复制 / 撤回 / 删除 / 下载媒体)
|
|
7
|
+
applies-to: [tuikit-atomicx-vue3]
|
|
8
|
+
sdk-version: "tuikit-atomicx-vue3 >=6.0.0"
|
|
9
|
+
depends-on-stores: [MessageActionStore]
|
|
10
|
+
trigger-keywords: [消息操作, 复制消息, 撤回, 删除消息, 下载, copy, revoke, recall, delete message, download, 右键菜单, 长按菜单, context menu, message actions]
|
|
11
|
+
prerequisites: [login-auth, message-list]
|
|
12
|
+
tags: ['MessageActionStore']
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. 这个 slice 处理什么
|
|
16
|
+
|
|
17
|
+
对**单条消息**的轻量操作集:复制文本、撤回、删除、下载媒体。四个操作共享同一触发入口(长按 / 右键菜单 / ActionSheet),每个操作是"菜单项 → 一次调用 → 反馈"的简单链路。
|
|
18
|
+
|
|
19
|
+
> 不覆盖:
|
|
20
|
+
> - 表情回应 → `features/message-reaction.md`
|
|
21
|
+
> - 消息置顶 → 未来 `features/pinned-message.md`
|
|
22
|
+
> - 转发 → 未来 `features/forward-message.md`
|
|
23
|
+
> - 引用回复 → 未来 `features/quote-reply.md`
|
|
24
|
+
|
|
25
|
+
## 2. AI 思考清单
|
|
26
|
+
|
|
27
|
+
- 操作菜单触发方式?(桌面右键 / 移动端长按 / 消息悬浮按钮)
|
|
28
|
+
- 菜单用什么组件?(项目 UI 库 Dropdown / 自建 Popover / BottomSheet)
|
|
29
|
+
- 撤回时限?(默认 2 分钟,由服务端控制,前端不做倒计时)
|
|
30
|
+
- 删除是否需二次确认?(建议需要)
|
|
31
|
+
- 哪些消息类型显示哪些菜单项?(文本 → 复制可用;媒体 → 下载可用)
|
|
32
|
+
- 下载在 Web 端是否可用?(SDK `downloadMedia` web 端不支持,改用 `<a download>` 直接下载 URL)
|
|
33
|
+
|
|
34
|
+
## 3. SDK API 必读
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { useMessageActionStore } from 'tuikit-atomicx-vue3/chat'
|
|
38
|
+
import type { MessageInfo } from 'tuikit-atomicx-vue3/chat'
|
|
39
|
+
|
|
40
|
+
// 针对单条消息创建 action store
|
|
41
|
+
const actionStore = useMessageActionStore(message)
|
|
42
|
+
|
|
43
|
+
// 可用方法(本 slice 范围)
|
|
44
|
+
actionStore.revoke() // () => Promise<void> — 撤回
|
|
45
|
+
actionStore.delete() // () => Promise<void> — 删除(仅本地)
|
|
46
|
+
actionStore.downloadMedia() // (quality?: MediaQuality) => Promise<void> — ⚠️ Web 端不支持
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> ⚠️ `useMessageActionStore(message)` 的 `message` 是 `MessageInfo` 对象(必填),不是 msgID 字符串。
|
|
50
|
+
|
|
51
|
+
`MediaQuality` 枚举(Web 端无实际作用):
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
enum MediaQuality {
|
|
55
|
+
Thumbnail = 'thumbnail',
|
|
56
|
+
Standard = 'standard',
|
|
57
|
+
Original = 'original',
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3.1 Store 生命周期
|
|
62
|
+
|
|
63
|
+
- `useMessageActionStore` 内部 `onScopeDispose` 自动销毁,**不需要**手动调 `destroy()`
|
|
64
|
+
- ❗ **store 在菜单组件 `setup()` 顶层创建一次,菜单关闭时随组件销毁**——不需要每次操作重新创建
|
|
65
|
+
- ❗ **操作顺序约束**:必须在 store 的操作(`revoke()`/`delete()`)**完全执行完毕后**,才能 `emit('close')` 或卸载菜单组件
|
|
66
|
+
- 原因:`emit('close')` 触发父组件卸载菜单 → `onScopeDispose` 销毁 store;如果先 close 再用 store,抛 `MessageActionStore has been destroyed`
|
|
67
|
+
- ❌ 先 `emit('close')` 再通过 callback 调 `actionStore.delete()`
|
|
68
|
+
- ✅ 先 `await actionStore.delete()` 完成,再 `emit('close')`
|
|
69
|
+
|
|
70
|
+
### 3.2 复制(纯前端,无 SDK API)
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await navigator.clipboard.writeText(text)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 4. Hard rules
|
|
77
|
+
|
|
78
|
+
> ❗ 所有 SDK 异步调用必须遵循 `references/06-a-defensive-coding.md` 防御编程规范(try/catch/finally、formatError、错误反馈形式、状态锁)。本节规则是该规范的专属补充,不替代它。
|
|
79
|
+
|
|
80
|
+
### 4.1 Store 创建
|
|
81
|
+
|
|
82
|
+
- ❗ `useMessageActionStore(message)` 的 `message` 必须是完整 `MessageInfo` 对象
|
|
83
|
+
- ❌ 传 `msgID` 字符串
|
|
84
|
+
- ❌ 传 `{ msgID: '...' }` 不完整对象
|
|
85
|
+
- ❗ 不需要手动调 `destroy()`(`onScopeDispose` 已接管)
|
|
86
|
+
- ❌ 在 `onUnmounted` 里手动 `actionStore.destroy()`
|
|
87
|
+
- ❗ 不准 `chat.revokeMessage(...)` 等旧 API 绕过 store
|
|
88
|
+
|
|
89
|
+
### 4.2 操作菜单
|
|
90
|
+
|
|
91
|
+
- ❗ 必须有操作菜单 UI(不准只写函数不给入口)
|
|
92
|
+
- ❗ 菜单项根据消息类型 + 状态动态过滤:
|
|
93
|
+
|
|
94
|
+
| 菜单项 | 条件 |
|
|
95
|
+
|---|---|
|
|
96
|
+
| 复制 | `messageType === MessageType.Text` |
|
|
97
|
+
| 撤回 | `isSentBySelf === true`(时限由服务端判,前端不拦) |
|
|
98
|
+
| 删除 | 始终显示 |
|
|
99
|
+
| 下载 | `messageType in [Image, Video, Audio, File]` |
|
|
100
|
+
|
|
101
|
+
- ❗ 菜单至少渲染满足条件的项,不准全部隐藏只剩空菜单
|
|
102
|
+
- ❌ 某类消息打开菜单一片空白
|
|
103
|
+
- ❗ 菜单弹出位置必须做边界检测,不准溢出视口:
|
|
104
|
+
- 自建 Popover:计算触发元素的 `getBoundingClientRect()`,靠右边缘时向左弹出,靠底部时向上弹出
|
|
105
|
+
- 使用 UI 库 Dropdown/Popover 组件:启用其内置的 `placement` 自动翻转(如 `flip: true` / `placement="auto"`)
|
|
106
|
+
- ❌ 固定弹出方向(如始终 `right` / 始终 `bottom`)不做边界判断
|
|
107
|
+
|
|
108
|
+
### 4.2.1 操作反馈(通用规则)
|
|
109
|
+
|
|
110
|
+
- ❗ 所有操作(复制 / 撤回 / 删除 / 下载)必须 `try/catch`,失败时给用户 **可见的错误提示**(Toast)
|
|
111
|
+
- ❌ 操作失败静默吞掉错误,用户无感知
|
|
112
|
+
- ❌ 只 `console.error` 不给 UI 反馈
|
|
113
|
+
- ❗ 操作成功也应有轻反馈(复制 → "已复制" / 撤回 → 消息变为撤回提示 / 删除 → 消息消失 / 下载 → 浏览器开始下载)
|
|
114
|
+
- ❗ 错误信息用 `formatError(err)`(来自 `@/im/error-map`),不准直接用 `err?.message ?? String(err)` 原始暴露
|
|
115
|
+
|
|
116
|
+
### 4.3 复制
|
|
117
|
+
|
|
118
|
+
- ❗ 仅 Text 消息可复制(取 `msg.messagePayload.text`)
|
|
119
|
+
- ❗ 使用 `navigator.clipboard.writeText()`
|
|
120
|
+
- ❗ 复制成功后给用户轻反馈(toast / 短暂提示 "Copied")
|
|
121
|
+
- ❌ 无反馈,用户不知道是否成功
|
|
122
|
+
- ❗ `clipboard.writeText` 需 `try/catch`(部分浏览器无权限时会 reject)
|
|
123
|
+
|
|
124
|
+
### 4.4 撤回
|
|
125
|
+
|
|
126
|
+
- ❗ 调 `actionStore.revoke()`
|
|
127
|
+
- ❗ 仅对 `isSentBySelf === true` 的消息显示撤回选项
|
|
128
|
+
- ❌ 对别人消息也显示撤回
|
|
129
|
+
- ❗ 撤回失败(超时限)时展示 errorMsg(SDK 会抛错)
|
|
130
|
+
- ❗ 撤回成功后 messageList 自动更新为 Tips 消息(store 内部处理),前端不手动 splice
|
|
131
|
+
- ❌ 手动从 messageList 中删除该消息
|
|
132
|
+
|
|
133
|
+
### 4.5 删除
|
|
134
|
+
|
|
135
|
+
- ❗ 调 `actionStore.delete()`
|
|
136
|
+
- ❗ 必须二次确认,按钮做危险态:
|
|
137
|
+
- 有 UI 库 → 用 UI 库的 Dialog 组件
|
|
138
|
+
- 无 UI 库 + 有 Tailwind/SCSS → 自建最小 Dialog 复用已有 CSS 方案
|
|
139
|
+
- 无 UI 库 + 空项目 → 原生 `confirm()`,标注 `// TODO: 替换为 Dialog 组件`
|
|
140
|
+
- ❌ 点击直接删除无确认
|
|
141
|
+
- ❗ 删除是**仅本地**,不影响对方——需在确认弹窗中明确告知
|
|
142
|
+
- ❗ 删除成功后 messageList 自动同步,不手动 splice
|
|
143
|
+
|
|
144
|
+
### 4.6 下载媒体
|
|
145
|
+
|
|
146
|
+
- ❗ Web 端 `downloadMedia()` 不可用(SDK throw "not supported on web")
|
|
147
|
+
- ❗ Web 端替代方案:用 `<a>` 标签 + `download` 属性,href 取 payload 中的 URL:
|
|
148
|
+
- Image: `msg.messagePayload.originalImageUrl`
|
|
149
|
+
- Video: `msg.messagePayload.videoUrl`
|
|
150
|
+
- Audio: `msg.messagePayload.audioUrl`
|
|
151
|
+
- File: `msg.messagePayload.fileUrl`
|
|
152
|
+
- ❗ 不准调 `downloadMedia()` 后不 catch(会 throw)
|
|
153
|
+
- ❌ 无 try/catch 直接调 `downloadMedia()`
|
|
154
|
+
- ❗ 下载按钮仅对媒体类型消息显示
|
|
155
|
+
|
|
156
|
+
## 5. 反例库
|
|
157
|
+
|
|
158
|
+
> § 4 已内联主要反例。本节补充跨规则复合错误:
|
|
159
|
+
|
|
160
|
+
- ❌ 把 `useMessageActionStore` 在 MessageList 级别创建一个长期实例(应该是按消息按需创建)
|
|
161
|
+
- ❌ 撤回 + 删除写在同一个函数里一起调(二者是独立操作,不应耦合)
|
|
162
|
+
- ❌ 复制非 Text 消息时 `JSON.stringify(messagePayload)` 给用户(无意义)
|
|
163
|
+
- ❌ 先 `emit('close')` 再通过 callback 使用 store(操作顺序错误导致 `MessageActionStore has been destroyed`):
|
|
164
|
+
```ts
|
|
165
|
+
// ❌ 先 close → store 被 onScopeDispose 销毁 → callback 里 store 已死
|
|
166
|
+
function handleDelete() {
|
|
167
|
+
emit('close')
|
|
168
|
+
emit('request-delete', { confirm: async () => actionStore.delete() })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ✅ 先操作完成,再 close
|
|
172
|
+
async function handleDelete() {
|
|
173
|
+
const confirmed = await showConfirmDialog('确认删除?')
|
|
174
|
+
if (!confirmed) return
|
|
175
|
+
await actionStore.delete() // store 还活着
|
|
176
|
+
emit('close') // 操作完成后再销毁
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 6. UI 自由度
|
|
181
|
+
|
|
182
|
+
> 样式由 AI 自由发挥,遵循项目已有 CSS 方案(见 `_base/detect-style.md`)。
|
|
183
|
+
|
|
184
|
+
✅ 完全自由:菜单形态(Dropdown / Popover / BottomSheet)/ 触发方式(右键 / 长按 / 悬浮按钮)/ 图标 / 动画 / toast 样式
|
|
185
|
+
⚠️ 必须遵循项目现状:UI 库 / CSS 方案
|
|
186
|
+
⚠️ § 4 强约束不可放宽
|
|
187
|
+
|
|
188
|
+
## 7. 参考实现
|
|
189
|
+
|
|
190
|
+
```vue
|
|
191
|
+
<script setup lang="ts">
|
|
192
|
+
import { useMessageActionStore } from 'tuikit-atomicx-vue3/chat'
|
|
193
|
+
import { MessageType } from 'tuikit-atomicx-vue3/chat'
|
|
194
|
+
import type { MessageInfo } from 'tuikit-atomicx-vue3/chat'
|
|
195
|
+
|
|
196
|
+
const props = defineProps<{ message: MessageInfo }>()
|
|
197
|
+
|
|
198
|
+
// setup 顶层创建一次,菜单关闭时随组件 onScopeDispose 自动销毁
|
|
199
|
+
const actionStore = useMessageActionStore(props.message)
|
|
200
|
+
|
|
201
|
+
const canCopy = props.message.messageType === MessageType.Text
|
|
202
|
+
const canRevoke = props.message.isSentBySelf
|
|
203
|
+
const canDownload = [MessageType.Image, MessageType.Video, MessageType.Audio, MessageType.File]
|
|
204
|
+
.includes(props.message.messageType)
|
|
205
|
+
|
|
206
|
+
async function handleCopy() {
|
|
207
|
+
try {
|
|
208
|
+
await navigator.clipboard.writeText(props.message.messagePayload.text)
|
|
209
|
+
// 展示 "Copied" 提示
|
|
210
|
+
} catch { /* clipboard 权限失败兜底 */ }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function handleRevoke() {
|
|
214
|
+
try {
|
|
215
|
+
await actionStore.revoke() // 先操作
|
|
216
|
+
emit('close') // 操作完成后再关闭菜单
|
|
217
|
+
} catch (err: any) {
|
|
218
|
+
// 展示错误(如"撤回超时")
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function handleDelete() {
|
|
223
|
+
// 有 UI 库 → 用 UI 库 Dialog;有 Tailwind → 自建最小 Dialog
|
|
224
|
+
const confirmed = await showConfirmDialog(`确认删除该消息?(仅删除本地)`)
|
|
225
|
+
if (!confirmed) return
|
|
226
|
+
await actionStore.delete() // 先操作
|
|
227
|
+
emit('close') // 操作完成后再关闭菜单
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function handleDownload() {
|
|
231
|
+
const payload = props.message.messagePayload as any
|
|
232
|
+
let url = ''
|
|
233
|
+
switch (props.message.messageType) {
|
|
234
|
+
case MessageType.Image: url = payload.originalImageUrl; break
|
|
235
|
+
case MessageType.Video: url = payload.videoUrl; break
|
|
236
|
+
case MessageType.Audio: url = payload.audioUrl; break
|
|
237
|
+
case MessageType.File: url = payload.fileUrl; break
|
|
238
|
+
}
|
|
239
|
+
if (!url) return
|
|
240
|
+
const a = document.createElement('a')
|
|
241
|
+
a.href = url
|
|
242
|
+
a.download = payload.fileName || 'download'
|
|
243
|
+
a.click()
|
|
244
|
+
}
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
<template>
|
|
248
|
+
<!-- 菜单 UI(Dropdown / Popover / BottomSheet,按项目实现) -->
|
|
249
|
+
<div class="action-menu">
|
|
250
|
+
<button v-if="canCopy" @click="handleCopy">Copy</button>
|
|
251
|
+
<button v-if="canRevoke" @click="handleRevoke">Revoke</button>
|
|
252
|
+
<button @click="handleDelete" class="danger">Delete</button>
|
|
253
|
+
<button v-if="canDownload" @click="handleDownload">Download</button>
|
|
254
|
+
</div>
|
|
255
|
+
</template>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 可改
|
|
259
|
+
|
|
260
|
+
- 菜单形态 / 触发方式 / 图标 / 动画
|
|
261
|
+
- 确认弹窗形式(confirm / Dialog 组件)
|
|
262
|
+
- 复制成功反馈形式(toast / inline 提示)
|
|
263
|
+
- 下载实现(a 标签 / window.open)
|
|
264
|
+
|
|
265
|
+
### 不可改
|
|
266
|
+
|
|
267
|
+
- `useMessageActionStore(message)` 传完整 MessageInfo
|
|
268
|
+
- 不手动 destroy
|
|
269
|
+
- 不绕过 store 调旧 API
|
|
270
|
+
- 菜单项按类型/状态动态过滤
|
|
271
|
+
- 撤回仅 isSentBySelf
|
|
272
|
+
- 删除必须二次确认
|
|
273
|
+
- Web 端下载不调 `downloadMedia()`(改用 URL 直接下载)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: chat/message-input
|
|
3
|
+
name: 消息输入框
|
|
4
|
+
product: chat
|
|
5
|
+
platform: web
|
|
6
|
+
description: 消息输入框(文本输入 + 发送 + toolbar 扩展位 + sending/error 三态)
|
|
7
|
+
applies-to: [tuikit-atomicx-vue3]
|
|
8
|
+
sdk-version: "tuikit-atomicx-vue3 >=6.0.0"
|
|
9
|
+
depends-on-stores: [MessageInputStore]
|
|
10
|
+
trigger-keywords: [输入框, 消息输入, message input, 工具栏, toolbar, MessageInputStore, sendMessage, 发消息, 发文本]
|
|
11
|
+
prerequisites: [login-auth, message-list]
|
|
12
|
+
tags: ['MessageInputStore']
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. 这个 slice 处理什么
|
|
16
|
+
|
|
17
|
+
路径 A 基础功能。在 `src/components/chat/MessageInput.vue` 通过 `useMessageInputStore(conversationID)` 拿到 `sendMessage` API。**本 slice 只覆盖文本发送 + toolbar 扩展位 + sending/error 三态**。
|
|
18
|
+
|
|
19
|
+
> 其余 payload 类型不在 starter 范围:
|
|
20
|
+
> - `imageMessage` / `videoMessage` / `fileMessage` / `audioMessage` → `features/send-media.md`
|
|
21
|
+
> - `customMessage` → `features/send-custom-message.md`
|
|
22
|
+
> - `faceMessage` / `locationMessage` → 对应 feature slice
|
|
23
|
+
>
|
|
24
|
+
> `SendMessageInputOption` 的字段也按 feature 拆分(`quotedMessage` / `atUserList` / `needReadReceipt` 等),starter 不传。
|
|
25
|
+
|
|
26
|
+
## 2. AI 思考清单
|
|
27
|
+
|
|
28
|
+
- `conversationID` 从哪传入?(props / route / 全局 store)
|
|
29
|
+
- toolbar 位置?(输入框上方 / 下方)
|
|
30
|
+
- 键盘约定?(默认 Enter 发送 / Shift+Enter 换行,跟随项目如有不同)
|
|
31
|
+
- 扩展位形式?(`<slot>` / props / 具名按钮组)
|
|
32
|
+
|
|
33
|
+
## 3. SDK API 必读
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { useMessageInputStore } from 'tuikit-atomicx-vue3/chat'
|
|
37
|
+
|
|
38
|
+
const { sendMessage } = useMessageInputStore(props.conversationID)
|
|
39
|
+
// sendMessage: (payload: SendMessagePayload, option?: SendMessageInputOption) => Promise<MessageInfo>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`SendMessagePayload`(可辨识联合,starter 只用第一种):
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
type SendMessagePayload =
|
|
46
|
+
| { type: 'textMessage'; text: string } // ← starter 范围
|
|
47
|
+
| { type: 'customMessage'; customData: string; description?: string; extension?: string }
|
|
48
|
+
| { type: 'imageMessage'; file: File | HTMLInputElement; width?: number; height?: number }
|
|
49
|
+
| { type: 'audioMessage'; file: File | HTMLInputElement; duration: number }
|
|
50
|
+
| { type: 'videoMessage'; file: File | HTMLInputElement; duration: number; ... }
|
|
51
|
+
| { type: 'fileMessage'; file: File | HTMLInputElement }
|
|
52
|
+
| { type: 'locationMessage'; description: string; longitude: number; latitude: number }
|
|
53
|
+
| { type: 'faceMessage'; index: number; data: string }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> `payload.type` 用小写驼峰(`textMessage`),不是旧版 `TIMTextElem`。
|
|
57
|
+
|
|
58
|
+
`SendMessageInputOption`(starter 不传,留给 features):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface SendMessageInputOption {
|
|
62
|
+
atUserList?: string[]
|
|
63
|
+
quotedMessage?: MessageInfo
|
|
64
|
+
needReadReceipt?: boolean
|
|
65
|
+
isExtensionEnabled?: boolean
|
|
66
|
+
onlineUserOnly?: boolean
|
|
67
|
+
offlinePushInfo?: { title?: string; description?: string; extensionInfo?: Record<string, unknown> }
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 4. Hard rules
|
|
72
|
+
|
|
73
|
+
> ❗ 所有 SDK 异步调用必须遵循 `references/06-a-defensive-coding.md` 防御编程规范(try/catch/finally、formatError、错误反馈形式、状态锁)。本节规则是该规范的专属补充,不替代它。
|
|
74
|
+
> ❗ 写 UI 代码前必须先 `read_file _base/style-guide.md`,按其规范生成样式,不准跳过。
|
|
75
|
+
|
|
76
|
+
### 4.1 调用范式
|
|
77
|
+
|
|
78
|
+
- ❗ `useMessageInputStore(props.conversationID)` 必填 conversationID
|
|
79
|
+
- ❌ 不传参(运行时报错)
|
|
80
|
+
- ❗ 唯一入口 `sendMessage(payload, option?)`,禁止绕过 store 调旧 API
|
|
81
|
+
- ❌ `chat.createTextMessage(...)` + `chat.sendMessage(...)`
|
|
82
|
+
- ❗ `payload.type` 必须用字面量 `'textMessage'`
|
|
83
|
+
- ❌ `'TIMTextElem'` / `'text'` / 大写驼峰
|
|
84
|
+
- ❗ 文本 `trim()` 后为空禁止发送(按钮 disabled + 早返回双重拦截)
|
|
85
|
+
- ❗ `try/catch/finally`:sending 锁住 → catch 显示 errorMsg → finally 复位
|
|
86
|
+
- ❌ `sending` 没在 finally 复位
|
|
87
|
+
- ❌ catch 里 console.error 但 UI 不显示
|
|
88
|
+
- ❗ 成功后不手动 push 到 messageList(store 自动同步)
|
|
89
|
+
- ❗ 不准手动调 `destroy()`(已由 onScopeDispose 自动接管)
|
|
90
|
+
- ❗ starter 不传 option(`quotedMessage` / `atUserList` 等属于 features)
|
|
91
|
+
|
|
92
|
+
### 4.2 输入与键盘
|
|
93
|
+
|
|
94
|
+
- ❗ textarea 用 `v-model`,不准 `ref="el"` + `el.value.value` 取 DOM 值
|
|
95
|
+
- ❗ Enter 发送 + Shift+Enter 换行(跟随项目如有不同)
|
|
96
|
+
- ❗ Enter 发送时 `e.preventDefault()`
|
|
97
|
+
- ❌ 发送同时插入换行
|
|
98
|
+
- ❗ 成功后清空 `text = ''`,并在 `sending` 复位后 `nextTick()` 再 `focus()` 回 textarea
|
|
99
|
+
- ❌ 在 `try` 块内直接 `focus()`(此时 `sending=true`,textarea 处于 disabled 状态,focus 无效)
|
|
100
|
+
- ✅ `focus()` 必须在 `finally` 里 `sending.value = false` 之后,配合 `await nextTick()`
|
|
101
|
+
|
|
102
|
+
### 4.3 toolbar 扩展位
|
|
103
|
+
|
|
104
|
+
- ❗ 必须渲染 toolbar,不准默认隐藏
|
|
105
|
+
- ❗ 必须暴露扩展位(slot / props / 具名按钮组三选一),后续 features 能追加按钮而不改源码
|
|
106
|
+
- ❗ 基础 4 件套阶段 toolbar 不挂任何业务按钮
|
|
107
|
+
- ❌ starter 里硬塞评价 / 订单 / 优惠券按钮
|
|
108
|
+
|
|
109
|
+
### 4.4 错误反馈
|
|
110
|
+
|
|
111
|
+
- ❗ errorMsg 就近显示(输入框附近小字),不用 alert / 顶部 toast
|
|
112
|
+
- ❗ 用户开始输入时清空 errorMsg
|
|
113
|
+
- ❗ catch 读 `err?.message ?? String(err)`
|
|
114
|
+
|
|
115
|
+
## 5. 反例库
|
|
116
|
+
|
|
117
|
+
> § 4 已内联主要反例。本节补充跨规则复合错误:
|
|
118
|
+
|
|
119
|
+
- ❌ `useMessageInputStore()` 不传 ID + 解构 `destroy` 手动调 + 绕过 store 调旧 API — 三重错误组合
|
|
120
|
+
- ❌ starter 阶段传 `option = { quotedMessage, atUserList }` — 会和未来 feature 打架
|
|
121
|
+
|
|
122
|
+
## 6. UI 自由度
|
|
123
|
+
|
|
124
|
+
> 样式由 AI 自由发挥,遵循项目已有 CSS 方案(见 `_base/detect-style.md`)。
|
|
125
|
+
|
|
126
|
+
✅ 完全自由:输入框形状 / toolbar 位置 / 按钮图标 / 发送按钮形态 / textarea 行数 / 错误提示位置
|
|
127
|
+
⚠️ 必须遵循项目现状:UI 库 / 同类组件 / CSS 方案
|
|
128
|
+
⚠️ § 4 强约束不可放宽
|
|
129
|
+
|
|
130
|
+
## 7. 参考实现
|
|
131
|
+
|
|
132
|
+
```vue
|
|
133
|
+
<script setup lang="ts">
|
|
134
|
+
import { ref, useTemplateRef, watch, nextTick } from 'vue'
|
|
135
|
+
import { useMessageInputStore } from 'tuikit-atomicx-vue3/chat'
|
|
136
|
+
|
|
137
|
+
const props = defineProps<{ conversationID: string }>()
|
|
138
|
+
const { sendMessage } = useMessageInputStore(props.conversationID)
|
|
139
|
+
|
|
140
|
+
const text = ref('')
|
|
141
|
+
const sending = ref(false)
|
|
142
|
+
const errorMsg = ref<string | null>(null)
|
|
143
|
+
const textareaEl = useTemplateRef<HTMLTextAreaElement>('textareaEl')
|
|
144
|
+
|
|
145
|
+
watch(text, (v) => { if (v.length > 0 && errorMsg.value) errorMsg.value = null })
|
|
146
|
+
|
|
147
|
+
async function handleSend() {
|
|
148
|
+
const trimmed = text.value.trim()
|
|
149
|
+
if (!trimmed || sending.value) return
|
|
150
|
+
errorMsg.value = null
|
|
151
|
+
sending.value = true
|
|
152
|
+
try {
|
|
153
|
+
await sendMessage({ type: 'textMessage', text: trimmed })
|
|
154
|
+
text.value = ''
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
errorMsg.value = err?.message ?? String(err)
|
|
157
|
+
} finally {
|
|
158
|
+
sending.value = false
|
|
159
|
+
await nextTick()
|
|
160
|
+
textareaEl.value?.focus()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
165
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
166
|
+
e.preventDefault()
|
|
167
|
+
handleSend()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<template>
|
|
173
|
+
<div class="msg-input-wrapper">
|
|
174
|
+
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
|
175
|
+
|
|
176
|
+
<textarea
|
|
177
|
+
ref="textareaEl"
|
|
178
|
+
v-model="text"
|
|
179
|
+
:disabled="sending"
|
|
180
|
+
placeholder="Type a message..."
|
|
181
|
+
rows="3"
|
|
182
|
+
@keydown="onKeyDown"
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<div class="toolbar">
|
|
186
|
+
<slot name="toolbar-actions" />
|
|
187
|
+
<div class="spacer" />
|
|
188
|
+
<button type="button" :disabled="sending || !text.trim()" @click="handleSend">
|
|
189
|
+
{{ sending ? 'Sending…' : 'Send' }}
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</template>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 可改
|
|
197
|
+
|
|
198
|
+
- 样式 / class / Tailwind / 项目 token
|
|
199
|
+
- toolbar 位置、按钮图标库、发送按钮形态
|
|
200
|
+
- 错误提示位置(上/下/内嵌)
|
|
201
|
+
- 扩展位形式(slot / props / 按钮组)
|
|
202
|
+
|
|
203
|
+
### 不可改
|
|
204
|
+
|
|
205
|
+
- `useMessageInputStore(conversationID)` 解构 `sendMessage`
|
|
206
|
+
- 唯一调用 `await sendMessage({ type: 'textMessage', text })`
|
|
207
|
+
- 三态闭环(disabled + try/catch/finally + errorMsg 就近)
|
|
208
|
+
- Enter 发送 preventDefault + 清空 + finally 里 nextTick + focus
|
|
209
|
+
- toolbar 必须存在 + 必须留扩展位 + 不挂业务按钮
|