@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @syfei49/mini-dynamic-renderer
2
2
 
3
- uni-app 浮层 AI 客服插件(v1.0.4)。在指定页面右下角显示 AI 浮层图标,点击后从底部弹出抽屉聊天面板,支持文字、图片、拍照/相册上传及 `[NEED_ICON]` 分类图标引导。
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://your-api.com/prod-api',
24
- chatApiPath: '/mini/ai/chat',
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
- > 微信小程序 `App.vue` template 不会被渲染,所以浮层组件需要放在具体页面里。
69
+ ### 3. openChat 打开抽屉
53
70
 
54
- ### 2. 在任意页面打开抽屉(可选)
71
+ 把旧入口从 `uni.navigateTo('/pages/user/aiChat')` 改为直接打开抽屉:
55
72
 
56
73
  ```javascript
57
- import { openChat } from '@syfei49/mini-dynamic-renderer'
58
-
59
- openChat()
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.4**:uni-app 浮层 AI 插件、抽屉聊天、迁移 aiChat 全能力、文档拆分
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.self="closeChat">
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="iconPickMode" class="ly-ai-icon-pick-bar">
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
@@ -5,7 +5,7 @@ var chatService = require('./src/chat/service');
5
5
  var configModule = require('./src/config');
6
6
 
7
7
  module.exports = {
8
- version: '1.0.3',
8
+ version: '1.0.9',
9
9
  init: initModule.init,
10
10
  getConfig: configModule.getConfig,
11
11
  isInitialized: initModule.isInitialized,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syfei49/mini-dynamic-renderer",
3
- "version": "1.0.4",
3
+ "version": "1.0.9",
4
4
  "description": "uni-app 浮层 AI 客服插件",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -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.baseUrl || '';
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, payload);
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: payload,
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) {
@@ -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
  };
@@ -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 sendMessage(query, imageUrl, displayContent) {
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
- conversationId: state.conversationId || undefined,
223
- query: query || undefined,
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,
package/src/config.js CHANGED
@@ -1,5 +1,7 @@
1
1
  var DEFAULT_CONFIG = {
2
2
  baseUrl: '',
3
+ uploadBaseUrl: '',
4
+ appId: '',
3
5
  chatApiPath: '/mini/ai/chat',
4
6
  uploadApiPath: '/mini/common/upload',
5
7
  getToken: function () {