Skip to content

Commit 2cea8d9

Browse files
authored
feat(message): extract core engine and migrate useMessage to adapters (#325)
1 parent 03cc4f0 commit 2cea8d9

31 files changed

Lines changed: 2580 additions & 745 deletions

docs/src/migration/use-message-migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ outline: [1, 3]
99
## 概述
1010

1111
- **v0.3.x**`useMessage({ client, useStreamByDefault, events... })`,内部直接调用 `client.chat` / `client.chatStream`
12-
- **0.4.x**`useMessage({ responseProvider, plugins... })`,由你提供数据源(Promise 或 AsyncGenerator),框架负责状态机、合并与扩展点;内置 `fallbackRolePlugin``thinkingPlugin``lengthPlugin`,工具调用使用 `toolPlugin`
12+
- **0.4.x**`useMessage({ responseProvider, plugins... })`,由你提供数据源(Promise 或 AsyncGenerator),框架负责状态机、合并与扩展点;内置 `thinkingPlugin``lengthPlugin`,工具调用使用 `toolPlugin`
1313

1414
## v0.3.x 用法
1515

@@ -97,7 +97,7 @@ const engine = useMessage({ responseProvider })
9797

9898
## 插件迁移建议
9999

100-
0.4.x 默认会注入基础插件(role fallback、thinking、length)。可通过 `plugins` 追加能力,或通过同名插件覆盖/禁用默认行为。
100+
0.4.x 默认会注入基础插件(thinking、length)。可通过 `plugins` 追加能力,或通过同名插件覆盖/禁用默认行为。
101101

102102
工具调用推荐使用内置 `toolPlugin`
103103

docs/src/tools/message.md

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,8 @@ interface UseMessageOptions {
9292
requestMessageFieldsExclude?: string[]
9393
/** 插件列表 */
9494
plugins?: UseMessagePlugin[]
95-
/**
96-
* 响应提供者函数,负责发起请求并返回响应。
97-
* 可返回 Promise、AsyncGenerator 或 Promise<AsyncGenerator>
98-
*/
99-
responseProvider: <T = ChatCompletion>(
100-
requestBody: MessageRequestBody,
101-
abortSignal: AbortSignal,
102-
) => Promise<T> | AsyncGenerator<T> | Promise<AsyncGenerator<T>>
95+
/** 响应提供者函数,负责发起请求并返回响应。 */
96+
responseProvider: ResponseProvider
10397
/**
10498
* 全局的数据块处理钩子,在接收到每个响应数据块时触发。
10599
* 注意:此钩子与插件中的 onCompletionChunk 有区别。
@@ -163,9 +157,9 @@ type RequestProcessingState = 'requesting' | 'completing' | string
163157
164158
`useMessage` 支持插件系统,可以通过插件扩展功能。
165159
166-
**默认激活的插件**:`fallbackRolePlugin``thinkingPlugin``lengthPlugin`(无需显式添加,已自动注入)。可通过插件的 `disabled` 参数禁用,例如 `thinkingPlugin({ disabled: true })`
160+
**默认激活的插件**:`thinkingPlugin``lengthPlugin`(无需显式添加,已自动注入)。可通过插件的 `disabled` 参数禁用,例如 `thinkingPlugin({ disabled: true })`
167161
168-
**内置可选插件**:`toolPlugin`工具调用,需添加到 `plugins` 数组中才会生效)
162+
**内置可选插件**:`toolPlugin`(需添加到 `plugins` 数组中才会生效)
169163
170164
可通过 `plugins` 选项追加或覆盖默认插件。插件提供了多个生命周期钩子:
171165
@@ -211,25 +205,6 @@ interface UseMessagePlugin {
211205

212206
### 内置插件
213207

214-
#### fallbackRolePlugin
215-
216-
在请求前为 `role` 为空的消息补全角色,默认使用 `assistant`。可用于兜底上游未设置 role 的消息。**已默认激活**;若需自定义配置,可显式传入覆盖:
217-
218-
| 参数 | 类型 | 默认值 | 说明 |
219-
| -------------- | -------- | ------------- | ------------------------------------ |
220-
| `fallbackRole` | `string` | `'assistant'` | 当消息 `role` 为空时使用的兜底角色。 |
221-
222-
```typescript
223-
import { fallbackRolePlugin, useMessage } from '@opentiny/tiny-robot-kit'
224-
225-
useMessage({
226-
responseProvider,
227-
plugins: [
228-
fallbackRolePlugin({ fallbackRole: 'assistant' }), // 可选,默认即为 'assistant'
229-
],
230-
})
231-
```
232-
233208
#### lengthPlugin
234209

235210
当模型返回 `finish_reason === 'length'`(达到 max_tokens 或上下文限制)时,自动追加一条 user 消息(如 "Please continue with your previous answer.")并调用 `requestNext()` 继续请求,实现“自动续写”。**已默认激活**;若需自定义配置,可显式传入覆盖:
@@ -272,26 +247,25 @@ useMessage({
272247

273248
用于接入模型返回的 `tool_calls`:在请求前注入 `tools` 列表,在请求完成后解析 `tool_calls`、执行 `callTool`、追加 tool 消息并自动发起下一轮请求。支持取消/失败时补充或标记 tool 消息、下一轮是否排除 tool 消息等。**需显式添加到 `plugins` 数组才会生效**
274249

275-
| 参数 | 类型 | 必填 | 默认值 | 说明 |
276-
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
277-
| `getTools` | `() => Promise<Tool[]>` || - | 返回当前轮次要传给 API 的工具列表(OpenAI 格式)。 |
278-
| `callTool` | `(toolCall, context) => Promise<string \| Record<string, any>> \| AsyncGenerator<string \| Record<string, any>>` || - | 执行单个工具调用,返回结果字符串或可流式返回的对象,结果会合并到对应 tool 消息的 `content`|
279-
| `beforeCallTools` | `(toolCalls, context) => Promise<void>` || - | 在真正执行工具前调用,可用于统一校验、鉴权、埋点。`context.currentMessage` 为当前 assistant 消息。 |
280-
| `onToolCallStart` | `(toolCall, context) => void` || - | 单个工具开始执行时触发。此时对应的 tool 消息已经创建并追加到 `messages` 中;`context` 额外包含 `primaryMessage``toolMessage`|
281-
| `onToolCallEnd` | `(toolCall, context) => void` || - | 单个工具执行结束时触发。`context.status``'success' \| 'failed' \| 'cancelled'`,并额外包含 `primaryMessage``toolMessage`,失败或取消时可能有 `context.error`|
282-
| `toolCallCancelledContent` | `string` || `'Tool call cancelled.'` | 请求被中止且需要补全缺失 tool 消息时,填入该默认内容。 |
283-
| `toolCallFailedContent` | `string` || `'Tool call failed.'` | 工具执行抛错且当前 tool 消息内容仍为空时,写入该失败提示。 |
284-
| `autoFillMissingToolMessages` | `boolean` || `false` | 在下一轮开始前,自动补齐上一次被取消但尚未写入的 tool 消息。 |
285-
| `excludeToolMessagesNextTurn` | `boolean \| 'remove'` || `false` | 下一轮请求是否排除带 `tool_calls` 的 assistant 消息及对应 tool 消息。`true` 表示仅从请求体中过滤;`'remove'` 表示直接从 `messages` 中移除。 |
250+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
251+
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
252+
| `getTools` | `() => Promise<Tool[]>` || - | 返回当前轮次要传给 API 的工具列表(OpenAI 格式)。 |
253+
| `callTool` | `(toolCall, context) => Promise<string \| Record<string, any>> \| AsyncGenerator<string \| Record<string, any>>` || - | 执行单个工具调用,返回结果字符串或可流式返回的对象,结果会合并到对应 tool 消息的 `content`|
254+
| `beforeCallTools` | `(toolCalls, context) => Promise<void>` || - | 在真正执行工具前调用,可用于统一校验、鉴权、埋点。新字段为 `context.assistantMessage``context.currentMessage` 继续保留,但已弃用。 |
255+
| `onToolCallStart` | `(toolCall, context) => void` || - | 单个工具开始执行时触发。此时对应的 tool 消息已经创建并追加到 `messages` 中;`context` 额外包含 `assistantMessage``primaryMessage`(兼容字段)和 `toolMessage`|
256+
| `onToolCallEnd` | `(toolCall, context) => void` || - | 单个工具执行结束时触发。`context.status``'success' \| 'failed' \| 'cancelled'`,并额外包含 `assistantMessage``primaryMessage`(兼容字段)和 `toolMessage`,失败或取消时可能有 `context.error`|
257+
| `toolCallCancelledContent` | `string` || `'Tool call cancelled.'` | 请求被中止且需要补全缺失 tool 消息时,填入该默认内容。 |
258+
| `toolCallFailedContent` | `string` || `'Tool call failed.'` | 工具执行抛错且当前 tool 消息内容仍为空时,写入该失败提示。 |
259+
| `autoFillMissingToolMessages` | `boolean` || `false` | 在下一轮开始前,自动补齐上一次被取消但尚未写入的 tool 消息。 |
286260

287261
**回调上下文补充:**
288262

289-
| 回调 | 额外上下文字段 | 说明 |
290-
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
291-
| `beforeCallTools` | `currentMessage` |`BasePluginContext` 基础上额外包含 `currentMessage`,表示当前这条包含 `tool_calls` 的 assistant 消息。 |
292-
| `callTool` | `currentMessage` |`BasePluginContext` 基础上额外包含 `currentMessage`,表示当前这条包含 `tool_calls` 的 assistant 消息 |
293-
| `onToolCallStart` | `primaryMessage``toolMessage` |`BasePluginContext` 基础上额外包含 `primaryMessage``toolMessage`。其中 `primaryMessage` 是触发当前工具调用的 assistant 消息,`toolMessage` 是当前工具对应的 tool 消息。 |
294-
| `onToolCallEnd` | `primaryMessage``toolMessage``status``error?` |`BasePluginContext` 基础上额外包含 `primaryMessage``toolMessage``status`;当工具执行失败或被取消时,还可能包含 `error` |
263+
| 回调 | 额外上下文字段 | 说明 |
264+
| ----------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
265+
| `beforeCallTools` | `assistantMessage``currentMessage`(已弃用) |`BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息。推荐使用 `assistantMessage``currentMessage` 为兼容旧代码保留。 |
266+
| `callTool` | `assistantMessage``currentMessage`(已弃用)、`toolMessage` |`BasePluginContext` 基础上额外包含当前这条带 `tool_calls` 的 assistant 消息,以及当前工具对应的 `toolMessage`。推荐使用 `assistantMessage``currentMessage` 为兼容旧代码保留。 |
267+
| `onToolCallStart` | `assistantMessage``primaryMessage`(兼容字段)`toolMessage` |`BasePluginContext` 基础上额外包含触发当前工具调用的 assistant 消息和当前 tool 消息。推荐使用 `assistantMessage``primaryMessage` 为兼容旧代码保留。 |
268+
| `onToolCallEnd` | `assistantMessage``primaryMessage`(兼容字段)`toolMessage``status``error?` |`BasePluginContext` 基础上额外包含 assistant 消息、当前 tool 消息和执行状态;当工具执行失败或被取消时,还可能包含 `error`推荐使用 `assistantMessage``primaryMessage` 为兼容旧代码保留。 |
295269

296270
##### 基础示例
297271

packages/kit/package.json

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,36 @@
3434
"main": "dist/index.js",
3535
"module": "dist/index.mjs",
3636
"types": "dist/index.d.ts",
37+
"exports": {
38+
".": {
39+
"types": "./dist/index.d.ts",
40+
"import": "./dist/index.mjs",
41+
"require": "./dist/index.js"
42+
},
43+
"./core": {
44+
"types": "./dist/core.d.ts",
45+
"import": "./dist/core.mjs",
46+
"require": "./dist/core.js"
47+
}
48+
},
3749
"files": [
3850
"dist"
3951
],
4052
"sideEffects": false,
4153
"scripts": {
42-
"build": "tsup src/index.ts --format cjs,esm --dts --minify",
43-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch"
54+
"build": "tsup src/index.ts src/core.ts --format cjs,esm --dts --minify",
55+
"dev": "tsup src/index.ts src/core.ts --format cjs,esm --dts --watch",
56+
"test": "vitest run",
57+
"test:watch": "vitest"
4458
},
4559
"author": "",
4660
"license": "MIT",
4761
"devDependencies": {
4862
"@types/node": "^22.13.17",
63+
"openai": "^6.34.0",
4964
"tsup": "^8.0.1",
50-
"typescript": "^5.8.2"
65+
"typescript": "^5.8.2",
66+
"vitest": "^4.1.4"
5167
},
5268
"peerDependencies": {
5369
"vue": ">=3.0.0"

packages/kit/src/core.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './message/adapters'
2+
export * from './message/core'
3+
export * from './message/plugins'
4+
export * from './message/types'
5+
export { combineDeltaData, normalizeToAsyncGenerator } from './message/utils'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './native'
2+
export * from './vue'
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { InternalMessageState, MessageStateAdapter, MutateMessageStateFn, PublicMessageState } from '../types'
2+
import { createStateSubscriptionController } from './shared'
3+
4+
export const createNativeMessageAdapter = (): MessageStateAdapter => {
5+
let initialized = false
6+
let state: InternalMessageState
7+
8+
const initialize = (initialState: InternalMessageState) => {
9+
if (initialized) {
10+
throw new Error('Message state adapter is already initialized')
11+
}
12+
13+
state = {
14+
requestState: initialState.requestState,
15+
processingState: initialState.processingState,
16+
messages: [...initialState.messages],
17+
}
18+
initialized = true
19+
}
20+
21+
const getState = () => {
22+
if (!initialized) {
23+
throw new Error('Message state adapter is not initialized')
24+
}
25+
26+
return {
27+
requestState: state.requestState,
28+
processingState: state.processingState,
29+
messages: [...state.messages],
30+
isProcessing: state.requestState === 'processing',
31+
} satisfies PublicMessageState
32+
}
33+
34+
const subscriptions = createStateSubscriptionController(getState)
35+
36+
const mutate: MutateMessageStateFn = (kind, recipe) => {
37+
if (!initialized) {
38+
throw new Error('Message state adapter is not initialized')
39+
}
40+
41+
let notifySkipped = false
42+
const skipNotify = () => {
43+
notifySkipped = true
44+
}
45+
46+
recipe(state, skipNotify)
47+
48+
if (!notifySkipped) {
49+
subscriptions.notify(kind)
50+
}
51+
}
52+
53+
return {
54+
initialize,
55+
getState,
56+
createMessage(message) {
57+
return message
58+
},
59+
mutate,
60+
subscribe: subscriptions.subscribe,
61+
}
62+
}

0 commit comments

Comments
 (0)