@hlw-zbzs/ai 1.0.1 → 1.0.3

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.
@@ -1,98 +0,0 @@
1
- <template>
2
- <div class="ai-header">
3
- <div class="header-title">
4
- <n-icon :size="18" class="title-icon"><HardwareChipOutline /></n-icon>
5
- <span class="title-text">大模型配置与测试</span>
6
- </div>
7
-
8
- <div class="header-right">
9
- <n-tag
10
- :type="ready ? 'success' : 'warning'"
11
- size="small"
12
- round
13
- :bordered="false"
14
- class="status-tag"
15
- >
16
- <template #icon>
17
- <n-icon>
18
- <CheckmarkCircleOutline v-if="ready" />
19
- <AlertCircleOutline v-else />
20
- </n-icon>
21
- </template>
22
- {{ ready ? `${info.name} 已就绪` : `${info.name} 未配 Key` }}
23
- </n-tag>
24
-
25
- <div class="switch-wrap">
26
- <span class="switch-label">启用 AI:</span>
27
- <n-switch :value="enabled" size="small" @update:value="onToggle" />
28
- </div>
29
- </div>
30
- </div>
31
- </template>
32
-
33
- <script setup lang="ts">
34
- import { HardwareChipOutline, CheckmarkCircleOutline, AlertCircleOutline } from '@vicons/ionicons5'
35
- import { NIcon, NTag, NSwitch } from 'naive-ui'
36
- import { toast } from '@hlw-zbzs/vue'
37
- import { useAi } from '../core'
38
-
39
- const { info, ready, enabled } = useAi()
40
-
41
- function onToggle(val: boolean) {
42
- enabled.value = val
43
- toast(`AI 功能已${val ? '开启' : '停用'}`, val ? 'success' : 'info')
44
- }
45
- </script>
46
-
47
- <style scoped lang="scss">
48
- .ai-header {
49
- height: 48px;
50
- background: #ffffff;
51
- border-bottom: 1px solid #e2e8f0;
52
- display: flex;
53
- align-items: center;
54
- justify-content: space-between;
55
- padding: 0 16px;
56
- flex-shrink: 0;
57
-
58
- .header-title {
59
- display: flex;
60
- align-items: center;
61
- gap: 8px;
62
-
63
- .title-icon {
64
- color: #2563eb;
65
- }
66
-
67
- .title-text {
68
- font-size: 15px;
69
- font-weight: 400;
70
- color: #0f172a;
71
- letter-spacing: 0.5px;
72
- }
73
- }
74
-
75
- .header-right {
76
- display: flex;
77
- align-items: center;
78
- gap: 14px;
79
-
80
- .status-tag {
81
- user-select: none;
82
- font-weight: 400;
83
- }
84
-
85
- .switch-wrap {
86
- display: flex;
87
- align-items: center;
88
- gap: 6px;
89
-
90
- .switch-label {
91
- font-size: 13px;
92
- font-weight: 400;
93
- color: #475569;
94
- }
95
- }
96
- }
97
- }
98
- </style>
@@ -1,15 +0,0 @@
1
- import HlwAi from './ai.vue'
2
- import AiHeader from './header.vue'
3
- import AiGrid from './grid.vue'
4
- import AiParams from './params.vue'
5
- import AiChat from './chat.vue'
6
-
7
- export {
8
- HlwAi,
9
- AiHeader,
10
- AiGrid,
11
- AiParams,
12
- AiChat
13
- }
14
-
15
- export default HlwAi
@@ -1,388 +0,0 @@
1
- <template>
2
- <div class="params-container">
3
- <div class="params-fields">
4
- <!-- 凭证与连接参数 -->
5
- <div class="field-section">
6
- <div class="section-header">
7
- <span class="section-title">API 凭证与模型</span>
8
- </div>
9
-
10
- <div class="form-row">
11
- <div class="form-field flex-2">
12
- <label class="field-label">
13
- <span class="label-title">
14
- <span>API Key</span>
15
- <span v-if="!info.free" class="req-star">*</span>
16
- <span v-else class="free-text">(本地模型无需填写)</span>
17
- </span>
18
- <a v-if="info.url" class="portal-link" title="在浏览器中打开开发者平台获取 Key" @click.stop="openPortal(info.url)">
19
- <span>获取 {{ info.name }} 密钥</span>
20
- <n-icon :size="12"><OpenOutline /></n-icon>
21
- </a>
22
- </label>
23
- <n-input
24
- :value="conf.key"
25
- type="password"
26
- show-password-on="click"
27
- :placeholder="info.free ? '免密模型' : '请输入 API Key (sk-...)'"
28
- size="medium"
29
- clearable
30
- @update:value="onKeyChange"
31
- />
32
- </div>
33
-
34
- <div class="form-field flex-1">
35
- <label class="field-label">模型 (Model)</label>
36
- <n-select
37
- :value="conf.model"
38
- :options="modelOptions"
39
- tag
40
- filterable
41
- size="medium"
42
- placeholder="选择或输入模型"
43
- @update:value="onModelChange"
44
- />
45
- </div>
46
- </div>
47
-
48
- <div class="form-field">
49
- <label class="field-label">接口地址 (Base URL)</label>
50
- <n-input
51
- :value="conf.url"
52
- type="text"
53
- placeholder="https://api.openai.com/v1"
54
- size="medium"
55
- clearable
56
- @update:value="onUrlChange"
57
- />
58
- </div>
59
- </div>
60
-
61
- <!-- 超参数调节 -->
62
- <div class="field-section">
63
- <div class="section-header">
64
- <span class="section-title">生成参数</span>
65
- </div>
66
-
67
- <div class="form-row">
68
- <div class="form-field flex-1">
69
- <label class="field-label">随机温度 (Temperature): {{ conf.temp ?? 0.7 }}</label>
70
- <div class="slider-box">
71
- <n-slider
72
- :value="conf.temp ?? 0.7"
73
- :min="0"
74
- :max="2"
75
- :step="0.05"
76
- style="flex: 1;"
77
- @update:value="onTempChange"
78
- />
79
- <n-input-number
80
- :value="conf.temp ?? 0.7"
81
- :min="0"
82
- :max="2"
83
- :step="0.05"
84
- size="small"
85
- style="width: 76px;"
86
- @update:value="onTempChange"
87
- />
88
- </div>
89
- </div>
90
-
91
- <div class="form-field flex-1">
92
- <label class="field-label">最大 Token 数</label>
93
- <n-input-number
94
- :value="conf.tokens ?? 2048"
95
- :min="128"
96
- :max="16384"
97
- :step="256"
98
- size="medium"
99
- style="width: 100%;"
100
- @update:value="onTokensChange"
101
- />
102
- </div>
103
- </div>
104
-
105
- <div class="form-field">
106
- <label class="field-label">系统人设 (System Prompt)</label>
107
- <n-input
108
- :value="conf.sys"
109
- type="textarea"
110
- placeholder="定义 AI 的角色定位与回答风格..."
111
- :autosize="{ minRows: 2.5, maxRows: 4.5 }"
112
- size="medium"
113
- @update:value="onSysChange"
114
- />
115
- </div>
116
- </div>
117
- </div>
118
-
119
- <!-- 左侧底部操作栏 -->
120
- <div class="params-footer">
121
- <div class="footer-actions">
122
- <hlw-button
123
- size="medium"
124
- type="primary"
125
- secondary
126
- :loading="testing"
127
- :icon="PulseOutline"
128
- @click="onTest"
129
- >
130
- 测试连接
131
- </hlw-button>
132
-
133
- <hlw-button
134
- size="medium"
135
- secondary
136
- :icon="RefreshOutline"
137
- @click="onReset"
138
- >
139
- 恢复默认
140
- </hlw-button>
141
- </div>
142
-
143
- <n-tag
144
- v-if="testResult"
145
- :type="testResult.success ? 'success' : 'error'"
146
- size="small"
147
- round
148
- :bordered="false"
149
- >
150
- <template #icon>
151
- <n-icon>
152
- <CheckmarkCircleOutline v-if="testResult.success" />
153
- <CloseCircleOutline v-else />
154
- </n-icon>
155
- </template>
156
- {{ testResult.success ? `已连通 (${testResult.latency}ms)` : '连接失败' }}
157
- </n-tag>
158
- </div>
159
- </div>
160
- </template>
161
-
162
- <script setup lang="ts">
163
- import { ref, computed } from 'vue'
164
- import {
165
- PulseOutline,
166
- RefreshOutline,
167
- CheckmarkCircleOutline,
168
- CloseCircleOutline,
169
- OpenOutline
170
- } from '@vicons/ionicons5'
171
- import { NInput, NSelect, NSlider, NInputNumber, NTag, NIcon } from 'naive-ui'
172
- import { tools } from '@hlw-zbzs/core'
173
- import { toast, HlwButton } from '@hlw-zbzs/vue'
174
- import { useAi } from '../core'
175
- import type { AiTest } from '../types'
176
-
177
- const {
178
- engine,
179
- conf,
180
- info,
181
- save,
182
- reset,
183
- test
184
- } = useAi()
185
-
186
- function openPortal(url?: string) {
187
- if (!url) return
188
- if (tools?.openExternal) {
189
- tools.openExternal(url)
190
- } else {
191
- window.open(url, '_blank')
192
- }
193
- }
194
-
195
- const testing = ref(false)
196
- const testResult = ref<AiTest | null>(null)
197
-
198
- const modelOptions = computed(() => {
199
- const models = info.value.models || []
200
- return models.map((item) => ({
201
- label: item,
202
- value: item
203
- }))
204
- })
205
-
206
- function onKeyChange(val: string) {
207
- save(engine.value, { key: val.trim() })
208
- }
209
-
210
- function onUrlChange(val: string) {
211
- save(engine.value, { url: val.trim() })
212
- }
213
-
214
- function onModelChange(val: string) {
215
- save(engine.value, { model: val.trim() })
216
- }
217
-
218
- function onTempChange(val: number | null) {
219
- if (val !== null && val !== undefined) {
220
- save(engine.value, { temp: val })
221
- }
222
- }
223
-
224
- function onTokensChange(val: number | null) {
225
- if (val) save(engine.value, { tokens: val })
226
- }
227
-
228
- function onSysChange(val: string) {
229
- save(engine.value, { sys: val })
230
- }
231
-
232
- async function onTest() {
233
- testing.value = true
234
- testResult.value = null
235
- try {
236
- const res = await test()
237
- testResult.value = res
238
- if (res.success) {
239
- toast(`连接成功,耗时 ${res.latency}ms`, 'success')
240
- } else {
241
- toast(`连接失败: ${res.msg}`, 'error')
242
- }
243
- } catch (err: any) {
244
- testResult.value = {
245
- success: false,
246
- msg: err?.message || '连接异常',
247
- latency: 0
248
- }
249
- toast(`连接异常: ${err?.message || '未知错误'}`, 'error')
250
- } finally {
251
- testing.value = false
252
- }
253
- }
254
-
255
- function onReset() {
256
- reset(engine.value)
257
- testResult.value = null
258
- toast(`已恢复【${info.value.name}】为默认配置`, 'info')
259
- }
260
-
261
- defineExpose({
262
- clearTest: () => {
263
- testResult.value = null
264
- }
265
- })
266
- </script>
267
-
268
- <style scoped lang="scss">
269
- .params-container {
270
- display: flex;
271
- flex-direction: column;
272
- gap: 16px;
273
- padding: 16px 16px 0 16px;
274
-
275
- .params-fields {
276
- display: flex;
277
- flex-direction: column;
278
- gap: 16px;
279
- }
280
-
281
- .field-section {
282
- display: flex;
283
- flex-direction: column;
284
- gap: 10px;
285
-
286
- .section-header {
287
- display: flex;
288
- align-items: center;
289
- justify-content: space-between;
290
- border-bottom: 1px solid #f1f5f9;
291
- padding-bottom: 6px;
292
-
293
- .section-title {
294
- font-size: 14px;
295
- font-weight: 400;
296
- color: #1e293b;
297
- }
298
- }
299
-
300
- .form-row {
301
- display: flex;
302
- gap: 12px;
303
-
304
- .flex-1 {
305
- flex: 1;
306
- min-width: 0;
307
- }
308
-
309
- .flex-2 {
310
- flex: 2;
311
- min-width: 0;
312
- }
313
- }
314
-
315
- .form-field {
316
- display: flex;
317
- flex-direction: column;
318
- gap: 6px;
319
-
320
- .field-label {
321
- font-size: 13px;
322
- font-weight: 400;
323
- color: #475569;
324
- display: flex;
325
- align-items: center;
326
- justify-content: space-between;
327
-
328
- .label-title {
329
- display: flex;
330
- align-items: center;
331
- gap: 4px;
332
- }
333
-
334
- .req-star {
335
- color: #ef4444;
336
- }
337
-
338
- .free-text {
339
- color: #059669;
340
- font-size: 12px;
341
- }
342
-
343
- .portal-link {
344
- display: inline-flex;
345
- align-items: center;
346
- gap: 3px;
347
- font-size: 12px;
348
- font-weight: 400;
349
- color: #2563eb;
350
- cursor: pointer;
351
- text-decoration: none;
352
- transition: all 0.15s ease;
353
-
354
- &:hover {
355
- color: #1d4ed8;
356
- text-decoration: underline;
357
- }
358
- }
359
- }
360
-
361
- .slider-box {
362
- display: flex;
363
- align-items: center;
364
- gap: 10px;
365
- }
366
- }
367
- }
368
-
369
- .params-footer {
370
- height: 54px;
371
- margin-top: auto;
372
- border-top: 1px solid #e2e8f0;
373
- display: flex;
374
- align-items: center;
375
- justify-content: space-between;
376
- padding: 0 16px;
377
- margin-left: -16px;
378
- margin-right: -16px;
379
- background: #ffffff;
380
-
381
- .footer-actions {
382
- display: flex;
383
- align-items: center;
384
- gap: 8px;
385
- }
386
- }
387
- }
388
- </style>
package/src/core/index.ts DELETED
@@ -1,169 +0,0 @@
1
- /**
2
- * @file AI 核心响应式 Hook (单单词极简命名,零冗余代码)
3
- */
4
-
5
- import { computed, ref } from 'vue'
6
- import { storeToRefs } from 'pinia'
7
- import { useAiStore } from '../store'
8
- import { Engine } from '../engines/base'
9
- import type {
10
- AiConfig,
11
- AiEngine,
12
- AiHandler,
13
- AiInfo,
14
- AiMsg,
15
- AiOption,
16
- AiResult,
17
- AiTest
18
- } from '../types'
19
-
20
- const engines = new Map<AiEngine, Engine>()
21
-
22
- function getEngine(type: AiEngine, conf: AiConfig): Engine {
23
- let instance = engines.get(type)
24
- if (!instance) {
25
- instance = new Engine(type, conf)
26
- engines.set(type, instance)
27
- } else {
28
- instance.save(conf)
29
- }
30
- return instance
31
- }
32
-
33
- export function useAi() {
34
- const store = useAiStore()
35
- const { engine, enabled } = storeToRefs(store)
36
-
37
- const loading = ref(false)
38
- const text = ref('')
39
- const think = ref('')
40
- let controller: AbortController | null = null
41
-
42
- const current = computed<Engine>(() => getEngine(store.engine, store.conf))
43
- const info = computed<AiInfo>(() => store.info)
44
- const conf = computed<AiConfig>(() => store.conf)
45
- const ready = computed<boolean>(() => store.ready)
46
- const list = computed<AiInfo[]>(() => store.list)
47
-
48
- function select(type: AiEngine) {
49
- store.set(type)
50
- }
51
-
52
- function abort() {
53
- if (controller) {
54
- controller.abort()
55
- controller = null
56
- }
57
- loading.value = false
58
- }
59
-
60
- async function chat(
61
- input: string | AiMsg[],
62
- option?: Partial<AiOption>,
63
- type?: AiEngine
64
- ): Promise<string> {
65
- if (!enabled.value) throw new Error('AI 功能已停用')
66
- loading.value = true
67
- text.value = ''
68
- think.value = ''
69
- controller = new AbortController()
70
-
71
- const target = type ? getEngine(type, store.configs[type] || store.conf) : current.value
72
-
73
- try {
74
- const list: AiMsg[] =
75
- typeof input === 'string' ? [{ role: 'user', text: input }] : input
76
-
77
- const res: AiResult = await target.chat({
78
- list,
79
- signal: controller.signal,
80
- ...option
81
- })
82
-
83
- text.value = res.text
84
- think.value = res.think || ''
85
- return res.text
86
- } finally {
87
- loading.value = false
88
- controller = null
89
- }
90
- }
91
-
92
- async function stream(
93
- input: string | AiMsg[],
94
- handler: AiHandler = {},
95
- type?: AiEngine
96
- ): Promise<string> {
97
- if (!enabled.value) throw new Error('AI 功能已停用')
98
- loading.value = true
99
- text.value = ''
100
- think.value = ''
101
- controller = new AbortController()
102
-
103
- const target = type ? getEngine(type, store.configs[type] || store.conf) : current.value
104
-
105
- try {
106
- const list: AiMsg[] =
107
- typeof input === 'string' ? [{ role: 'user', text: input }] : input
108
-
109
- const res: AiResult = await target.stream(
110
- {
111
- list,
112
- signal: controller.signal
113
- },
114
- {
115
- onToken: (delta) => {
116
- text.value += delta
117
- handler.onToken?.(delta)
118
- },
119
- onThink: (delta) => {
120
- think.value += delta
121
- handler.onThink?.(delta)
122
- },
123
- onDone: (fullText, fullThink) => {
124
- text.value = fullText
125
- think.value = fullThink || ''
126
- handler.onDone?.(fullText, fullThink)
127
- },
128
- onError: (err) => {
129
- handler.onError?.(err)
130
- }
131
- }
132
- )
133
-
134
- return res.text
135
- } finally {
136
- loading.value = false
137
- controller = null
138
- }
139
- }
140
-
141
- async function test(type?: AiEngine, tempConf?: Partial<AiConfig>): Promise<AiTest> {
142
- const targetType = type || store.engine
143
- const targetConf = store.configs[targetType] || store.conf
144
- const target = getEngine(targetType, targetConf)
145
- return target.test(tempConf)
146
- }
147
-
148
- return {
149
- store,
150
- engine,
151
- enabled,
152
- conf,
153
- info,
154
- list,
155
- ready,
156
- loading: computed(() => loading.value),
157
- text: computed(() => text.value),
158
- think: computed(() => think.value),
159
- select,
160
- chat,
161
- stream,
162
- test,
163
- abort,
164
- save: store.save.bind(store),
165
- reset: store.reset.bind(store)
166
- }
167
- }
168
-
169
- export default useAi