diff --git a/package-lock.json b/package-lock.json
index e9ed8e7..51108cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -197,7 +197,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -244,7 +243,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1145,7 +1143,6 @@
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/lodash": "*"
}
@@ -2338,7 +2335,6 @@
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -2377,15 +2373,13 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
@@ -2630,7 +2624,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2657,7 +2650,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -3048,7 +3040,6 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -3201,7 +3192,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",
diff --git a/src/views/ChatView.vue b/src/views/ChatView.vue
index 71c6f79..f129372 100644
--- a/src/views/ChatView.vue
+++ b/src/views/ChatView.vue
@@ -19,6 +19,7 @@ const inputText = ref('')
const uploadFiles = ref([])
const fileList = ref([])
const sending = ref(false)
+const runStatus = ref('DONE')
const loadingMessages = ref(false)
const errorMessage = ref('')
const showJumpDown = ref(false)
@@ -72,6 +73,48 @@ const normalizeMessage = (msg = {}) => {
}
}
+const extractRunStatus = (msg = {}) => {
+ const status = msg.payload?.status || msg.content
+ return status === 'RUNNING' || status === 'DONE' ? status : null
+}
+
+const applyRunStatus = (msg = {}) => {
+ if (msg.type !== 'run.status') return false
+ const status = extractRunStatus(msg)
+ if (!status) return false
+ runStatus.value = status
+ if (typeof msg.seq === 'number') {
+ lastSeq.value = Math.max(lastSeq.value || 0, msg.seq)
+ summary.lastSeq = lastSeq.value
+ }
+ return true
+}
+
+const refreshLastSeq = (list = []) => {
+ const maxSeq = list.reduce((max, item) => {
+ const seq = typeof item.seq === 'number' ? item.seq : 0
+ return seq > max ? seq : max
+ }, lastSeq.value || 0)
+ lastSeq.value = maxSeq
+ summary.lastSeq = maxSeq
+}
+
+const normalizeMessageList = (list = []) => {
+ let latestStatus = runStatus.value
+ const normalized = []
+ list.forEach((msg) => {
+ if (msg?.type === 'run.status') {
+ const status = extractRunStatus(msg)
+ if (status) latestStatus = status
+ return
+ }
+ normalized.push(normalizeMessage(msg))
+ })
+ runStatus.value = latestStatus
+ refreshLastSeq(list)
+ return normalized
+}
+
const upsertMessage = (msg) => {
const list = messagesState.value
const idx = list.findIndex(
@@ -106,6 +149,8 @@ const normalizedSummary = (data = {}) => ({
tags: data.last_message_type ? [data.last_message_type] : [],
})
+const runActive = computed(() => runStatus.value === 'RUNNING')
+const inputLocked = computed(() => summary.status === 'CLOSED')
const messages = computed(() => messagesState.value || [])
const handleFileChange = (uploadFile, uploadFilesList) => {
@@ -142,8 +187,19 @@ const handleFileRemove = (file, list) => {
uploadFiles.value = filtered.map(toAttachment)
}
+const handleComposerKeydown = (event) => {
+ if (event.key === 'Enter' && event.ctrlKey) {
+ event.preventDefault()
+ handleSend()
+ }
+}
+
const handleSend = () => {
if (sending.value) return
+ if (runActive.value) {
+ ElMessage.info('Agent 正在处理中,请稍候再试')
+ return
+ }
if (summary.status === 'CLOSED') {
ElMessage.warning('会话已归档,无法发送')
return
@@ -291,7 +347,7 @@ const fetchMessages = async () => {
}
)
const list = data.data || []
- messagesState.value = list.map((msg) => normalizeMessage(msg))
+ messagesState.value = normalizeMessageList(list)
if (list.length) {
const last = list[list.length - 1]
lastSeq.value = last.seq || 0
@@ -331,6 +387,7 @@ const fetchSingleMessage = async (messageId) => {
{ headers: { Authorization: `Bearer ${token}` } }
)
const detail = data.data || data
+ if (applyRunStatus(detail)) return
const normalized = normalizeMessage(detail)
upsertMessage(normalized)
} catch (err) {
@@ -366,6 +423,26 @@ const stopSse = () => {
}
}
+const checkSseAuth = async () => {
+ const token = localStorage.getItem('ars-token') || ''
+ if (!token) {
+ router.replace({ name: 'login' })
+ return false
+ }
+ try {
+ await axios.get(`${apiBase}/me`, {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+ return true
+ } catch (err) {
+ if (err.response?.status === 401) {
+ router.replace({ name: 'login' })
+ return false
+ }
+ return true
+ }
+}
+
const startSse = () => {
if (typeof window === 'undefined' || typeof EventSource === 'undefined')
return
@@ -391,6 +468,7 @@ const startSse = () => {
fetchSingleMessage(data.message_id)
return
}
+ if (applyRunStatus(data)) return
const normalized = normalizeMessage(data)
upsertMessage(normalized)
nextTick(() => scrollToBottom())
@@ -400,7 +478,10 @@ const startSse = () => {
}
es.onerror = () => {
stopSse()
- sseRetryTimer.value = window.setTimeout(() => startSse(), 3000)
+ checkSseAuth().then((authed) => {
+ if (!authed) return
+ sseRetryTimer.value = window.setTimeout(() => startSse(), 3000)
+ })
}
eventSource.value = es
}
@@ -409,6 +490,7 @@ const applySession = () => {
if (!sessionId.value) return
stopSse()
lastSeq.value = 0
+ runStatus.value = 'DONE'
localStorage.setItem('ars-current-session', sessionId.value)
fetchSummary()
fetchMessages().then(() => {
@@ -566,6 +648,7 @@ onBeforeUnmount(() => {
multiple
accept="image/*,.pdf,.txt,.doc,.docx,.md"
:auto-upload="false"
+ :disabled="inputLocked"
:show-file-list="false"
:file-list="fileList"
:on-change="handleFileChange"
@@ -581,17 +664,24 @@ onBeforeUnmount(() => {
:autosize="{ minRows: 3, maxRows: 8 }"
class="composer-input"
:placeholder="summary.status === 'CLOSED' ? '会话已归档,无法继续发送' : '输入消息,支持粘贴大段文本与附件...'"
- :disabled="summary.status === 'CLOSED'"
+ :disabled="inputLocked"
+ @keydown="handleComposerKeydown"
/>
-