ai-helper-core 1.0.0
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/dist/ai-helper-core.js +1663 -0
- package/dist/ai-helper-core.umd.cjs +56 -0
- package/dist/api/client.d.ts +23 -0
- package/dist/config.d.ts +11 -0
- package/dist/index.d.ts +6 -0
- package/dist/markdown/renderer.d.ts +1 -0
- package/dist/state/manager.d.ts +21 -0
- package/dist/theme/detector.d.ts +2 -0
- package/dist/types.d.ts +115 -0
- package/package.json +32 -0
- package/src/api/client.ts +221 -0
- package/src/config.ts +75 -0
- package/src/index.ts +21 -0
- package/src/markdown/renderer.ts +5 -0
- package/src/state/manager.ts +115 -0
- package/src/theme/detector.ts +25 -0
- package/src/types.ts +136 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { AiHelperConfig, AiHelperOptions } from './types'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ICON = `data:image/svg+xml,<svg viewBox=%220 0 1051 1024%22 xmlns=%22http://www.w3.org/2000/svg%22><path d=%22M451.99 1005.12a369.77 369.77 0 0 1-369.81-369.81 369.81 369.81 0 0 1 369.81-369.81h147.93a369.81 369.81 0 0 1 369.81 369.81 369.77 369.77 0 0 1-369.81 369.81zM208.96 709.25a197.25 197.25 0 0 0 197.25 197.25h239.48a197.25 197.25 0 0 0 197.25-197.25 197.22 197.22 0 0 0-197.25-197.22h-239.48a197.22 197.22 0 0 0-197.25 197.18z m475.86 98.65a31.99 31.99 0 0 1-31.99-31.99v-133.28a31.99 31.99 0 0 1 31.99-31.99h6.46a31.99 31.99 0 0 1 31.99 31.99v133.28a31.99 31.99 0 0 1-31.99 31.99z m-324.07 0a31.99 31.99 0 0 1-31.99-31.99v-133.28a31.99 31.99 0 0 1 31.99-31.99h6.5a31.99 31.99 0 0 1 31.99 31.99v133.28a31.99 31.99 0 0 1-31.99 31.99z m91.92-624.2a20.73 20.73 0 0 1-13.37-26.17 88 88 0 0 1 87.39-49.59 91.85 91.85 0 0 1 85.09 43.7 20.77 20.77 0 0 1-10.12 27.57 20.81 20.81 0 0 1-27.65-10.16 53.48 53.48 0 0 0-47.32-19.56 51.32 51.32 0 0 0-47.85 20.77 20.77 20.77 0 0 1-19.79 14.43 20.54 20.54 0 0 1-6.42-0.98z m-69.41-78.78a20.77 20.77 0 0 1-2.34-29.27 194.08 194.08 0 0 1 145.74-56.65 193.52 193.52 0 0 1 144.12 54.91 20.81 20.81 0 0 1-1.78 29.38 20.85 20.85 0 0 1-29.38-1.78 154.39 154.39 0 0 0-112.96-40.94 154.5 154.5 0 0 0-114.09 42.11 20.58 20.58 0 0 1-15.82 7.29 20.88 20.88 0 0 1-13.52-5.02z%22 fill=%22%23fff%22 p-id=%223539%22/><path d=%22M0.04 643.54a115.04 115.04 0 0 0 115.04 115.04v-230.11A115.04 115.04 0 0 0 0.04 643.54z%22 fill=%22%23fff%22 opacity=%22.74%22 p-id=%223540%22/><path d=%22M936.87 528.5v230.11a115.04 115.04 0 0 0 115.04-115.04 115.04 115.04 0 0 0-115.04-115.07z%22 fill=%22%23fff%22 opacity=%22.74%22 p-id=%223541%22/></svg>`
|
|
4
|
+
|
|
5
|
+
const defaultConfig: AiHelperConfig = {
|
|
6
|
+
apiBase: '/api/ai-chat',
|
|
7
|
+
branding: {
|
|
8
|
+
name: '坤土智脑',
|
|
9
|
+
icon: DEFAULT_ICON,
|
|
10
|
+
welcomeTitle: '你好,我是坤土智脑',
|
|
11
|
+
welcomeDesc: '我可以帮你查询数据、分析趋势、生成图表'
|
|
12
|
+
},
|
|
13
|
+
theme: 'auto',
|
|
14
|
+
showQuickQuestions: true,
|
|
15
|
+
endpoints: {
|
|
16
|
+
chat: '/chat',
|
|
17
|
+
history: '/history',
|
|
18
|
+
prompts: '/prompts'
|
|
19
|
+
},
|
|
20
|
+
closeOnRouteChange: true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class AiHelperConfigManager {
|
|
24
|
+
private config: AiHelperConfig
|
|
25
|
+
|
|
26
|
+
constructor(options?: AiHelperOptions) {
|
|
27
|
+
this.config = this.buildConfig(options)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(options: AiHelperOptions): void {
|
|
31
|
+
this.config = this.buildConfig(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
merge(options: AiHelperOptions): void {
|
|
35
|
+
this.config = this.buildConfig(options)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get(): AiHelperConfig {
|
|
39
|
+
return { ...this.config }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getApiUrl(path: string): string {
|
|
43
|
+
const base = this.config.apiBase.replace(/\/$/, '')
|
|
44
|
+
const p = path.startsWith('/') ? path : `/${path}`
|
|
45
|
+
return `${base}${p}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private buildConfig(options?: AiHelperOptions): AiHelperConfig {
|
|
49
|
+
if (!options) return { ...defaultConfig }
|
|
50
|
+
return {
|
|
51
|
+
apiBase: options.apiBase ?? defaultConfig.apiBase,
|
|
52
|
+
branding: {
|
|
53
|
+
name: options.branding?.name ?? defaultConfig.branding.name,
|
|
54
|
+
icon: options.branding?.icon ?? defaultConfig.branding.icon,
|
|
55
|
+
welcomeTitle: options.branding?.welcomeTitle ?? defaultConfig.branding.welcomeTitle,
|
|
56
|
+
welcomeDesc: options.branding?.welcomeDesc ?? defaultConfig.branding.welcomeDesc
|
|
57
|
+
},
|
|
58
|
+
theme: options.theme ?? defaultConfig.theme,
|
|
59
|
+
getHeaders: options.getHeaders,
|
|
60
|
+
showQuickQuestions: options.showQuickQuestions ?? defaultConfig.showQuickQuestions,
|
|
61
|
+
endpoints: {
|
|
62
|
+
chat: options.endpoints?.chat ?? defaultConfig.endpoints.chat,
|
|
63
|
+
history: options.endpoints?.history ?? defaultConfig.endpoints.history,
|
|
64
|
+
prompts: options.endpoints?.prompts ?? defaultConfig.endpoints.prompts
|
|
65
|
+
},
|
|
66
|
+
closeOnRouteChange: options.closeOnRouteChange ?? defaultConfig.closeOnRouteChange,
|
|
67
|
+
buildChatBody: options.buildChatBody,
|
|
68
|
+
parseChatChunk: options.parseChatChunk,
|
|
69
|
+
parseHistoryResponse: options.parseHistoryResponse,
|
|
70
|
+
parsePromptResponse: options.parsePromptResponse
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const configManager = new AiHelperConfigManager()
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { AiHelperConfigManager, configManager } from './config'
|
|
2
|
+
export { AiHelperApiClient } from './api/client'
|
|
3
|
+
export { AiHelperStateManager } from './state/manager'
|
|
4
|
+
export { detectTheme, onThemeChange } from './theme/detector'
|
|
5
|
+
export { renderMarkdown } from './markdown/renderer'
|
|
6
|
+
export type {
|
|
7
|
+
AiHelperOptions,
|
|
8
|
+
AiHelperConfig,
|
|
9
|
+
AiHelperBranding,
|
|
10
|
+
AiHelperEndpoints,
|
|
11
|
+
ChatParseResult,
|
|
12
|
+
ChatRequestParams,
|
|
13
|
+
Message,
|
|
14
|
+
AiHelperState,
|
|
15
|
+
PromptRow,
|
|
16
|
+
PromptListResponse,
|
|
17
|
+
PromptQueryParams,
|
|
18
|
+
HistoryMessage,
|
|
19
|
+
HistoryResponse,
|
|
20
|
+
HistoryParams
|
|
21
|
+
} from './types'
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AiHelperState, Message } from '../types'
|
|
2
|
+
|
|
3
|
+
type StateListener = (state: Readonly<AiHelperState>) => void
|
|
4
|
+
|
|
5
|
+
export class AiHelperStateManager {
|
|
6
|
+
private state: AiHelperState = {
|
|
7
|
+
messages: [],
|
|
8
|
+
isVisible: false,
|
|
9
|
+
isLoading: false,
|
|
10
|
+
isInitial: true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private listeners = new Map<string, Set<StateListener>>()
|
|
14
|
+
|
|
15
|
+
on(event: 'stateChange' | 'messageChange', listener: StateListener): () => void {
|
|
16
|
+
if (!this.listeners.has(event)) {
|
|
17
|
+
this.listeners.set(event, new Set())
|
|
18
|
+
}
|
|
19
|
+
this.listeners.get(event)!.add(listener)
|
|
20
|
+
return () => this.off(event, listener)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
off(event: string, listener: StateListener): void {
|
|
24
|
+
this.listeners.get(event)?.delete(listener)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private emit(event: string): void {
|
|
28
|
+
const listeners = this.listeners.get(event)
|
|
29
|
+
if (listeners) {
|
|
30
|
+
for (const listener of listeners) {
|
|
31
|
+
listener(this.state)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Also emit stateChange for all state mutations
|
|
35
|
+
if (event !== 'stateChange') {
|
|
36
|
+
const stateListeners = this.listeners.get('stateChange')
|
|
37
|
+
if (stateListeners) {
|
|
38
|
+
for (const listener of stateListeners) {
|
|
39
|
+
listener(this.state)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getState(): Readonly<AiHelperState> {
|
|
46
|
+
return this.state
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
show(): void {
|
|
50
|
+
this.state.isVisible = true
|
|
51
|
+
this.emit('stateChange')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
hide(): void {
|
|
55
|
+
this.state.isVisible = false
|
|
56
|
+
this.emit('stateChange')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toggleVisibility(): void {
|
|
60
|
+
this.state.isVisible = !this.state.isVisible
|
|
61
|
+
this.emit('stateChange')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
addUserMessage(content: string): Message {
|
|
65
|
+
const msg: Message = { id: Date.now(), role: 'user', content }
|
|
66
|
+
this.state.messages = [...this.state.messages, msg]
|
|
67
|
+
this.state.isInitial = false
|
|
68
|
+
this.emit('messageChange')
|
|
69
|
+
return msg
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
startAssistantMessage(): Message {
|
|
73
|
+
const msg: Message = { id: Date.now() + 1, role: 'assistant', content: '', isStreaming: true }
|
|
74
|
+
this.state.messages = [...this.state.messages, msg]
|
|
75
|
+
this.state.isLoading = true
|
|
76
|
+
this.emit('messageChange')
|
|
77
|
+
return msg
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
appendToAssistantMessage(chunk: string): void {
|
|
81
|
+
const lastMsg = this.state.messages[this.state.messages.length - 1]
|
|
82
|
+
if (lastMsg && lastMsg.role === 'assistant') {
|
|
83
|
+
const newContent = lastMsg.content + chunk
|
|
84
|
+
const updated: Message = { ...lastMsg, content: newContent }
|
|
85
|
+
this.state.messages = [...this.state.messages.slice(0, -1), updated]
|
|
86
|
+
this.emit('messageChange')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
finishAssistantMessage(): void {
|
|
91
|
+
const lastMsg = this.state.messages[this.state.messages.length - 1]
|
|
92
|
+
if (lastMsg && lastMsg.role === 'assistant') {
|
|
93
|
+
lastMsg.isStreaming = false
|
|
94
|
+
}
|
|
95
|
+
this.state.isLoading = false
|
|
96
|
+
this.emit('messageChange')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setMessages(messages: Message[]): void {
|
|
100
|
+
this.state.messages = messages
|
|
101
|
+
this.state.isInitial = messages.length === 0
|
|
102
|
+
this.emit('messageChange')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
clearMessages(): void {
|
|
106
|
+
this.state.messages = []
|
|
107
|
+
this.state.isInitial = true
|
|
108
|
+
this.emit('messageChange')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setLoading(loading: boolean): void {
|
|
112
|
+
this.state.isLoading = loading
|
|
113
|
+
this.emit('stateChange')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function detectTheme(): 'light' | 'dark' {
|
|
2
|
+
if (typeof document === 'undefined') return 'light'
|
|
3
|
+
return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function onThemeChange(callback: (theme: 'light' | 'dark') => void): () => void {
|
|
7
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
|
8
|
+
return () => {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const observer = new MutationObserver(() => {
|
|
12
|
+
callback(detectTheme())
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
16
|
+
|
|
17
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
18
|
+
const mediaHandler = () => callback(detectTheme())
|
|
19
|
+
mediaQuery.addEventListener('change', mediaHandler)
|
|
20
|
+
|
|
21
|
+
return () => {
|
|
22
|
+
observer.disconnect()
|
|
23
|
+
mediaQuery.removeEventListener('change', mediaHandler)
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// 品牌配置
|
|
2
|
+
export interface AiHelperBranding {
|
|
3
|
+
name?: string
|
|
4
|
+
icon?: string
|
|
5
|
+
welcomeTitle?: string
|
|
6
|
+
welcomeDesc?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// API 端点配置
|
|
10
|
+
export interface AiHelperEndpoints {
|
|
11
|
+
chat?: string
|
|
12
|
+
history?: string
|
|
13
|
+
prompts?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 流式聊天响应解析结果
|
|
18
|
+
* content: 从当前 chunk 中提取的文本(追加到 assistant 消息)
|
|
19
|
+
* done: 是否已结束(设为 true 时终止流,不再调用 onChunk)
|
|
20
|
+
*/
|
|
21
|
+
export interface ChatParseResult {
|
|
22
|
+
content: string
|
|
23
|
+
done?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 聊天请求构建参数
|
|
28
|
+
*/
|
|
29
|
+
export interface ChatRequestParams {
|
|
30
|
+
message: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 初始化选项
|
|
34
|
+
export interface AiHelperOptions {
|
|
35
|
+
apiBase?: string
|
|
36
|
+
branding?: AiHelperBranding
|
|
37
|
+
theme?: 'light' | 'dark' | 'auto'
|
|
38
|
+
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>
|
|
39
|
+
showQuickQuestions?: boolean
|
|
40
|
+
endpoints?: AiHelperEndpoints
|
|
41
|
+
closeOnRouteChange?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* 自定义聊天请求体构建函数
|
|
44
|
+
* 返回将作为 POST body 发送的字符串
|
|
45
|
+
* 默认: JSON.stringify({ message })
|
|
46
|
+
*/
|
|
47
|
+
buildChatBody?: (params: ChatRequestParams) => string
|
|
48
|
+
/**
|
|
49
|
+
* 自定义流式响应解析函数
|
|
50
|
+
* 每个 SSE chunk(或 NDJSON 行)调用一次
|
|
51
|
+
* 返回 { content, done }
|
|
52
|
+
* 默认: 解析为 { content: string } 格式
|
|
53
|
+
*/
|
|
54
|
+
parseChatChunk?: (rawChunk: string) => ChatParseResult
|
|
55
|
+
/**
|
|
56
|
+
* 自定义历史响应解析函数
|
|
57
|
+
* 接收 fetch().json() 的原始结果,返回 HistoryResponse
|
|
58
|
+
* 默认: 期望原始结果为 { messages: [{role, content}], total }
|
|
59
|
+
*/
|
|
60
|
+
parseHistoryResponse?: (raw: unknown) => HistoryResponse
|
|
61
|
+
/**
|
|
62
|
+
* 自定义提示词响应解析函数
|
|
63
|
+
* 接收 fetch().json() 的原始结果,返回 PromptListResponse
|
|
64
|
+
* 默认: 期望原始结果为 { items: [{id, name, content}], total }
|
|
65
|
+
*/
|
|
66
|
+
parsePromptResponse?: (raw: unknown) => PromptListResponse
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 解析后的完整配置
|
|
70
|
+
export interface AiHelperConfig {
|
|
71
|
+
apiBase: string
|
|
72
|
+
branding: Required<AiHelperBranding>
|
|
73
|
+
theme: 'light' | 'dark' | 'auto'
|
|
74
|
+
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>
|
|
75
|
+
showQuickQuestions: boolean
|
|
76
|
+
endpoints: Required<AiHelperEndpoints>
|
|
77
|
+
closeOnRouteChange: boolean
|
|
78
|
+
buildChatBody?: (params: ChatRequestParams) => string
|
|
79
|
+
parseChatChunk?: (rawChunk: string) => ChatParseResult
|
|
80
|
+
parseHistoryResponse?: (raw: unknown) => HistoryResponse
|
|
81
|
+
parsePromptResponse?: (raw: unknown) => PromptListResponse
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 聊天消息
|
|
85
|
+
export interface Message {
|
|
86
|
+
id: number
|
|
87
|
+
role: 'user' | 'assistant'
|
|
88
|
+
content: string
|
|
89
|
+
isStreaming?: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 快捷问题
|
|
93
|
+
export interface PromptRow {
|
|
94
|
+
id: number
|
|
95
|
+
name: string
|
|
96
|
+
content: string
|
|
97
|
+
description?: string
|
|
98
|
+
created_at: string
|
|
99
|
+
updated_at: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface PromptListResponse {
|
|
103
|
+
items: PromptRow[]
|
|
104
|
+
total: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PromptQueryParams {
|
|
108
|
+
keyword?: string
|
|
109
|
+
limit?: number
|
|
110
|
+
offset?: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 历史记录
|
|
114
|
+
export interface HistoryMessage {
|
|
115
|
+
role: 'user' | 'assistant'
|
|
116
|
+
content: string
|
|
117
|
+
timestamp: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface HistoryResponse {
|
|
121
|
+
messages: HistoryMessage[]
|
|
122
|
+
total: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface HistoryParams {
|
|
126
|
+
limit?: number
|
|
127
|
+
offset?: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 内部状态
|
|
131
|
+
export interface AiHelperState {
|
|
132
|
+
messages: Message[]
|
|
133
|
+
isVisible: boolean
|
|
134
|
+
isLoading: boolean
|
|
135
|
+
isInitial: boolean
|
|
136
|
+
}
|