Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions docs/src/tools/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,29 +126,36 @@ interface UseMessageReturn {
messages: Ref<ChatMessage[]>
/** 响应提供者(可动态更新) */
responseProvider: Ref<UseMessageOptions['responseProvider']>
/** 是否正在处理中 */
/** 是否正在处理请求(不包含暂停) */
isProcessing: ComputedRef<boolean>
/** 当前回合是否仍在运行或暂停等待确认 */
isCurrentTurn: ComputedRef<boolean>
/** 是否处于暂停等待确认状态 */
isPaused: ComputedRef<boolean>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/** 发送消息 */
sendMessage: (content: string) => Promise<void>
/** 发送消息(支持传入多个消息对象) */
send: (...msgs: ChatMessage[]) => Promise<void>
/** 中止当前请求 */
abortRequest: () => Promise<void>
/** 调度插件命令,例如工具确认或拒绝 */
dispatchCommand: <Result = unknown>(command: string, payload?: unknown) => Promise<Result>
}
```

### 请求状态类型

```typescript
/** 请求状态 */
type RequestState = 'idle' | 'processing' | 'completed' | 'aborted' | 'error'
type RequestState = 'idle' | 'processing' | 'paused' | 'completed' | 'aborted' | 'error'

/** 处理状态 */
type RequestProcessingState = 'requesting' | 'completing' | string
```

- `idle`: 空闲状态,没有正在进行的请求
- `processing`: 正在处理中(包含 `requesting` 和 `completing` 两个子状态)
- `paused`: 当前回合暂停,等待工具确认或外部恢复
- `completed`: 请求已完成
- `aborted`: 请求被中止
- `error`: 请求发生错误
Expand All @@ -169,10 +176,18 @@ interface UseMessagePlugin {
name?: string
/** 是否禁用插件 */
disabled?: boolean | ((context: BasePluginContext) => boolean)
/** 引擎创建时初始化插件运行时状态 */
onInit?: (context: BasePluginContext & { initialMessages: ChatMessage[] }) => MessageEngineInitResult | void
/** 回合从暂停状态恢复前触发 */
onResumed?: (context: BasePluginContext) => MaybePromise<void>
/** 回合进入暂停状态后触发 */
onPaused?: (context: BasePluginContext) => MaybePromise<void>
/** 对话回合开始钩子 */
onTurnStart?: (context: BasePluginContext) => MaybePromise<void>
/** 对话回合结束钩子 */
onTurnEnd?: (context: BasePluginContext) => MaybePromise<void>
/** 对话回合被外部中止时触发 */
onTurnAbort?: (context: BasePluginContext) => MaybePromise<void>
/** 请求开始前钩子 */
onBeforeRequest?: (
context: BasePluginContext & {
Expand All @@ -185,7 +200,7 @@ interface UseMessagePlugin {
currentMessage: ChatMessage
lastChoice?: CompletionChoice
appendMessage: (message: ChatMessage | ChatMessage[]) => void
requestNext: () => void
requestNext: (resume?: boolean) => void
},
) => MaybePromise<void>
/** 数据块处理钩子 */
Expand All @@ -200,9 +215,39 @@ interface UseMessagePlugin {
onError?: (context: BasePluginContext & { error: unknown }) => void
/** 最终清理钩子 */
onFinally?: (context: BasePluginContext) => void
/** 插件命令集合 */
commands?: Record<
string,
(
payload: unknown,
context: BasePluginContext & {
appendMessage: (message: ChatMessage | ChatMessage[]) => void
requestNext: (resume?: boolean) => void
},
) => MaybePromise<unknown>
>
}
```

插件命令通过 `dispatchCommand` 调用。工具插件提供单个工具的确认和拒绝命令:

```typescript
import { TOOL_REJECT_COMMAND, TOOL_RESUME_COMMAND } from '@opentiny/tiny-robot-kit'

// message 是已配置 toolPlugin 的 useMessage 实例

await message.dispatchCommand(TOOL_RESUME_COMMAND, {
toolCallId: 'call-123',
})

await message.dispatchCommand(TOOL_REJECT_COMMAND, {
toolCallId: 'call-456',
reason: '用户拒绝执行该工具',
})
```

