@syfei49/mini-dynamic-renderer 1.0.4 → 1.0.9
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 +90 -21
- package/components/ly-ai-assistant/ly-ai-assistant.vue +249 -3
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/adapters/request.js +13 -5
- package/src/chat/parser.js +29 -1
- package/src/chat/service.js +81 -17
- package/src/config.js +2 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @syfei49/mini-dynamic-renderer
|
|
2
2
|
|
|
3
|
-
uni-app 浮层 AI
|
|
3
|
+
uni-app 浮层 AI 客服插件。在指定页面右下角显示 AI 浮层图标,点击后从底部弹出抽屉聊天面板,支持文字、图片、拍照/相册上传及 `[NEED_ICON]` 分类图标引导。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -9,65 +9,95 @@ cd your-uniapp-project
|
|
|
9
9
|
npm install @syfei49/mini-dynamic-renderer
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
本地调试可指向 tar 包:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@syfei49/mini-dynamic-renderer": "^1.0.9"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
12
22
|
微信开发者工具 → **工具** → **构建 npm**
|
|
13
23
|
|
|
14
24
|
## 接入
|
|
15
25
|
|
|
16
26
|
### 1. App.vue 初始化
|
|
17
27
|
|
|
28
|
+
只负责注入配置,不渲染 UI(微信小程序 `App.vue` 的 template 不会显示)。
|
|
29
|
+
|
|
18
30
|
```vue
|
|
19
31
|
<script setup>
|
|
20
32
|
import { init } from '@syfei49/mini-dynamic-renderer'
|
|
33
|
+
import { baseUrl } from '@/utils/request.js'
|
|
21
34
|
|
|
22
35
|
init({
|
|
23
|
-
baseUrl: 'https://
|
|
24
|
-
|
|
36
|
+
baseUrl: 'https://lygateway.ilyixy.com/prod-api',
|
|
37
|
+
uploadBaseUrl: 'https://jky.mall.ilyixy.com/prod-api',
|
|
38
|
+
appId: 'sass-mall-xxx',
|
|
39
|
+
chatApiPath: '/openapi/mini/ai/chat',
|
|
25
40
|
uploadApiPath: '/mini/common/upload',
|
|
26
41
|
getToken: () => uni.getStorageSync('token'),
|
|
27
42
|
visiblePages: ['pages/user/user'],
|
|
28
43
|
requireLogin: true,
|
|
29
|
-
loginPage: '/pages/login/login?noauto=1'
|
|
44
|
+
loginPage: '/pages/login/login?noauto=1',
|
|
45
|
+
debug: process.env.NODE_ENV === 'development'
|
|
30
46
|
})
|
|
31
47
|
</script>
|
|
32
48
|
```
|
|
33
49
|
|
|
34
|
-
### 2.
|
|
50
|
+
### 2. 目标页面引入组件
|
|
35
51
|
|
|
36
|
-
以 `pages/user/user.vue`
|
|
52
|
+
以 `pages/user/user.vue` 为例,模板末尾挂载一次:
|
|
37
53
|
|
|
38
54
|
```vue
|
|
39
|
-
<script setup>
|
|
40
|
-
import LyAiAssistant from '@syfei49/mini-dynamic-renderer/components/ly-ai-assistant/ly-ai-assistant.vue'
|
|
41
|
-
import { openChat } from '@syfei49/mini-dynamic-renderer'
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
55
|
<template>
|
|
45
|
-
<view>
|
|
46
|
-
<!--
|
|
56
|
+
<view class="container">
|
|
57
|
+
<!-- 页面原有内容 -->
|
|
58
|
+
<lyTabbar server="sc"></lyTabbar>
|
|
47
59
|
<LyAiAssistant />
|
|
48
60
|
</view>
|
|
49
61
|
</template>
|
|
62
|
+
|
|
63
|
+
<script setup>
|
|
64
|
+
import LyAiAssistant from '@syfei49/mini-dynamic-renderer/components/ly-ai-assistant/ly-ai-assistant.vue'
|
|
65
|
+
import { openChat } from '@syfei49/mini-dynamic-renderer'
|
|
66
|
+
</script>
|
|
50
67
|
```
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
### 3. 用 openChat 打开抽屉
|
|
53
70
|
|
|
54
|
-
|
|
71
|
+
把旧入口从 `uni.navigateTo('/pages/user/aiChat')` 改为直接打开抽屉:
|
|
55
72
|
|
|
56
73
|
```javascript
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
function gotoPath(item) {
|
|
75
|
+
if (item.title === '分销中心') {
|
|
76
|
+
handleDistributionCenter()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (item.title === 'AI客服') {
|
|
81
|
+
openChat()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 其他页面保持原来的 uni.navigateTo 逻辑
|
|
86
|
+
uni.navigateTo({ url: item.path })
|
|
87
|
+
}
|
|
60
88
|
```
|
|
61
89
|
|
|
62
90
|
## 配置项
|
|
63
91
|
|
|
64
92
|
| 字段 | 类型 | 默认值 | 说明 |
|
|
65
93
|
|------|------|--------|------|
|
|
66
|
-
| `baseUrl` | `string` | `''` | API
|
|
94
|
+
| `baseUrl` | `string` | `''` | API 根地址,示例 `https://lygateway.ilyixy.com/prod-api` |
|
|
95
|
+
| `uploadBaseUrl` | `string` | `''` | 上传接口基地址,默认与 `baseUrl` 一致,示例 `https://jky.mall.ilyixy.com/prod-api` |
|
|
96
|
+
| `appId` | `string` | `''` | 应用标识,配置后每次请求自动带上请求头 `X-App-Id` |
|
|
67
97
|
| `chatApiPath` | `string` | `'/mini/ai/chat'` | 对话接口路径 |
|
|
68
98
|
| `uploadApiPath` | `string` | `'/mini/common/upload'` | 图片上传路径 |
|
|
69
99
|
| `getToken` | `Function` | `() => ''` | 返回 Authorization |
|
|
70
|
-
| `getHeaders` | `Function` | `() => ({})` |
|
|
100
|
+
| `getHeaders` | `Function` | `() => ({})` | 额外请求头,与自动头合并 |
|
|
71
101
|
| `visiblePages` | `string[]` | `['pages/user/user']` | 显示浮层图标的页面路由 |
|
|
72
102
|
| `floatIcon` | `string` | 内置默认图标 | 浮层按钮图片 URL |
|
|
73
103
|
| `requireLogin` | `boolean` | `true` | 打开前检查登录 |
|
|
@@ -75,6 +105,8 @@ openChat()
|
|
|
75
105
|
| `debug` | `boolean` | `false` | 调试日志 |
|
|
76
106
|
| `enableInterceptor` | `boolean` | `false` | 是否启用 wx.request 拦截(预留) |
|
|
77
107
|
|
|
108
|
+
> 注意:`appId` 非空时,所有请求头都会自动注入 `X-App-Id: <appId>`。
|
|
109
|
+
|
|
78
110
|
## API
|
|
79
111
|
|
|
80
112
|
| 方法 | 说明 |
|
|
@@ -87,8 +119,45 @@ openChat()
|
|
|
87
119
|
| `uninstall()` | 卸载拦截器,恢复原始 `wx.request` |
|
|
88
120
|
| `parseRenderConfig(data)` | 解析响应中的渲染配置(预留) |
|
|
89
121
|
|
|
122
|
+
## 后端协议
|
|
123
|
+
|
|
124
|
+
### 1. 动态参数收集(`[RENDER_CONFIG]`)
|
|
125
|
+
|
|
126
|
+
后端可在 `answer` 中返回 `[RENDER_CONFIG]{...}` 来让插件渲染一个动态表单,用户填写后自动作为 `inputs` 再次发起对话请求。
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
请填写新增商品所需信息 [RENDER_CONFIG]{
|
|
130
|
+
"type": "form",
|
|
131
|
+
"title": "新增商品",
|
|
132
|
+
"description": "请补充以下字段",
|
|
133
|
+
"fields": [
|
|
134
|
+
{ "name": "productName", "label": "商品名称", "type": "input" },
|
|
135
|
+
{ "name": "price", "label": "价格", "type": "number" },
|
|
136
|
+
{ "name": "category", "label": "分类", "type": "select", "options": [{ "label": "保健品", "value": "1" }] },
|
|
137
|
+
{ "name": "cover", "label": "封面图", "type": "image" }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
支持字段类型:
|
|
143
|
+
|
|
144
|
+
- `input`:文本输入框
|
|
145
|
+
- `number`:数字输入框
|
|
146
|
+
- `select`:`picker` 选择器,需配 `options: [{ label, value }]`
|
|
147
|
+
- `image`:拍照 / 相册选择并上传
|
|
148
|
+
|
|
149
|
+
### 2. 分类图标引导(兼容 `[NEED_ICON]`)
|
|
150
|
+
|
|
151
|
+
当需要用户上传分类图标时,后端仍可在 `answer` 中返回:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
[NEED_ICON]{"name": "张三", "sort": 0, "msg": "请上传一张图片作为「张三」的分类图标"}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
插件会弹出「拍照 / 从相册选择 / 取消」选择条,并在上传后自动把图标 URL 拼回对话继续请求。
|
|
158
|
+
|
|
90
159
|
## 版本说明
|
|
91
160
|
|
|
92
|
-
- **v1.0.
|
|
161
|
+
- **v1.0.x**:uni-app 浮层 AI 插件、抽屉聊天、迁移 aiChat 全能力、文档拆分
|
|
93
162
|
- v1.0.0:wx.request 拦截 + hello world 弹窗(测试版)
|
|
94
163
|
- 后续版本将根据后端 `renderConfig` 动态渲染更多组件类型
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<view v-else class="ly-ai-float-text">AI</view>
|
|
6
6
|
</view>
|
|
7
7
|
|
|
8
|
-
<view v-if="opened" class="ly-ai-overlay" @click
|
|
9
|
-
<view class="ly-ai-drawer" :style="drawerStyle">
|
|
8
|
+
<view v-if="opened" class="ly-ai-overlay" @click="onOverlayClick">
|
|
9
|
+
<view class="ly-ai-drawer" :style="drawerStyle" @click.stop>
|
|
10
10
|
<view class="ly-ai-header">
|
|
11
11
|
<text class="ly-ai-title">AI 智能客服</text>
|
|
12
12
|
<view class="ly-ai-close" @click="closeChat">×</view>
|
|
@@ -37,7 +37,39 @@
|
|
|
37
37
|
</view>
|
|
38
38
|
</scroll-view>
|
|
39
39
|
|
|
40
|
-
<view v-if="
|
|
40
|
+
<view v-if="renderConfig" class="ly-ai-form-bar">
|
|
41
|
+
<view v-if="renderConfig.title" class="ly-ai-form-title">{{ renderConfig.title }}</view>
|
|
42
|
+
<view v-if="renderConfig.description" class="ly-ai-form-desc">{{ renderConfig.description }}</view>
|
|
43
|
+
<view v-for="(field, fIndex) in renderConfig.fields" :key="fIndex" class="ly-ai-form-item">
|
|
44
|
+
<text class="ly-ai-form-label">{{ field.label || field.name }}</text>
|
|
45
|
+
<template v-if="field.type === 'input' || field.type === 'number'">
|
|
46
|
+
<input class="ly-ai-form-input" :type="field.type === 'number' ? 'number' : 'text'"
|
|
47
|
+
:value="formInputs[field.name]" :placeholder="field.placeholder || '请输入' + (field.label || field.name)"
|
|
48
|
+
:disabled="loading || uploading" @input="onFormInput($event, field.name)" />
|
|
49
|
+
</template>
|
|
50
|
+
<template v-else-if="field.type === 'select'">
|
|
51
|
+
<picker class="ly-ai-form-picker" mode="selector" :range="getFieldOptions(field).labels"
|
|
52
|
+
:value="getFieldIndex(field)" :disabled="loading || uploading" @change="onFormSelect($event, field)">
|
|
53
|
+
<view class="ly-ai-form-picker-text">{{ getFieldSelectedLabel(field) }}</view>
|
|
54
|
+
</picker>
|
|
55
|
+
</template>
|
|
56
|
+
<template v-else-if="field.type === 'image'">
|
|
57
|
+
<view v-if="formInputs[field.name]" class="ly-ai-form-image-wrap">
|
|
58
|
+
<image class="ly-ai-form-image" :src="formInputs[field.name]" mode="aspectFill"
|
|
59
|
+
@click="previewImage(formInputs[field.name], [formInputs[field.name]])" />
|
|
60
|
+
<view class="ly-ai-form-image-remove" @click="onFormImageRemove(field.name)">×</view>
|
|
61
|
+
</view>
|
|
62
|
+
<view v-else class="ly-ai-form-image-btn" :class="{ disabled: loading || uploading }"
|
|
63
|
+
@click="onFormImageChoose(field.name)">拍照 / 相册</view>
|
|
64
|
+
</template>
|
|
65
|
+
</view>
|
|
66
|
+
<view class="ly-ai-form-actions">
|
|
67
|
+
<view class="ly-ai-form-btn confirm" :class="{ disabled: loading || uploading }" @click="submitForm">确认</view>
|
|
68
|
+
<view class="ly-ai-form-btn cancel" :class="{ disabled: loading || uploading }" @click="cancelForm">取消</view>
|
|
69
|
+
</view>
|
|
70
|
+
</view>
|
|
71
|
+
|
|
72
|
+
<view v-else-if="iconPickMode" class="ly-ai-icon-pick-bar">
|
|
41
73
|
<view class="ly-ai-icon-pick-title">请为「{{ pendingIconCategory && pendingIconCategory.name }}」选择分类图标</view>
|
|
42
74
|
<view class="ly-ai-icon-pick-actions">
|
|
43
75
|
<view class="ly-ai-icon-pick-btn" :class="{ disabled: loading || uploading }" @click="pickCategoryIcon('camera')">拍照</view>
|
|
@@ -91,6 +123,8 @@
|
|
|
91
123
|
const scrollIntoView = ref('')
|
|
92
124
|
const floatIconUrl = ref('')
|
|
93
125
|
const keyboardHeight = ref(0)
|
|
126
|
+
const renderConfig = ref(null)
|
|
127
|
+
const formInputs = ref({})
|
|
94
128
|
|
|
95
129
|
const canSend = computed(() => {
|
|
96
130
|
return !!(inputText.value && inputText.value.trim()) || !!pendingImageUrl.value
|
|
@@ -114,6 +148,21 @@
|
|
|
114
148
|
let closeHandler = null
|
|
115
149
|
let keyboardHandler = null
|
|
116
150
|
|
|
151
|
+
function initFormInputs() {
|
|
152
|
+
var cfg = renderConfig.value
|
|
153
|
+
var values = {}
|
|
154
|
+
if (cfg && cfg.fields && cfg.fields.length) {
|
|
155
|
+
cfg.fields.forEach(function (field) {
|
|
156
|
+
if (field.type === 'select' && field.options && field.options.length) {
|
|
157
|
+
values[field.name] = field.defaultValue !== undefined ? field.defaultValue : field.options[0].value
|
|
158
|
+
} else {
|
|
159
|
+
values[field.name] = field.defaultValue !== undefined ? field.defaultValue : ''
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
formInputs.value = values
|
|
164
|
+
}
|
|
165
|
+
|
|
117
166
|
function syncState(snapshot) {
|
|
118
167
|
messages.value = snapshot.messages || []
|
|
119
168
|
loading.value = snapshot.loading || false
|
|
@@ -122,7 +171,9 @@
|
|
|
122
171
|
pendingIconCategory.value = snapshot.pendingIconCategory || null
|
|
123
172
|
pendingImageUrl.value = snapshot.pendingImageUrl || ''
|
|
124
173
|
inputText.value = snapshot.inputText || ''
|
|
174
|
+
renderConfig.value = snapshot.renderConfig || null
|
|
125
175
|
quickQuestions.value = snapshot.quickQuestions || []
|
|
176
|
+
initFormInputs()
|
|
126
177
|
scrollToBottom()
|
|
127
178
|
}
|
|
128
179
|
|
|
@@ -162,6 +213,13 @@
|
|
|
162
213
|
chatService.closeConversation()
|
|
163
214
|
}
|
|
164
215
|
|
|
216
|
+
function onOverlayClick(e) {
|
|
217
|
+
if (e && e.target && e.currentTarget && e.target !== e.currentTarget) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
closeChat()
|
|
221
|
+
}
|
|
222
|
+
|
|
165
223
|
function onInput(e) {
|
|
166
224
|
inputText.value = e.detail.value
|
|
167
225
|
chatService.setInputText(e.detail.value)
|
|
@@ -201,6 +259,75 @@
|
|
|
201
259
|
chatService.cancelIconPick()
|
|
202
260
|
}
|
|
203
261
|
|
|
262
|
+
function getFieldOptions(field) {
|
|
263
|
+
var options = field.options || []
|
|
264
|
+
return {
|
|
265
|
+
labels: options.map(function (opt) {
|
|
266
|
+
return opt.label || opt.value
|
|
267
|
+
}),
|
|
268
|
+
values: options.map(function (opt) {
|
|
269
|
+
return opt.value
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getFieldIndex(field) {
|
|
275
|
+
var opts = getFieldOptions(field)
|
|
276
|
+
var index = opts.values.indexOf(formInputs.value[field.name])
|
|
277
|
+
return index >= 0 ? index : 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getFieldSelectedLabel(field) {
|
|
281
|
+
var index = getFieldIndex(field)
|
|
282
|
+
var opts = getFieldOptions(field)
|
|
283
|
+
var option = field.options ? field.options[index] : null
|
|
284
|
+
return option ? (option.label || option.value) : (opts.labels[index] || '请选择')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function onFormInput(e, name) {
|
|
288
|
+
formInputs.value[name] = e.detail.value
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function onFormSelect(e, field) {
|
|
292
|
+
var index = e.detail.value
|
|
293
|
+
var opts = getFieldOptions(field)
|
|
294
|
+
formInputs.value[field.name] = opts.values[index]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function onFormImageChoose(name) {
|
|
298
|
+
chatService.chooseImage()
|
|
299
|
+
.then(function (imageUrl) {
|
|
300
|
+
formInputs.value[name] = imageUrl
|
|
301
|
+
})
|
|
302
|
+
.catch(function (error) {
|
|
303
|
+
uni.showToast({
|
|
304
|
+
title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
|
|
305
|
+
icon: 'none'
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function onFormImageRemove(name) {
|
|
311
|
+
formInputs.value[name] = ''
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function submitForm() {
|
|
315
|
+
chatService.submitInputs(formInputs.value)
|
|
316
|
+
.then(function () {
|
|
317
|
+
scrollToBottom()
|
|
318
|
+
})
|
|
319
|
+
.catch(function (error) {
|
|
320
|
+
uni.showToast({
|
|
321
|
+
title: error && (error.message || error.msg) ? (error.message || error.msg) : '提交失败',
|
|
322
|
+
icon: 'none'
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cancelForm() {
|
|
328
|
+
chatService.cancelForm()
|
|
329
|
+
}
|
|
330
|
+
|
|
204
331
|
function clearPendingImage() {
|
|
205
332
|
chatService.clearPendingImage()
|
|
206
333
|
}
|
|
@@ -547,4 +674,123 @@
|
|
|
547
674
|
.ly-ai-send-btn.disabled {
|
|
548
675
|
opacity: 0.5;
|
|
549
676
|
}
|
|
677
|
+
|
|
678
|
+
.ly-ai-form-bar {
|
|
679
|
+
padding: 24rpx;
|
|
680
|
+
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
|
681
|
+
background: #fff;
|
|
682
|
+
border-top: 1rpx solid #eee;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.ly-ai-form-title {
|
|
686
|
+
font-size: 30rpx;
|
|
687
|
+
font-weight: 600;
|
|
688
|
+
color: #333;
|
|
689
|
+
margin-bottom: 12rpx;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.ly-ai-form-desc {
|
|
693
|
+
font-size: 26rpx;
|
|
694
|
+
color: #666;
|
|
695
|
+
line-height: 1.5;
|
|
696
|
+
margin-bottom: 24rpx;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.ly-ai-form-item {
|
|
700
|
+
margin-bottom: 24rpx;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.ly-ai-form-label {
|
|
704
|
+
display: block;
|
|
705
|
+
font-size: 28rpx;
|
|
706
|
+
color: #333;
|
|
707
|
+
margin-bottom: 12rpx;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.ly-ai-form-input {
|
|
711
|
+
height: 72rpx;
|
|
712
|
+
background: #f5f6f8;
|
|
713
|
+
border-radius: 12rpx;
|
|
714
|
+
padding: 0 24rpx;
|
|
715
|
+
font-size: 28rpx;
|
|
716
|
+
color: #333;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.ly-ai-form-picker {
|
|
720
|
+
height: 72rpx;
|
|
721
|
+
background: #f5f6f8;
|
|
722
|
+
border-radius: 12rpx;
|
|
723
|
+
padding: 0 24rpx;
|
|
724
|
+
display: flex;
|
|
725
|
+
align-items: center;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.ly-ai-form-picker-text {
|
|
729
|
+
font-size: 28rpx;
|
|
730
|
+
color: #333;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.ly-ai-form-image-wrap {
|
|
734
|
+
display: flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.ly-ai-form-image {
|
|
739
|
+
width: 160rpx;
|
|
740
|
+
height: 160rpx;
|
|
741
|
+
border-radius: 12rpx;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.ly-ai-form-image-remove {
|
|
745
|
+
margin-left: 16rpx;
|
|
746
|
+
width: 48rpx;
|
|
747
|
+
height: 48rpx;
|
|
748
|
+
line-height: 44rpx;
|
|
749
|
+
text-align: center;
|
|
750
|
+
font-size: 36rpx;
|
|
751
|
+
color: #999;
|
|
752
|
+
background: #f5f6f8;
|
|
753
|
+
border-radius: 50%;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.ly-ai-form-image-btn {
|
|
757
|
+
width: 200rpx;
|
|
758
|
+
height: 160rpx;
|
|
759
|
+
line-height: 160rpx;
|
|
760
|
+
text-align: center;
|
|
761
|
+
background: #f5f6f8;
|
|
762
|
+
color: #666;
|
|
763
|
+
border-radius: 12rpx;
|
|
764
|
+
font-size: 28rpx;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.ly-ai-form-actions {
|
|
768
|
+
display: flex;
|
|
769
|
+
gap: 16rpx;
|
|
770
|
+
margin-top: 12rpx;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.ly-ai-form-btn {
|
|
774
|
+
flex: 1;
|
|
775
|
+
height: 80rpx;
|
|
776
|
+
line-height: 80rpx;
|
|
777
|
+
text-align: center;
|
|
778
|
+
border-radius: 12rpx;
|
|
779
|
+
font-size: 28rpx;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.ly-ai-form-btn.confirm {
|
|
783
|
+
background: #f84616;
|
|
784
|
+
color: #fff;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.ly-ai-form-btn.cancel {
|
|
788
|
+
background: #f5f6f8;
|
|
789
|
+
color: #666;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.ly-ai-form-btn.disabled,
|
|
793
|
+
.ly-ai-form-image-btn.disabled {
|
|
794
|
+
opacity: 0.5;
|
|
795
|
+
}
|
|
550
796
|
</style>
|
package/index.js
CHANGED
package/package.json
CHANGED
package/src/adapters/request.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
var configModule = require('../config');
|
|
2
2
|
|
|
3
|
-
function getFullUrl(path) {
|
|
3
|
+
function getFullUrl(path, useUploadBase) {
|
|
4
4
|
var config = configModule.getConfig();
|
|
5
|
-
var baseUrl = config.
|
|
5
|
+
var baseUrl = (useUploadBase && config.uploadBaseUrl) ? config.uploadBaseUrl : config.baseUrl;
|
|
6
|
+
baseUrl = baseUrl || '';
|
|
6
7
|
var normalizedPath = (path || '').replace(/^\/+/, '/');
|
|
7
8
|
if (baseUrl.slice(-1) === '/' && normalizedPath.indexOf('/') === 0) {
|
|
8
9
|
return baseUrl + normalizedPath.slice(1);
|
|
@@ -20,6 +21,9 @@ function getHeaders(extraHeaders) {
|
|
|
20
21
|
if (token) {
|
|
21
22
|
headers.Authorization = token;
|
|
22
23
|
}
|
|
24
|
+
if (config.appId) {
|
|
25
|
+
headers['X-App-Id'] = config.appId;
|
|
26
|
+
}
|
|
23
27
|
if (extraHeaders) {
|
|
24
28
|
Object.assign(headers, extraHeaders);
|
|
25
29
|
}
|
|
@@ -32,16 +36,20 @@ function sendChat(payload) {
|
|
|
32
36
|
var headers = getHeaders({
|
|
33
37
|
'Content-Type': 'application/json'
|
|
34
38
|
});
|
|
39
|
+
var data = Object.assign({}, payload || {});
|
|
40
|
+
if (!data.inputs) {
|
|
41
|
+
delete data.inputs;
|
|
42
|
+
}
|
|
35
43
|
|
|
36
44
|
if (config.debug) {
|
|
37
|
-
console.log('[mini-dynamic-renderer] sendChat', url,
|
|
45
|
+
console.log('[mini-dynamic-renderer] sendChat', url, data);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
return new Promise(function (resolve, reject) {
|
|
41
49
|
uni.request({
|
|
42
50
|
url: url,
|
|
43
51
|
method: 'POST',
|
|
44
|
-
data:
|
|
52
|
+
data: data,
|
|
45
53
|
header: headers,
|
|
46
54
|
timeout: 120000,
|
|
47
55
|
success: function (res) {
|
|
@@ -61,7 +69,7 @@ function sendChat(payload) {
|
|
|
61
69
|
|
|
62
70
|
function uploadImage(filePath) {
|
|
63
71
|
var config = configModule.getConfig();
|
|
64
|
-
var url = getFullUrl(config.uploadApiPath);
|
|
72
|
+
var url = getFullUrl(config.uploadApiPath, true);
|
|
65
73
|
var headers = getHeaders();
|
|
66
74
|
|
|
67
75
|
if (config.debug) {
|
package/src/chat/parser.js
CHANGED
|
@@ -48,8 +48,36 @@ function parseNeedIcon(answer) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function parseRenderConfig(answer) {
|
|
52
|
+
if (!answer || typeof answer !== 'string') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
var renderIdx = answer.indexOf('[RENDER_CONFIG]');
|
|
56
|
+
if (renderIdx !== -1) {
|
|
57
|
+
var jsonPart = answer.substring(renderIdx + '[RENDER_CONFIG]'.length);
|
|
58
|
+
var start = jsonPart.indexOf('{');
|
|
59
|
+
var end = jsonPart.lastIndexOf('}');
|
|
60
|
+
if (start === -1 || end === -1 || end < start) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(jsonPart.substring(start, end + 1));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (answer.indexOf('[NEED_ICON]') !== -1) {
|
|
70
|
+
return {
|
|
71
|
+
type: 'iconPicker',
|
|
72
|
+
fields: [{ name: 'imageUrl', label: '分类图标', type: 'image' }]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
module.exports = {
|
|
52
79
|
cleanImageUrl: cleanImageUrl,
|
|
53
80
|
parseAssistantMessage: parseAssistantMessage,
|
|
54
|
-
parseNeedIcon: parseNeedIcon
|
|
81
|
+
parseNeedIcon: parseNeedIcon,
|
|
82
|
+
parseRenderConfig: parseRenderConfig
|
|
55
83
|
};
|
package/src/chat/service.js
CHANGED
|
@@ -11,7 +11,10 @@ var state = {
|
|
|
11
11
|
iconPickMode: false,
|
|
12
12
|
pendingIconCategory: null,
|
|
13
13
|
pendingImageUrl: '',
|
|
14
|
-
inputText: ''
|
|
14
|
+
inputText: '',
|
|
15
|
+
renderConfig: null,
|
|
16
|
+
pendingInputs: {},
|
|
17
|
+
lastUserQuery: ''
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
var quickQuestions = [
|
|
@@ -29,6 +32,9 @@ function reset() {
|
|
|
29
32
|
state.pendingIconCategory = null;
|
|
30
33
|
state.pendingImageUrl = '';
|
|
31
34
|
state.inputText = '';
|
|
35
|
+
state.renderConfig = null;
|
|
36
|
+
state.pendingInputs = {};
|
|
37
|
+
state.lastUserQuery = '';
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
function ensureLogin() {
|
|
@@ -65,6 +71,8 @@ function getSnapshot() {
|
|
|
65
71
|
pendingIconCategory: state.pendingIconCategory,
|
|
66
72
|
pendingImageUrl: state.pendingImageUrl,
|
|
67
73
|
inputText: state.inputText,
|
|
74
|
+
renderConfig: state.renderConfig,
|
|
75
|
+
pendingInputs: Object.assign({}, state.pendingInputs),
|
|
68
76
|
quickQuestions: quickQuestions.slice()
|
|
69
77
|
};
|
|
70
78
|
}
|
|
@@ -86,6 +94,7 @@ function handleAssistantAnswer(answer) {
|
|
|
86
94
|
sort: needIcon.sort
|
|
87
95
|
};
|
|
88
96
|
state.iconPickMode = true;
|
|
97
|
+
state.renderConfig = null;
|
|
89
98
|
state.messages.push({
|
|
90
99
|
role: 'assistant',
|
|
91
100
|
content: needIcon.msg,
|
|
@@ -94,8 +103,19 @@ function handleAssistantAnswer(answer) {
|
|
|
94
103
|
notifyUpdate();
|
|
95
104
|
return;
|
|
96
105
|
}
|
|
106
|
+
var renderConfig = parser.parseRenderConfig(answer);
|
|
107
|
+
if (renderConfig) {
|
|
108
|
+
state.iconPickMode = false;
|
|
109
|
+
state.pendingIconCategory = null;
|
|
110
|
+
state.renderConfig = renderConfig;
|
|
111
|
+
var cleanedAnswer = answer.replace(/\[RENDER_CONFIG\]\{[\s\S]*\}/, '').replace(/\[NEED_ICON\]\{[\s\S]*\}/, '').trim();
|
|
112
|
+
state.messages.push(createAssistantMessage(cleanedAnswer || '请填写以下信息'));
|
|
113
|
+
notifyUpdate();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
97
116
|
state.iconPickMode = false;
|
|
98
117
|
state.pendingIconCategory = null;
|
|
118
|
+
state.renderConfig = null;
|
|
99
119
|
state.messages.push(createAssistantMessage(answer));
|
|
100
120
|
notifyUpdate();
|
|
101
121
|
}
|
|
@@ -206,24 +226,13 @@ function sendQuick(text) {
|
|
|
206
226
|
return handleSend();
|
|
207
227
|
}
|
|
208
228
|
|
|
209
|
-
function
|
|
210
|
-
if (!ensureLogin()) {
|
|
211
|
-
return Promise.resolve();
|
|
212
|
-
}
|
|
213
|
-
state.messages.push({
|
|
214
|
-
role: 'user',
|
|
215
|
-
content: displayContent || query,
|
|
216
|
-
imageUrls: imageUrl ? [imageUrl] : []
|
|
217
|
-
});
|
|
229
|
+
function doSendChat(payload) {
|
|
218
230
|
state.loading = true;
|
|
219
231
|
notifyUpdate();
|
|
220
232
|
|
|
221
|
-
return requestAdapter.sendChat(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
imageUrl: imageUrl || undefined
|
|
225
|
-
})
|
|
226
|
-
.then(function (data) {
|
|
233
|
+
return requestAdapter.sendChat(payload)
|
|
234
|
+
.then(function (res) {
|
|
235
|
+
var data = res && res.data ? res.data : res;
|
|
227
236
|
if (data.conversationId) {
|
|
228
237
|
state.conversationId = data.conversationId;
|
|
229
238
|
}
|
|
@@ -237,10 +246,63 @@ function sendMessage(query, imageUrl, displayContent) {
|
|
|
237
246
|
});
|
|
238
247
|
}
|
|
239
248
|
|
|
249
|
+
function sendMessage(query, imageUrl, displayContent) {
|
|
250
|
+
if (!ensureLogin()) {
|
|
251
|
+
return Promise.resolve();
|
|
252
|
+
}
|
|
253
|
+
state.lastUserQuery = displayContent || query || '';
|
|
254
|
+
state.messages.push({
|
|
255
|
+
role: 'user',
|
|
256
|
+
content: displayContent || query,
|
|
257
|
+
imageUrls: imageUrl ? [imageUrl] : []
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return doSendChat({
|
|
261
|
+
conversationId: state.conversationId || undefined,
|
|
262
|
+
query: query || undefined,
|
|
263
|
+
imageUrl: imageUrl || undefined,
|
|
264
|
+
inputs: state.pendingInputs || {}
|
|
265
|
+
}).then(function () {
|
|
266
|
+
state.pendingInputs = {};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function submitInputs(inputs) {
|
|
271
|
+
inputs = inputs || {};
|
|
272
|
+
var hasOwn = Object.prototype.hasOwnProperty;
|
|
273
|
+
for (var key in inputs) {
|
|
274
|
+
if (hasOwn.call(inputs, key)) {
|
|
275
|
+
state.pendingInputs[key] = inputs[key];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
state.renderConfig = null;
|
|
279
|
+
state.pendingImageUrl = '';
|
|
280
|
+
notifyUpdate();
|
|
281
|
+
|
|
282
|
+
if (!ensureLogin()) {
|
|
283
|
+
return Promise.resolve();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return doSendChat({
|
|
287
|
+
conversationId: state.conversationId || undefined,
|
|
288
|
+
query: state.lastUserQuery || undefined,
|
|
289
|
+
inputs: state.pendingInputs
|
|
290
|
+
}).then(function () {
|
|
291
|
+
state.pendingInputs = {};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function cancelForm() {
|
|
296
|
+
state.renderConfig = null;
|
|
297
|
+
state.pendingInputs = {};
|
|
298
|
+
state.pendingImageUrl = '';
|
|
299
|
+
notifyUpdate();
|
|
300
|
+
}
|
|
301
|
+
|
|
240
302
|
function handleSend() {
|
|
241
303
|
var query = (state.inputText || '').trim();
|
|
242
304
|
var imageUrl = state.pendingImageUrl;
|
|
243
|
-
if ((!query && !imageUrl) || state.loading || state.uploading || state.iconPickMode) {
|
|
305
|
+
if ((!query && !imageUrl) || state.loading || state.uploading || state.iconPickMode || state.renderConfig) {
|
|
244
306
|
return Promise.resolve();
|
|
245
307
|
}
|
|
246
308
|
var displayContent = query || (imageUrl ? '[图片]' : '');
|
|
@@ -271,6 +333,8 @@ module.exports = {
|
|
|
271
333
|
chooseImage: chooseImage,
|
|
272
334
|
pickCategoryIcon: pickCategoryIcon,
|
|
273
335
|
cancelIconPick: cancelIconPick,
|
|
336
|
+
submitInputs: submitInputs,
|
|
337
|
+
cancelForm: cancelForm,
|
|
274
338
|
sendQuick: sendQuick,
|
|
275
339
|
handleSend: handleSend,
|
|
276
340
|
openConversation: openConversation,
|