@hlw-zbzs/ai 1.0.1

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/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /// <reference path="./env.d.ts" />
2
+ /**
3
+ * @file @hlw-zbzs/ai 统一入口 (遵循单单词极简原则)
4
+ */
5
+
6
+ export type * from './types'
7
+ export * from './src/engines'
8
+ export * from './src/store'
9
+ export * from './src/core'
10
+ export * from './src/components'
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@hlw-zbzs/ai",
3
+ "version": "1.0.1",
4
+ "description": "HLW Universal Multi-Platform AI SDK & LLM Client (OpenAI, DeepSeek, Claude, Qwen, Zhipu, Moonshot, Doubao, Ollama)",
5
+ "main": "index.ts",
6
+ "types": "index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.ts",
10
+ "import": "./index.ts",
11
+ "require": "./index.ts",
12
+ "default": "./index.ts"
13
+ },
14
+ "./*": {
15
+ "types": "./src/*.ts",
16
+ "import": "./src/*.ts",
17
+ "require": "./src/*.ts",
18
+ "default": "./src/*.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "index.ts",
23
+ "src/"
24
+ ],
25
+ "dependencies": {
26
+ "@hlw-zbzs/core": "^1.0.25",
27
+ "@hlw-zbzs/vue": "^1.0.5",
28
+ "@vicons/ionicons5": "^0.13.0",
29
+ "axios": "^1.10.0",
30
+ "naive-ui": "^2.44.1",
31
+ "openai": "^7.8.0",
32
+ "pinia": "^3.0.3",
33
+ "pinia-plugin-persistedstate": "^4.3.0",
34
+ "vue": "^3.5.13"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.14.1",
38
+ "typescript": "^5.8.3"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "license": "MIT"
44
+ }
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <div class="hlw-ai">
3
+ <!-- 顶部状态栏 -->
4
+ <ai-header />
5
+
6
+ <!-- 主体:50% / 50% 左右双栏工作区 -->
7
+ <div class="ai-body">
8
+ <!-- 左栏:模型服务商选择 + 参数配置与测试 -->
9
+ <div class="left-pane">
10
+ <div class="pane-content">
11
+ <!-- 服务商网格选择 -->
12
+ <ai-grid @change="onGridChange" />
13
+
14
+ <!-- 凭证、模型、生成参数及测试栏 -->
15
+ <ai-params ref="paramsRef" />
16
+ </div>
17
+ </div>
18
+
19
+ <!-- 右栏:对话测试演练沙箱 -->
20
+ <div class="right-pane">
21
+ <ai-chat />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { ref } from 'vue'
29
+ import AiHeader from './header.vue'
30
+ import AiGrid from './grid.vue'
31
+ import AiParams from './params.vue'
32
+ import AiChat from './chat.vue'
33
+
34
+ const paramsRef = ref<InstanceType<typeof AiParams> | null>(null)
35
+
36
+ function onGridChange() {
37
+ paramsRef.value?.clearTest?.()
38
+ }
39
+ </script>
40
+
41
+ <style scoped lang="scss">
42
+ .hlw-ai {
43
+ display: flex;
44
+ flex-direction: column;
45
+ width: 100%;
46
+ height: 100%;
47
+ background-color: #ffffff;
48
+ overflow: hidden;
49
+ font-weight: 400;
50
+ }
51
+
52
+ .ai-body {
53
+ flex: 1;
54
+ display: flex;
55
+ min-height: 0;
56
+ overflow: hidden;
57
+ background: #ffffff;
58
+ }
59
+
60
+ .left-pane {
61
+ flex: 1;
62
+ min-width: 0;
63
+ border-right: 1px solid #e2e8f0;
64
+ display: flex;
65
+ flex-direction: column;
66
+ background: #ffffff;
67
+
68
+ .pane-content {
69
+ flex: 1;
70
+ overflow-y: auto;
71
+ padding: 0;
72
+ display: flex;
73
+ flex-direction: column;
74
+ }
75
+ }
76
+
77
+ .right-pane {
78
+ flex: 1;
79
+ min-width: 0;
80
+ display: flex;
81
+ flex-direction: column;
82
+ background: #ffffff;
83
+ }
84
+ </style>
@@ -0,0 +1,474 @@
1
+ <template>
2
+ <div class="chat-wrapper">
3
+ <!-- 顶部预设话术栏 -->
4
+ <div class="prompt-bar">
5
+ <span class="prompt-label">常用话术:</span>
6
+ <span class="prompt-chip" @click="setPrompt('帮我写一段幽默风趣的直播间求点赞话术')">
7
+ 求点赞
8
+ </span>
9
+ <span class="prompt-chip" @click="setPrompt('观众送出了嘉年华,写一段热情霸气的感谢词')">
10
+ 感谢大哥
11
+ </span>
12
+ <span class="prompt-chip" @click="setPrompt('有观众在弹幕质疑商品价格偏高,给出高情商回复话术')">
13
+ 高情商解围
14
+ </span>
15
+ <span class="prompt-chip" @click="setPrompt('直播间准备开始抽奖,帮我写一段炒热气氛的话术')">
16
+ 抽奖倒计时
17
+ </span>
18
+ </div>
19
+
20
+ <!-- 对话气泡滚动区域 -->
21
+ <div class="chat-area">
22
+ <div v-if="dialogue.length === 0" class="chat-empty">
23
+ <n-icon :size="40" class="empty-icon"><HardwareChipOutline /></n-icon>
24
+ <span class="empty-title">AI 对话演练沙箱</span>
25
+ <span class="empty-desc">选择服务商后,在下方输入话术需求或点击上方预设,实时体验大模型流式输出与思考推理</span>
26
+ </div>
27
+
28
+ <div v-else class="chat-list">
29
+ <div
30
+ v-for="(msg, index) in dialogue"
31
+ :key="index"
32
+ class="chat-row"
33
+ :class="msg.role"
34
+ >
35
+ <div class="role-tag" :class="msg.role">
36
+ {{ msg.role === 'user' ? '主播' : 'AI' }}
37
+ </div>
38
+
39
+ <div class="msg-bubble">
40
+ <!-- 深度思考过程折叠块 (DeepSeek R1 等推理模型) -->
41
+ <div v-if="msg.think" class="think-box">
42
+ <div class="think-header">
43
+ <n-icon :size="12"><CodeSlashOutline /></n-icon>
44
+ <span>思考过程</span>
45
+ </div>
46
+ <div class="think-content">{{ msg.think }}</div>
47
+ </div>
48
+
49
+ <!-- 消息正文与复制操作 -->
50
+ <div class="msg-text">
51
+ <div class="text-content">
52
+ <span v-if="msg.text">{{ msg.text }}</span>
53
+ <span v-else-if="loading && index === dialogue.length - 1" class="loading-hint">
54
+ 正在思考与生成回复...
55
+ </span>
56
+ </div>
57
+
58
+ <div v-if="msg.text" class="bubble-actions">
59
+ <button
60
+ class="copy-btn"
61
+ title="复制话术内容"
62
+ @click.stop="onCopy(msg.text)"
63
+ >
64
+ <n-icon :size="12"><CopyOutline /></n-icon>
65
+ <span>复制话术</span>
66
+ </button>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- 底部输入操作栏 -->
75
+ <div class="chat-input-bar">
76
+ <n-input
77
+ v-model:value="input"
78
+ type="textarea"
79
+ placeholder="输入提问内容 (快捷键 Enter 发送,Shift + Enter 换行)..."
80
+ :autosize="{ minRows: 2.5, maxRows: 5 }"
81
+ :disabled="loading"
82
+ size="medium"
83
+ @keydown="onKeyDown"
84
+ />
85
+
86
+ <div class="input-bottom-row">
87
+ <span class="shortcut-tip">Enter 发送 / Shift + Enter 换行</span>
88
+
89
+ <div class="action-buttons">
90
+ <hlw-button
91
+ size="medium"
92
+ secondary
93
+ :icon="TrashOutline"
94
+ :disabled="dialogue.length === 0 || loading"
95
+ @click="onClear"
96
+ >
97
+ 清空记录
98
+ </hlw-button>
99
+
100
+ <hlw-button
101
+ v-if="loading"
102
+ size="medium"
103
+ type="warning"
104
+ secondary
105
+ :icon="StopCircleOutline"
106
+ @click="onAbort"
107
+ >
108
+ 停止生成
109
+ </hlw-button>
110
+
111
+ <hlw-button
112
+ v-else
113
+ size="medium"
114
+ type="primary"
115
+ :icon="PaperPlaneOutline"
116
+ :disabled="!input.trim()"
117
+ @click="onSend"
118
+ >
119
+ 发送测试
120
+ </hlw-button>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </template>
126
+
127
+ <script setup lang="ts">
128
+ import { ref } from 'vue'
129
+ import {
130
+ TrashOutline,
131
+ PaperPlaneOutline,
132
+ StopCircleOutline,
133
+ HardwareChipOutline,
134
+ CodeSlashOutline,
135
+ CopyOutline
136
+ } from '@vicons/ionicons5'
137
+ import { NInput, NIcon } from 'naive-ui'
138
+ import { toast, HlwButton } from '@hlw-zbzs/vue'
139
+ import { useAi } from '../core'
140
+ import type { AiMsg } from '../types'
141
+
142
+ const { loading, stream, abort } = useAi()
143
+
144
+ const input = ref('')
145
+ const dialogue = ref<AiMsg[]>([])
146
+
147
+ function setPrompt(prompt: string) {
148
+ input.value = prompt
149
+ }
150
+
151
+ async function onCopy(content: string) {
152
+ if (!content) return
153
+ try {
154
+ if (navigator?.clipboard?.writeText) {
155
+ await navigator.clipboard.writeText(content)
156
+ } else {
157
+ const textarea = document.createElement('textarea')
158
+ textarea.value = content
159
+ textarea.style.position = 'fixed'
160
+ textarea.style.opacity = '0'
161
+ document.body.appendChild(textarea)
162
+ textarea.select()
163
+ document.execCommand('copy')
164
+ document.body.removeChild(textarea)
165
+ }
166
+ toast('已复制话术到剪贴板', 'success')
167
+ } catch (err: any) {
168
+ toast('复制失败,请手动复制', 'error')
169
+ }
170
+ }
171
+
172
+ function onKeyDown(event: KeyboardEvent) {
173
+ if (event.key === 'Enter' && !event.shiftKey) {
174
+ event.preventDefault()
175
+ if (input.value.trim() && !loading.value) {
176
+ onSend()
177
+ }
178
+ }
179
+ }
180
+
181
+ async function onSend() {
182
+ const userText = input.value.trim()
183
+ if (!userText || loading.value) return
184
+
185
+ input.value = ''
186
+ dialogue.value.push({ role: 'user', text: userText })
187
+
188
+ const aiMsg: AiMsg = { role: 'assistant', text: '', think: '' }
189
+ dialogue.value.push(aiMsg)
190
+
191
+ try {
192
+ await stream(
193
+ dialogue.value.slice(0, -1),
194
+ {
195
+ onToken: (delta) => {
196
+ aiMsg.text += delta
197
+ },
198
+ onThink: (delta) => {
199
+ if (!aiMsg.think) aiMsg.think = ''
200
+ aiMsg.think += delta
201
+ },
202
+ onError: (err) => {
203
+ toast(`生成错误: ${err.message}`, 'error')
204
+ }
205
+ }
206
+ )
207
+ } catch (err: any) {
208
+ aiMsg.text = `[请求失败]: ${err.message || '网络异常'}`
209
+ }
210
+ }
211
+
212
+ function onAbort() {
213
+ abort()
214
+ toast('已中止当前生成', 'info')
215
+ }
216
+
217
+ function onClear() {
218
+ dialogue.value = []
219
+ }
220
+ </script>
221
+
222
+ <style scoped lang="scss">
223
+ .chat-wrapper {
224
+ width: 100%;
225
+ height: 100%;
226
+ display: flex;
227
+ flex-direction: column;
228
+ background: #ffffff;
229
+
230
+ .prompt-bar {
231
+ height: 40px;
232
+ padding: 0 16px;
233
+ border-bottom: 1px solid #f1f5f9;
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 6px;
237
+ overflow-x: auto;
238
+ flex-shrink: 0;
239
+
240
+ .prompt-label {
241
+ font-size: 13px;
242
+ font-weight: 400;
243
+ color: #64748b;
244
+ white-space: nowrap;
245
+ }
246
+
247
+ .prompt-chip {
248
+ font-size: 12px;
249
+ font-weight: 400;
250
+ color: #2563eb;
251
+ background: #eff6ff;
252
+ border: 1px solid #dbeafe;
253
+ padding: 3px 10px;
254
+ border-radius: 4px;
255
+ cursor: pointer;
256
+ white-space: nowrap;
257
+ user-select: none;
258
+ transition: all 0.15s ease;
259
+
260
+ &:hover {
261
+ background: #dbeafe;
262
+ }
263
+ }
264
+ }
265
+
266
+ .chat-area {
267
+ flex: 1;
268
+ min-height: 0;
269
+ overflow-y: auto;
270
+ padding: 16px;
271
+
272
+ .chat-empty {
273
+ height: 100%;
274
+ display: flex;
275
+ flex-direction: column;
276
+ align-items: center;
277
+ justify-content: center;
278
+ gap: 8px;
279
+ color: #94a3b8;
280
+ text-align: center;
281
+
282
+ .empty-icon {
283
+ color: #cbd5e1;
284
+ }
285
+
286
+ .empty-title {
287
+ font-size: 15px;
288
+ font-weight: 400;
289
+ color: #475569;
290
+ }
291
+
292
+ .empty-desc {
293
+ font-size: 13px;
294
+ font-weight: 400;
295
+ color: #94a3b8;
296
+ max-width: 340px;
297
+ line-height: 1.5;
298
+ }
299
+ }
300
+
301
+ .chat-list {
302
+ display: flex;
303
+ flex-direction: column;
304
+ gap: 14px;
305
+
306
+ .chat-row {
307
+ display: flex;
308
+ gap: 10px;
309
+ align-items: flex-start;
310
+
311
+ .role-tag {
312
+ width: 32px;
313
+ height: 32px;
314
+ border-radius: 6px;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ font-size: 12px;
319
+ font-weight: 400;
320
+ flex-shrink: 0;
321
+
322
+ &.user {
323
+ background: #2563eb;
324
+ color: #ffffff;
325
+ }
326
+
327
+ &.assistant {
328
+ background: #f1f5f9;
329
+ color: #334155;
330
+ border: 1px solid #e2e8f0;
331
+ }
332
+ }
333
+
334
+ .msg-bubble {
335
+ max-width: 85%;
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 6px;
339
+
340
+ .think-box {
341
+ background: #f8fafc;
342
+ border-left: 3px solid #64748b;
343
+ padding: 8px 12px;
344
+ font-size: 12px;
345
+ font-weight: 400;
346
+ color: #64748b;
347
+ border-radius: 0 4px 4px 0;
348
+ font-family: monospace;
349
+
350
+ .think-header {
351
+ display: flex;
352
+ align-items: center;
353
+ gap: 4px;
354
+ font-weight: 400;
355
+ color: #475569;
356
+ margin-bottom: 3px;
357
+ font-size: 12px;
358
+ }
359
+
360
+ .think-content {
361
+ white-space: pre-wrap;
362
+ line-height: 1.5;
363
+ }
364
+ }
365
+
366
+ .msg-text {
367
+ background: #f8fafc;
368
+ border: 1px solid #f1f5f9;
369
+ color: #0f172a;
370
+ padding: 10px 14px;
371
+ border-radius: 8px;
372
+ font-size: 13px;
373
+ font-weight: 400;
374
+ line-height: 1.6;
375
+ white-space: pre-wrap;
376
+ word-break: break-word;
377
+
378
+ .bubble-actions {
379
+ display: flex;
380
+ justify-content: flex-end;
381
+ margin-top: 6px;
382
+ border-top: 1px dashed #e2e8f0;
383
+ padding-top: 5px;
384
+
385
+ .copy-btn {
386
+ display: inline-flex;
387
+ align-items: center;
388
+ gap: 3px;
389
+ background: #ffffff;
390
+ border: 1px solid #cbd5e1;
391
+ border-radius: 4px;
392
+ padding: 2px 8px;
393
+ font-size: 12px;
394
+ font-weight: 400;
395
+ color: #475569;
396
+ cursor: pointer;
397
+ user-select: none;
398
+ transition: all 0.15s ease;
399
+
400
+ &:hover {
401
+ color: #2563eb;
402
+ border-color: #93c5fd;
403
+ background: #eff6ff;
404
+ }
405
+ }
406
+ }
407
+
408
+ .loading-hint {
409
+ color: #94a3b8;
410
+ font-style: italic;
411
+ }
412
+ }
413
+ }
414
+
415
+ &.user {
416
+ flex-direction: row-reverse;
417
+
418
+ .msg-bubble {
419
+ align-items: flex-end;
420
+
421
+ .msg-text {
422
+ background: #2563eb;
423
+ border: none;
424
+ color: #ffffff;
425
+
426
+ .bubble-actions {
427
+ border-top: 1px dashed rgba(255, 255, 255, 0.3);
428
+
429
+ .copy-btn {
430
+ background: rgba(255, 255, 255, 0.15);
431
+ border-color: rgba(255, 255, 255, 0.4);
432
+ color: #ffffff;
433
+
434
+ &:hover {
435
+ background: rgba(255, 255, 255, 0.25);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+
446
+ .chat-input-bar {
447
+ padding: 12px 16px;
448
+ background: #ffffff;
449
+ border-top: 1px solid #e2e8f0;
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: 8px;
453
+ flex-shrink: 0;
454
+
455
+ .input-bottom-row {
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: space-between;
459
+
460
+ .shortcut-tip {
461
+ font-size: 12px;
462
+ font-weight: 400;
463
+ color: #94a3b8;
464
+ }
465
+
466
+ .action-buttons {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 8px;
470
+ }
471
+ }
472
+ }
473
+ }
474
+ </style>