No description
  • Vue 71%
  • JavaScript 27.4%
  • Shell 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
liuxingyu b9faf8385a fix: hide hermes profile codes from chat UI chrome
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 17:49:18 +08:00
dist fix: hide hermes profile codes from chat UI chrome 2026-09-28 17:49:18 +08:00
docs docs: bump amcc Maven coordinate to 1.1.0 2026-09-24 19:58:37 +08:00
scripts chore: add Gitea smoke-test install script 2026-09-28 17:29:00 +08:00
src fix: hide hermes profile codes from chat UI chrome 2026-09-28 17:49:18 +08:00
.gitignore Initial scaffold for @souta/agent-chat-ui. 2026-09-24 17:09:35 +08:00
package-lock.json Initial scaffold for @souta/agent-chat-ui. 2026-09-24 17:09:35 +08:00
package.json fix: hide hermes profile codes from chat UI chrome 2026-09-28 17:49:18 +08:00
README.md chore: publish to Gitea soutan/agent-chat-ui; refresh dist 2026-09-28 17:28:14 +08:00
vite.config.js Initial scaffold for @souta/agent-chat-ui. 2026-09-24 17:09:35 +08:00

@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。