Skip to content

Commit dfd83bd

Browse files
author
linyuan.yang
committed
up
1 parent 3c08e8a commit dfd83bd

14 files changed

Lines changed: 194 additions & 74 deletions

File tree

packages/admin/src/components/MessageList.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const labels = computed<ChatLabels>(() => ({
3636
download: t('common.download'),
3737
close: t('common.close'),
3838
archivedTag: t('savers.archived_tag'),
39+
commandTag: t('savers.command_tag'),
40+
exceptionTag: t('savers.exception_tag'),
3941
}))
4042
4143
async function fetchFn(url: string) {

packages/admin/src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ export default {
241241
history_cleared: 'History cleared',
242242
show_archived: 'Show archived',
243243
archived_tag: 'Archived',
244+
command_tag: 'Command',
245+
exception_tag: 'Exception',
244246
},
245247
memories: {
246248
add: '+ Add Snippet Knowledge',

packages/admin/src/i18n/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ export default {
241241
history_cleared: '历史已清除',
242242
show_archived: '显示已归档消息',
243243
archived_tag: '已归档',
244+
command_tag: '命令',
245+
exception_tag: '异常',
244246
},
245247
memories: {
246248
add: '+ 添加碎片知识库',

packages/chat-ui/src/WebSocketTransport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ export class WebSocketTransport implements IChatTransport {
103103
}
104104

105105
async deleteSession(sessionId: string): Promise<void> {
106-
await this.api(`/api/settings/sessions/${encodeURIComponent(sessionId)}`, 'DELETE')
107106
await this.api(`/api/sessions/${encodeURIComponent(sessionId)}/history`, 'DELETE').catch(() => {})
107+
await this.api(`/api/settings/sessions/${encodeURIComponent(sessionId)}`, 'DELETE')
108108
}
109109

110110
async updateSession(sessionId: string, patch: Partial<SessionItem>): Promise<void> {

packages/chat-ui/src/components/MessageList.vue

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ import type { StoredMessage, ToolCall, ChatLabels, DisplayContent } from '../typ
66
function isArchived(m: StoredMessage): boolean {
77
return m.kind === MessageKind.Archive
88
}
9+
function isCommand(m: StoredMessage): boolean {
10+
return m.kind === MessageKind.Command
11+
}
12+
function isException(m: StoredMessage): boolean {
13+
return m.kind === MessageKind.Exception
14+
}
15+
function rowKindClass(m: StoredMessage) {
16+
return {
17+
archived: isArchived(m),
18+
command: isCommand(m),
19+
exception: isException(m),
20+
}
21+
}
922
import { fmtTs, fmtDateSep, toggleToolCall } from '../messageRender'
1023
import { inlineArgs, resultPreview } from '../toolCallFormat'
1124
import { resolveLabels, tpl } from '../labels'
@@ -76,11 +89,13 @@ function isEmbeddedTool(msg: StoredMessage): boolean {
7689

7790
<template v-if="!isEmbeddedTool(msg)">
7891
<!-- Human message -->
79-
<div v-if="msg.message.role === MessageRole.Human" class="msg-row human" :class="{ archived: isArchived(msg) }">
92+
<div v-if="msg.message.role === MessageRole.Human" class="msg-row human" :class="rowKindClass(msg)">
8093
<div class="msg-bubble human">
8194
<div class="msg-role-bar">
8295
<span class="msg-role">{{ L.roleUser }}</span>
8396
<span v-if="isArchived(msg)" class="msg-archived-tag">{{ L.archivedTag }}</span>
97+
<span v-if="isCommand(msg)" class="msg-kind-tag msg-kind-command">{{ L.commandTag }}</span>
98+
<span v-if="isException(msg)" class="msg-kind-tag msg-kind-exception">{{ L.exceptionTag }}</span>
8499
<span v-if="msg.createdAt" class="msg-time">{{ fmtTs(msg.createdAt) }}</span>
85100
<div v-if="msg.thinkId && thinksUrlPrefix" class="think-toggle think-toggle-human" @click="openThink(msg.thinkId!)">
86101
<span>▸</span><span>{{ L.think }}</span>
@@ -91,11 +106,13 @@ function isEmbeddedTool(msg: StoredMessage): boolean {
91106
</div>
92107

93108
<!-- AI message -->
94-
<div v-else-if="msg.message.role === MessageRole.AI" class="msg-row ai" :class="{ archived: isArchived(msg) }">
109+
<div v-else-if="msg.message.role === MessageRole.AI" class="msg-row ai" :class="rowKindClass(msg)">
95110
<div v-if="msg.message.content" class="msg-bubble ai">
96111
<div class="msg-role-bar">
97112
<span class="msg-role">{{ L.roleAi }}</span>
98113
<span v-if="isArchived(msg)" class="msg-archived-tag">{{ L.archivedTag }}</span>
114+
<span v-if="isCommand(msg)" class="msg-kind-tag msg-kind-command">{{ L.commandTag }}</span>
115+
<span v-if="isException(msg)" class="msg-kind-tag msg-kind-exception">{{ L.exceptionTag }}</span>
99116
<span v-if="msg.createdAt" class="msg-time">{{ fmtTs(msg.createdAt) }}</span>
100117
<div v-if="msg.thinkId && thinksUrlPrefix" class="think-toggle" @click="openThink(msg.thinkId!)">
101118
<span>▸</span><span>{{ L.think }}</span>
@@ -109,6 +126,8 @@ function isEmbeddedTool(msg: StoredMessage): boolean {
109126
<div class="msg-role has-think">
110127
{{ tpl(L.toolCalls, { count: msg.message.tool_calls.length }) }}
111128
<span v-if="isArchived(msg) && !msg.message.content" class="msg-archived-tag">{{ L.archivedTag }}</span>
129+
<span v-if="isCommand(msg) && !msg.message.content" class="msg-kind-tag msg-kind-command">{{ L.commandTag }}</span>
130+
<span v-if="isException(msg) && !msg.message.content" class="msg-kind-tag msg-kind-exception">{{ L.exceptionTag }}</span>
112131
<div v-if="msg.thinkId && thinksUrlPrefix && !msg.message.content" class="think-toggle" @click="openThink(msg.thinkId!)">
113132
<span>▸</span><span>{{ L.think }}</span>
114133
</div>
@@ -141,22 +160,26 @@ function isEmbeddedTool(msg: StoredMessage): boolean {
141160
</div>
142161

143162
<!-- Tool message (standalone, no tool_call_id) -->
144-
<div v-else-if="msg.message.role === MessageRole.Tool" class="msg-row ai" :class="{ archived: isArchived(msg) }">
163+
<div v-else-if="msg.message.role === MessageRole.Tool" class="msg-row ai" :class="rowKindClass(msg)">
145164
<div class="msg-bubble tool">
146165
<div class="msg-role-bar">
147166
<span class="msg-role">Tool{{ msg.message.name ? ` · ${msg.message.name}` : '' }}</span>
148167
<span v-if="isArchived(msg)" class="msg-archived-tag">{{ L.archivedTag }}</span>
168+
<span v-if="isCommand(msg)" class="msg-kind-tag msg-kind-command">{{ L.commandTag }}</span>
169+
<span v-if="isException(msg)" class="msg-kind-tag msg-kind-exception">{{ L.exceptionTag }}</span>
149170
</div>
150171
<ContentParts :content="msg.message.content" @open-image="openLightbox" />
151172
</div>
152173
</div>
153174

154175
<!-- System / other -->
155-
<div v-else class="msg-row ai" :class="{ archived: isArchived(msg) }">
176+
<div v-else class="msg-row ai" :class="rowKindClass(msg)">
156177
<div class="msg-bubble ai">
157178
<div class="msg-role-bar">
158179
<span class="msg-role">{{ msg.message.role }}</span>
159180
<span v-if="isArchived(msg)" class="msg-archived-tag">{{ L.archivedTag }}</span>
181+
<span v-if="isCommand(msg)" class="msg-kind-tag msg-kind-command">{{ L.commandTag }}</span>
182+
<span v-if="isException(msg)" class="msg-kind-tag msg-kind-exception">{{ L.exceptionTag }}</span>
160183
<span v-if="msg.createdAt" class="msg-time">{{ fmtTs(msg.createdAt) }}</span>
161184
</div>
162185
<ContentParts :content="msg.message.content" @open-image="openLightbox" />
@@ -469,6 +492,47 @@ function isEmbeddedTool(msg: StoredMessage): boolean {
469492
white-space: nowrap;
470493
}
471494
495+
/* Command (指令型回调) — 不进入 LLM 上下文,仅作为旁路记录展示 */
496+
.msg-row.command .msg-bubble,
497+
.msg-row.command .msg-tool-calls {
498+
border-left: 3px solid #818cf8;
499+
}
500+
.msg-row.command .msg-bubble.ai,
501+
.msg-row.command .msg-bubble.tool,
502+
.msg-row.command .msg-tool-calls {
503+
background: rgba(99, 102, 241, 0.06);
504+
}
505+
.msg-kind-tag {
506+
font-size: 10px;
507+
font-weight: 600;
508+
padding: 1px 6px;
509+
border-radius: 3px;
510+
text-transform: uppercase;
511+
letter-spacing: 0.04em;
512+
white-space: nowrap;
513+
}
514+
.msg-kind-command {
515+
color: #4338ca;
516+
background: #e0e7ff;
517+
border: 1px solid #c7d2fe;
518+
}
519+
520+
/* Exception (异常记录) — 标红警示 */
521+
.msg-row.exception .msg-bubble,
522+
.msg-row.exception .msg-tool-calls {
523+
border-left: 3px solid #f87171;
524+
}
525+
.msg-row.exception .msg-bubble.ai,
526+
.msg-row.exception .msg-bubble.tool,
527+
.msg-row.exception .msg-tool-calls {
528+
background: rgba(239, 68, 68, 0.06);
529+
}
530+
.msg-kind-exception {
531+
color: #b91c1c;
532+
background: #fee2e2;
533+
border: 1px solid #fca5a5;
534+
}
535+
472536
/* Thinking indicator */
473537
.chatui-thinking {
474538
color: var(--chatui-fg-secondary);

packages/chat-ui/src/labels.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export const defaultLabels: Required<ChatLabels> = {
8080
selectOrCreate: '← 请选择或创建会话',
8181
archivedTag: '已归档',
8282
showArchived: '显示已归档',
83+
commandTag: '命令',
84+
exceptionTag: '异常',
8385
}
8486

8587
export function resolveLabels(partial?: ChatLabels): Required<ChatLabels> {

packages/chat-ui/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ export interface ChatLabels {
180180
archivedTag?: string
181181
/** StatusBar 的「显示已归档」复选框文案 */
182182
showArchived?: string
183+
/** 命令型消息行右上角的小角标文案 */
184+
commandTag?: string
185+
/** 异常型消息行右上角的小角标文案 */
186+
exceptionTag?: string
183187
}
184188

185189
// ── Wiki option ──

packages/sbot/src/Session/SessionManager.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import "reflect-metadata";
2-
import { ICommand, MessageType, type ChatMessage, type MessageContent, trimContent, isEmptyContent } from "scorpio.ai";
2+
import { ICommand, MessageType, MessageRole, MessageKind, type ChatMessage, type MessageContent, trimContent, isEmptyContent } from "scorpio.ai";
33
import { SessionManager, SessionService, ChannelMessageArgs, ChannelSessionHandler } from "channel.base";
44
import { type StructuredToolInterface } from "@langchain/core/tools";
55
import { WEB_CHANNEL_ID, WEB_CHANNEL_TYPE, type ChannelConfig } from "sbot.commons";
66
import { config } from "../Core/Config";
77
import { ChannelSessionRow, getChannelSession } from "../Core/Database";
88
import { channelManager } from "../Channel/ChannelManager";
99
import { createProcessAIHandler } from "../Processing/createProcessAIHandler";
10+
import { AgentRunner } from "../Agent/AgentRunner";
1011
import { MiddlewarePipeline } from "../Middleware/MiddlewarePipeline";
1112
import { intentFilterMiddleware } from "../Middleware/intentFilter";
1213
import type { MessageContext } from "../Middleware/types";
@@ -62,7 +63,18 @@ class SbotSession extends SessionService {
6263
await this.getChannel(args).processAI(query, args);
6364
}
6465

65-
protected async onCommandResult(content: string, args: ChannelRouteArgs): Promise<void> {
66+
protected async onCommandResult(query: string, content: string, args: ChannelRouteArgs): Promise<void> {
67+
const resolved = await this.resolveSessionConfig(args);
68+
const saverId = resolved?.dbSession.saver || resolved?.channelConfig?.saver;
69+
if (saverId) {
70+
const saver = await AgentRunner.createSaverService(saverId, this.threadId);
71+
try {
72+
await saver.pushMessage({ role: MessageRole.Human, content: query }, { kind: MessageKind.Command });
73+
await saver.pushMessage({ role: MessageRole.AI, content, isCommand: true }, { kind: MessageKind.Command });
74+
} finally {
75+
await saver.dispose();
76+
}
77+
}
6678
await this.getChannel(args).onCommandResult(content, args);
6779
}
6880

packages/scorpio.ai/src/Agents/ACP/ACPAgentServiceBase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
7575
return await this.collectResponse(response, callback);
7676
} catch (e) {
7777
this.onStreamError(e);
78+
await this.recordException(e, this._thinkId ? { thinkId: this._thinkId } : undefined);
7879
throw e;
7980
} finally {
8081
signal?.removeEventListener("abort", onCancel);

packages/scorpio.ai/src/Agents/AgentServiceBase.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ServiceContainer } from "scorpio.di";
22
import { IMemoryService } from "../Memory";
33
import { IWikiService } from "../Wiki";
4-
import { IAgentSaverService, AgentMemorySaver, ChatMessage, ChatToolCall, MessageRole, type MessageContent, type TokenUsage } from "../Saver";
4+
import { IAgentSaverService, AgentMemorySaver, ChatMessage, ChatToolCall, MessageKind, MessageRole, type MessageContent, type TokenUsage } from "../Saver";
55
import { ILoggerService, ILogger } from "../Logger";
66

77

@@ -125,6 +125,25 @@ export abstract class AgentServiceBase {
125125
}
126126

127127
abstract stream(query: MessageContent, callback: IAgentCallback, signal?: AbortSignal): Promise<ChatMessage[]>;
128+
129+
/**
130+
* 将运行/工具异常以 {@link MessageKind.Exception} 形式落库,便于回溯。
131+
* 取消({@link AgentCancelledError})不记录;落库失败仅日志告警,不影响原始 error 的传播。
132+
*/
133+
protected async recordException(error: unknown, options?: { thinkId?: string }): Promise<void> {
134+
if (error instanceof AgentCancelledError) return;
135+
const e = error as { name?: string; message?: string };
136+
const text = `[${e?.name ?? 'Error'}] ${e?.message ?? String(error)}`;
137+
try {
138+
await this.saverService.pushMessage(
139+
{ role: MessageRole.System, content: text },
140+
{ kind: MessageKind.Exception, ...(options?.thinkId ? { thinkId: options.thinkId } : {}) },
141+
);
142+
} catch (saveErr: any) {
143+
this.logger?.warn(`Failed to record exception to saver: ${saveErr?.message ?? saveErr}`);
144+
}
145+
}
146+
128147
/**
129148
* 释放资源
130149
*/

0 commit comments

Comments
 (0)