`TOOL_RESUME_COMMAND` 和 `TOOL_REJECT_COMMAND` 每次只处理一个 `toolCallId`。确认后会执行该工具;拒绝后会将工具状态标记为 `denied`,不会直接中止整个对话回合。

### 内置插件

#### lengthPlugin
Expand Down Expand Up @@ -257,9 +302,9 @@ useMessage({
| `callTool` | `(toolCall, context) => MaybeStreamableResult<string \| Record<string, unknown>>` | 是 | - | 执行单个工具调用,返回结果字符串或可流式返回的对象,结果会合并到对应 tool 消息的 `content`。可通过 `context.toolSource` 判断工具来源。 |
| `beforeCallTools` | `(toolCalls, context) => Promise<void>` | 否 | - | 在真正执行工具前调用,可用于统一校验、鉴权、埋点。新字段为 `context.assistantMessage`;`context.currentMessage` 继续保留,但已弃用。 |
| `onToolCallStart` | `(toolCall, context) => void` | 否 | - | 单个工具开始执行时触发。此时对应的 tool 消息已经创建并追加到 `messages` 中;`context` 额外包含 `assistantMessage`、`primaryMessage`(兼容字段)和 `toolMessage`。 |
| `onToolCallEnd` | `(toolCall, context) => void` | 否 | - | 单个工具执行结束时触发。`context.status` 为 `'success' \| 'failed' \| 'cancelled'`,并额外包含 `assistantMessage`、`primaryMessage`(兼容字段)和 `toolMessage`,失败或取消时可能有 `context.error`。 |
| `onToolCallEnd` | `(toolCall, context) => void` | 否 | - | 单个工具执行结束时触发。`context.status` 为 `'success' \| 'failed' \| 'cancelled' \| 'denied'`,并额外包含 `assistantMessage`、`primaryMessage`(兼容字段)和 `toolMessage`,失败、取消或拒绝时可能有 `context.error`。 |
| `toolCallCancelledContent` | `string` | 否 | `'Tool call cancelled.'` | 请求被中止且需要补全缺失 tool 消息时,填入该默认内容。 |
| `toolCallFailedContent` | `string` | 否 | `'Tool call failed.'` | 工具执行抛错且当前 tool 消息内容仍为空时,写入该失败提示。 |
| `toolCallFailedContent` | `string` | 否 | `'Tool call failed.'` | 工具执行失败、被用户拒绝或因请求中止而未执行时,写入该提示。 |
| `autoFillMissingToolMessages` | `boolean` | 否 | `false` | 在下一轮开始前,自动补齐上一次被取消但尚未写入的 tool 消息。 |

**回调上下文补充:**
Expand All @@ -269,7 +314,7 @@ useMessage({
| `beforeCallTools` | `assistantMessage`、`currentMessage`(已弃用) | 在 `BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息。推荐使用 `assistantMessage`;`currentMessage` 为兼容旧代码保留。 |
| `callTool` | `assistantMessage`、`currentMessage`(已弃用)、`toolMessage`、`toolSource` | 在 `BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息、当前工具对应的 `toolMessage` 和工具来源。推荐使用 `assistantMessage`;`currentMessage` 为兼容旧代码保留。 |
| `onToolCallStart` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage`、`toolSource` | 在 `BasePluginContext` 基础上额外包含触发当前工具调用的 assistant 消息、当前 tool 消息和工具来源。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 |
| `onToolCallEnd` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage`、`toolSource`、`status`、`error?` | 在 `BasePluginContext` 基础上额外包含 assistant 消息、当前 tool 消息、工具来源和执行状态;当工具执行失败或被取消时,还可能包含 `error`。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 |
| `onToolCallEnd` | `assistantMessage`、`primaryMessage`(兼容字段)、`toolMessage`、`toolSource`、`status`、`error?` | 在 `BasePluginContext` 基础上额外包含 assistant 消息、当前 tool 消息、工具来源和执行状态;当工具执行失败、取消或拒绝时,还可能包含 `error`。推荐使用 `assistantMessage`;`primaryMessage` 为兼容旧代码保留。 |

`toolSource` 类型:

Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/bubble/composables/useToolCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BubbleContentRendererProps, ChatMessageContent } from '../index.type'
import { getJsonrepair } from '../utils'
import { useBubbleStore } from './useBubbleStore'

const toolCallStatus = ['running', 'success', 'failed', 'cancelled'] as const
const toolCallStatus = ['running', 'success', 'failed', 'cancelled', 'awaiting-approval', 'denied'] as const
export type ToolCallStatus = (typeof toolCallStatus)[number]

export const useToolCall = (
Expand Down
23 changes: 20 additions & 3 deletions packages/components/src/bubble/renderers/Tool.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<script setup lang="ts">
import { IconArrowDown, IconCancelled, IconError, IconLoading, IconPlugin } from '@opentiny/tiny-robot-svgs'
import {
IconArrowDown,
IconCancelled,
IconClose,
IconError,
IconLoading,
IconPlugin,
IconWarning,
} from '@opentiny/tiny-robot-svgs'
import { type Component, computed, nextTick, ref, useCssModule, watch, watchEffect } from 'vue'
import { ToolCallStatus, useBubbleEventFn, useToolCall } from '../composables'
import { BubbleContentRendererProps, ChatMessageContent } from '../index.type'
Expand All @@ -20,10 +28,13 @@ const textAndIconMap = new Map<string, { text: string; icon: Component }>([
['success', { text: '已调用', icon: IconPlugin }],
['failed', { text: '调用失败', icon: IconError }],
['cancelled', { text: '已取消', icon: IconCancelled }],
['awaiting-approval', { text: '等待确认', icon: IconWarning }],
['denied', { text: '已拒绝', icon: IconClose }],
])

const textAndIcon = computed(() => {
return textAndIconMap.get(state.value?.status || '') || { text: '', icon: IconPlugin }
const status = state.value?.status || ''
return textAndIconMap.get(status) || { text: '', icon: IconPlugin }
})
Comment thread
xuanlid marked this conversation as resolved.

const prettyJSON = (json: unknown, space = 2) => {
Expand Down Expand Up @@ -193,6 +204,7 @@ const handleClick = () => {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
}

.header-icon {
Expand All @@ -209,9 +221,14 @@ const handleClick = () => {
}

&.icon-failed,
&.icon-cancelled {
&.icon-cancelled,
&.icon-denied {
color: var(--tr-color-error);
}

&.icon-awaiting-approval {
color: var(--tr-color-warning);
}
}

.expand-icon {
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/message/adapters/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const createNativeMessageAdapter = (): MessageStateAdapter => {
processingState: state.processingState,
messages: [...state.messages],
isProcessing: state.requestState === 'processing',
isCurrentTurn: state.requestState === 'processing' || state.requestState === 'paused',
isPaused: state.requestState === 'paused',
} satisfies PublicMessageState
}

Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/message/adapters/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface VueMessageStateAdapter extends MessageStateAdapter {
processingState: Ref<RequestProcessingState | undefined>
messages: Ref<ChatMessage[]>
isProcessing: ComputedRef<boolean>
isCurrentTurn: ComputedRef<boolean>
isPaused: ComputedRef<boolean>
}

const toReactiveMessage = (message: ChatMessage) => reactive(message) as ChatMessage
Expand Down Expand Up @@ -46,6 +48,8 @@ export const createVueMessageAdapter = (): VueMessageStateAdapter => {
const processingState = ref<RequestProcessingState | undefined>(undefined)
const messages = ref<ChatMessage[]>([])
const isProcessing = computed(() => requestState.value === 'processing')
const isCurrentTurn = computed(() => requestState.value === 'processing' || requestState.value === 'paused')
const isPaused = computed(() => requestState.value === 'paused')

const initialize = (initialState: InternalMessageState) => {
if (initialized) {
Expand All @@ -72,6 +76,8 @@ export const createVueMessageAdapter = (): VueMessageStateAdapter => {
processingState: processingState.value,
messages: toPlainValue(messages.value),
isProcessing: isProcessing.value,
isCurrentTurn: isCurrentTurn.value,
isPaused: isPaused.value,
} satisfies PublicMessageState
}

Expand Down Expand Up @@ -127,6 +133,8 @@ export const createVueMessageAdapter = (): VueMessageStateAdapter => {
processingState,
messages,
isProcessing,
isCurrentTurn,
isPaused,
initialize,
getState,
createMessage,
Expand Down
Loading
Loading