No description
- Vue 71%
- JavaScript 27.4%
- Shell 1.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| dist | ||
| docs | ||
| scripts | ||
| src | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| vite.config.js | ||
@souta/agent-chat-ui
SouTan Vue3 智能体对话 UI:管理端全屏对话与大屏语音壳打在同一个包里。
Phase-1(现行):UI 只负责展示与交互;会话 / 发消息走宿主注入的 transport,由业务 BFF 代理 AMC(sessions CRUD + SSE messages)。浏览器不直连 Hermes TUI WebSocket。包内不写死 JWT / baseURL,也不嵌入 axios,不会发明默认智能体。
对齐后端客户端命名:com.souta / amcc-agent-chat-client → 前端包名 @souta/agent-chat-ui。
仓库:http://gitea.frp.bwker.com/soutan/agent-chat-ui
宿主工程升级提示词(交给编码 Agent):docs/host-upgrade-prompt.md
Install
git+https(立即可用)
pnpm add git+http://gitea.frp.bwker.com/soutan/agent-chat-ui.git
# 或指定 tag / commit
pnpm add git+http://gitea.frp.bwker.com/soutan/agent-chat-ui.git#v0.2.0
Forgejo npm(若已配置私服)
# .npmrc
@souta:registry=https://forgejo.bingjun360.com/api/packages/SouTan/npm/
pnpm add @souta/agent-chat-ui
Peer:vue@^3.5、naive-ui@^2;可选 peer:marked、echarts(管理端 Markdown/图表)。
import { AgentChatApp, DashboardVoiceChat } from '@souta/agent-chat-ui'
import '@souta/agent-chat-ui/style.css'
宿主需自行注册 Naive UI(全量或按需),本包将 naive-ui 视为 external。
Exports
| Export | 用途 |
|---|---|
AgentChatApp |
管理端:侧栏选智能体 + 会话历史 + 附件/语音 |
DashboardChat / DashboardVoiceChat |
大屏浮动语音对话壳(安卓 window 事件) |
AGENT_CHAT_TRANSPORT_KEY / AGENT_CHAT_NOTIFY_KEY |
provide/inject |
assertTransport / unwrapData / parseSseData |
工具 |
saveBlobAsFile |
下载保存 |
useAgentChatTransport / useAgentChatNotify |
组合式 |
已移除公开 API:createTuiGatewayClient、buildTicketWsUrl、transport.wsHandoff。
Transport 约定(强制 · Phase-1)
type AgentChatSseEvent = {
event: 'delta' | 'interim' | 'complete' | 'status' | 'error' | string
data?: any // 对象或 JSON 字符串均可;包内会 parseSseData
}
type AgentChatTransport = {
/** 管理端必填 */
context?: () => Promise<{
superAdmin?: boolean
agents?: Array<{ agentId: string|number, hermesName?: string, aliasName?: string }>
userLabel?: string
}>
/** 超管搜索/刷新 */
listAgents?: (opts?: { aliasName?: string }) => Promise<Agent[]>
/** 管理端必填:会话 CRUD */
listSessions?: (opts?: { agentId?: string|number }) => Promise<Array<{
sessionId: string, title?: string, preview?: string, updatedAt?: string
}>>
createSession?: (opts: { agentId: string|number, title?: string }) => Promise<{
sessionId: string, messages?: Array<{ id?: string, role: string, text?: string }>, compressions?: number
}>
openSession?: (opts: { sessionId: string, agentId?: string|number }) => Promise<SessionDetail>
deleteSession?: (opts: { sessionId: string, agentId?: string|number }) => Promise<void>
/**
* 必填:发消息并消费 SSE。
* sessionId 缺省或 "new" 时由 BFF/AMC 懒创建;complete/status 可带回 sessionId。
*/
sendMessage: (
opts: {
sessionId?: string
agentId?: string|number
text?: string
attachments?: Array<{ kind?: string, name?: string, mimeType?: string, dataUrl?: string, contentBase64?: string }>
},
onEvent: (ev: AgentChatSseEvent) => void,
) => Promise<void>
transcribe?: (opts: { dataUrl: string, mimeType?: string, profile?: string }) => Promise<{ transcript: string }>
download?: (path: string) => Promise<{ blob: Blob, fileName: string }>
}
宿主实现 sendMessage 时应对 BFF 做 fetch/axios + text/event-stream 解析,并按事件名回调;本包不内置 HTTP 客户端。
无智能体 → 明确报错
- 普通用户:
context().agents为空 → 抛错 / 展示错误:当前用户未绑定智能体… - 超管:
listAgents缺失或返回空 → 同样报错 - 不会回退到「超级管理员默认搜索」或硬编码 profile
Manage 示例
<script setup>
import { AgentChatApp, AGENT_CHAT_NOTIFY_KEY } from '@souta/agent-chat-ui'
import { provide } from 'vue'
import { useMessage, useDialog } from 'naive-ui'
import { request } from '@/utils' // 宿主自己的 axios
const message = useMessage()
const dialog = useDialog()
provide(AGENT_CHAT_NOTIFY_KEY, {
success: m => message.success(m),
error: m => message.error(m),
warning: m => message.warning(m),
confirm: opts => dialog.warning({
title: opts.title,
content: opts.content,
positiveText: opts.positiveText,
negativeText: opts.negativeText,
onPositiveClick: opts.onPositiveClick,
}),
})
async function readSse(res, onEvent) {
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buf = ''
let event = 'message'
let data = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() || ''
for (const line of lines) {
if (!line) {
if (data || event !== 'message')
onEvent({ event, data })
event = 'message'
data = ''
continue
}
if (line.startsWith('event:')) event = line.slice(6).trim()
else if (line.startsWith('data:')) data += (data ? '\n' : '') + line.slice(5).trim()
}
}
}
const transport = {
async context() {
const res = await request.get('/business/agentChat/context')
return res?.data || res
},
async listAgents(opts = {}) {
const res = await request.post('/business/agentChat/agents', opts)
return res?.data || res || []
},
async listSessions({ agentId } = {}) {
const res = await request.get('/business/agentChat/sessions', { params: { agentId } })
return res?.data || res || []
},
async createSession({ agentId, title }) {
const res = await request.post('/business/agentChat/sessions', { agentId, title })
return res?.data || res
},
async openSession({ sessionId, agentId }) {
const res = await request.post(`/business/agentChat/sessions/${encodeURIComponent(sessionId)}/open`, null, {
params: { agentId },
})
return res?.data || res
},
async deleteSession({ sessionId, agentId }) {
await request.delete(`/business/agentChat/sessions/${encodeURIComponent(sessionId)}`, { params: { agentId } })
},
async sendMessage(opts, onEvent) {
const sid = opts.sessionId || 'new'
const res = await fetch(`/business/agentChat/sessions/${encodeURIComponent(sid)}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
// Authorization: 宿主自行带 JWT
},
body: JSON.stringify({
agentId: opts.agentId,
text: opts.text,
attachments: opts.attachments,
}),
credentials: 'include',
})
if (!res.ok) throw new Error(`发送失败 HTTP ${res.status}`)
await readSse(res, onEvent)
},
async transcribe(opts) {
const res = await request.post('/business/agentChat/audio/transcribe', opts)
const body = res?.data || res || {}
return { transcript: String(body.transcript || '').trim(), provider: body.provider }
},
async download(path) {
const blob = await request.get('/business/agentChat/files/download', {
params: { path },
responseType: 'blob',
})
return { blob: blob instanceof Blob ? blob : new Blob([blob]), fileName: path.split('/').pop() || 'download' }
},
}
</script>
<template>
<AgentChatApp :transport="transport" />
</template>
Dashboard 示例
<script setup>
import { DashboardVoiceChat } from '@souta/agent-chat-ui'
const props = defineProps({ unicode: { type: String, required: true } })
const transport = {
// Dashboard 只需 sendMessage;unicode 闭包在宿主
async sendMessage(opts, onEvent) {
const res = await fetch('/dashboard/agentChat/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({
unicode: props.unicode,
sessionId: opts.sessionId || 'new',
text: opts.text,
attachments: opts.attachments,
}),
})
if (!res.ok) throw new Error(`发送失败 HTTP ${res.status}`)
// …同 Manage:解析 SSE 后 onEvent({ event, data })
},
}
</script>
<template>
<DashboardVoiceChat :transport="transport" title="数智能碳管理员" />
</template>
Build
pnpm install
pnpm build
# → dist/agent-chat-ui.js + dist/agent-chat-ui.css
与 YuanZhou 关系
源逻辑来自:
- 管理端:
agent_manage_center_ui/.../agentChat(canonical UI 壳) - 大屏:
yuanzhou_ui/.../DashboardVoiceChat
宿主工程(能碳 / AMC / 元州)需自行写 transport 适配后再替换本地拷贝;本仓库不内嵌项目 axios。