Files
dsh-telegram-channel/lib/history.js
T
owoandCursor fb5a334edb 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>
2026-08-14 20:25:07 +08:00

110 lines
3.6 KiB
JavaScript

import { randomUUID } from 'node:crypto';
import { resolveApiProxy } from './apiproxy.js';
function unwrap(res) {
const r = res;
if (r?.result?.ok === true)
return r.result.value;
return undefined;
}
function contentToText(content) {
if (!Array.isArray(content))
return '';
return content
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
.map((block) => block.text)
.join('')
.trim();
}
/** Extract the latest human Q + assistant A from a session event list. */
export function extractLastTurn(events) {
let lastUser;
let lastAssistant;
for (const ev of events) {
if (ev.type === 'user/message') {
const sourceKind = ev.data?.source?.kind ?? ev.data?.message?.source?.kind;
// Prefer real human prompts; skip plugin-injected context noise.
if (sourceKind && sourceKind !== 'user')
continue;
const text = contentToText(ev.data?.content ?? ev.data?.message?.content);
if (text) {
lastUser = text;
// New user turn resets prior assistant for pairing.
lastAssistant = undefined;
}
continue;
}
if (ev.type === 'assistant/message') {
const text = contentToText(ev.data?.message?.content ?? ev.data?.content);
if (text)
lastAssistant = text;
continue;
}
if (ev.type === 'compaction/summary') {
const text = typeof ev.data?.text === 'string' ? ev.data.text.trim() : '';
if (text && !lastAssistant)
lastAssistant = `(摘要)${text}`;
}
}
return { userText: lastUser, assistantText: lastAssistant };
}
export function formatLastTurn(turn, maxEach = 3500) {
if (!turn.userText && !turn.assistantText) {
return '暂无上次问答(会话可能为空或尚未产生用户回合)。';
}
const clip = (s) => {
const chars = [...s];
if (chars.length <= maxEach)
return s;
return `${chars.slice(0, maxEach - 1).join('')}…`;
};
const parts = ['—— 上次对话 ——'];
if (turn.userText) {
parts.push('', '【用户】', clip(turn.userText));
}
if (turn.assistantText) {
parts.push('', '【助手】', clip(turn.assistantText));
}
return parts.join('\n');
}
async function historyViaApi(ctx, sessionId) {
const api = resolveApiProxy(ctx);
const history = api?.sessions?.history;
if (typeof history !== 'function')
return undefined;
const raw = await history({
rpcId: randomUUID(),
payload: { sessionId, maxMessages: 8 },
});
const value = unwrap(raw);
if (!value?.events)
return undefined;
return value.events.map((entry) => {
if (entry && typeof entry === 'object' && 'event' in entry && entry.event) {
return entry.event;
}
return entry;
});
}
function historyViaAgent(agent) {
const events = agent?.session?.events;
if (!events || events.length === 0)
return undefined;
return [...events];
}
/** Load last Q/A for a bound session (apiProxy history, else live agent events). */
export async function loadLastTurn(ctx, sessionId, agent) {
let events;
try {
events = await historyViaApi(ctx, sessionId);
}
catch {
events = undefined;
}
if (!events || events.length === 0) {
events = historyViaAgent(agent);
}
if (!events || events.length === 0)
return {};
return extractLastTurn(events);
}