feat: /last and bind button for previous Q/A context (v0.3.4)
After attaching a session, show a button and /last command that load the latest human/assistant turn via session.history so phone users can continue with context. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+75
-1
@@ -4,7 +4,7 @@ import type { UserMessage } from '@deepseek-ai/dsh-llm/types'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { TelegramBridge } from '../src/bridge.ts'
|
||||
import type { InlineKeyboardMarkup, TelegramClientLike, TelegramUpdate } from '../src/client.ts'
|
||||
import { BIND_CB_PREFIX, MSG } from '../src/commands.ts'
|
||||
import { BIND_CB_PREFIX, LAST_CB, MSG } from '../src/commands.ts'
|
||||
|
||||
type SentMessage = {
|
||||
chatId: number
|
||||
@@ -285,6 +285,7 @@ test('callback bind then plain text followups live agent; mirror assistant to ch
|
||||
},
|
||||
})
|
||||
assert.match(sent.at(-1)!.text, /已附着/)
|
||||
assert.equal(sent.at(-1)!.replyMarkup?.inline_keyboard?.[0]?.[0]?.callback_data, LAST_CB)
|
||||
|
||||
await bridge.processUpdate(messageUpdate(10, 1, 'hello from phone', 3))
|
||||
assert.equal(followups.length, 1)
|
||||
@@ -444,6 +445,79 @@ test('/model lists and selects via apiProxy', async () => {
|
||||
assert.match(sent.at(-1)!.text, /已切换模型/)
|
||||
})
|
||||
|
||||
test('/last returns previous Q/A via apiProxy history', async () => {
|
||||
const sent: SentMessage[] = []
|
||||
const followups: UserMessage[] = []
|
||||
const agent = makeAgent('live-last', followups, { title: '有历史' })
|
||||
;(agent as any).session.events = [
|
||||
{
|
||||
type: 'user/message',
|
||||
data: { source: { kind: 'user' }, content: [{ type: 'text', text: '手机续接前的问题' }] },
|
||||
},
|
||||
{
|
||||
type: 'assistant/message',
|
||||
data: { message: { content: [{ type: 'text', text: '电脑上的回答' }] } },
|
||||
},
|
||||
]
|
||||
const ctx = {
|
||||
logger: { info() {}, warn() {}, error() {} },
|
||||
agents: {
|
||||
list: () => [agent],
|
||||
roots: () => [agent],
|
||||
get: () => agent,
|
||||
},
|
||||
apiProxy: {
|
||||
sessions: {
|
||||
history: async () => ({
|
||||
rpcId: 'h',
|
||||
result: {
|
||||
ok: true,
|
||||
value: {
|
||||
events: [
|
||||
{
|
||||
event: {
|
||||
type: 'user/message',
|
||||
data: { source: { kind: 'user' }, content: [{ type: 'text', text: '手机续接前的问题' }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
event: {
|
||||
type: 'assistant/message',
|
||||
data: { message: { content: [{ type: 'text', text: '电脑上的回答' }] } },
|
||||
},
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
on() { return () => {} },
|
||||
}
|
||||
const bridge = new TelegramBridge(ctx as any, {
|
||||
token: 't',
|
||||
allowedUserIds: [1],
|
||||
allowAllUsers: false,
|
||||
client: fakeClient(sent),
|
||||
sleep: async () => {},
|
||||
})
|
||||
await bridge.processUpdate({
|
||||
update_id: 1,
|
||||
callback_query: {
|
||||
id: 'bind',
|
||||
from: { id: 1 },
|
||||
message: { message_id: 1, date: 0, chat: { id: 10, type: 'private' } },
|
||||
data: `${BIND_CB_PREFIX}live-last`,
|
||||
},
|
||||
})
|
||||
await bridge.processUpdate(messageUpdate(10, 1, '/last', 2))
|
||||
const body = sent.at(-1)!.text
|
||||
assert.match(body, /上次对话|用户/)
|
||||
assert.match(body, /手机续接前的问题/)
|
||||
assert.match(body, /电脑上的回答/)
|
||||
})
|
||||
|
||||
test('/unbind clears binding without needing create/dispose', async () => {
|
||||
const sent: SentMessage[] = []
|
||||
const followups: UserMessage[] = []
|
||||
|
||||
@@ -11,6 +11,8 @@ test('parse slash commands for remote control', () => {
|
||||
assert.equal(parseCommand('/unbind').type, 'unbind')
|
||||
assert.equal(parseCommand('/disconnect').type, 'unbind')
|
||||
assert.equal(parseCommand('/model').type, 'model')
|
||||
assert.equal(parseCommand('/last').type, 'last')
|
||||
assert.equal(parseCommand('/context').type, 'last')
|
||||
assert.equal(parseCommand('/sessions@MyBot').type, 'sessions')
|
||||
assert.equal(parseCommand('/foo').type, 'unknown')
|
||||
assert.equal(parseCommand('hello').type, 'plain')
|
||||
@@ -19,6 +21,7 @@ test('parse slash commands for remote control', () => {
|
||||
test('Chinese copy mentions sessions and bind', () => {
|
||||
assert.ok(MSG.DENIED.includes('权限') || MSG.DENIED.includes('授权'))
|
||||
assert.ok(MSG.HELP.includes('/sessions'))
|
||||
assert.ok(MSG.HELP.includes('/last'))
|
||||
assert.ok(MSG.HELP.includes('/model'))
|
||||
assert.ok(MSG.NEED_BIND.includes('/sessions'))
|
||||
assert.ok(MSG.WELCOME.includes('Web') || MSG.WELCOME.includes('遥控器'))
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { extractLastTurn, formatLastTurn } from '../src/history.ts'
|
||||
|
||||
test('extractLastTurn pairs latest human user with following assistant', () => {
|
||||
const turn = extractLastTurn([
|
||||
{
|
||||
type: 'user/message',
|
||||
data: { source: { kind: 'plugin', plugin: 'x' }, content: [{ type: 'text', text: 'injected' }] },
|
||||
},
|
||||
{
|
||||
type: 'user/message',
|
||||
data: { source: { kind: 'user' }, content: [{ type: 'text', text: '第一问' }] },
|
||||
},
|
||||
{
|
||||
type: 'assistant/message',
|
||||
data: { message: { content: [{ type: 'text', text: '第一答' }] } },
|
||||
},
|
||||
{
|
||||
type: 'user/message',
|
||||
data: { source: { kind: 'user' }, content: [{ type: 'text', text: '第二问' }] },
|
||||
},
|
||||
{
|
||||
type: 'assistant/message',
|
||||
data: { message: { content: [{ type: 'text', text: '第二答' }] } },
|
||||
},
|
||||
])
|
||||
assert.equal(turn.userText, '第二问')
|
||||
assert.equal(turn.assistantText, '第二答')
|
||||
})
|
||||
|
||||
test('formatLastTurn renders sections', () => {
|
||||
const text = formatLastTurn({ userText: 'hi', assistantText: 'hello' })
|
||||
assert.match(text, /【用户】/)
|
||||
assert.match(text, /hi/)
|
||||
assert.match(text, /【助手】/)
|
||||
assert.match(text, /hello/)
|
||||
})
|
||||
Reference in New Issue
Block a user