feat: /compress 上下文压缩 + Skill 渐进式加载
This commit is contained in:
@@ -16,6 +16,7 @@ import { addToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExter
|
||||
import { ChatDB } from '../db/chat-db.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { runAgentLoop } from '../services/agent-engine.js';
|
||||
import { estimateTokens } from '../services/context-manager.js';
|
||||
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
|
||||
import { showToolConfirm } from './tool-confirm-modal.js';
|
||||
import { logInfo, logStream, logError, logSuccess, logWarn } from '../services/log-service.js';
|
||||
@@ -448,7 +449,7 @@ async function handleCompress(): Promise<void> {
|
||||
|
||||
logInfo('上下文压缩: 开始');
|
||||
|
||||
// 取中间的消息做摘要(保留首尾各 2 条)
|
||||
// 取中间的消息做摘要(保留首尾各 2 条,跳过已压缩的)
|
||||
const messages = currentSession.messages;
|
||||
const keepStart = 2;
|
||||
const keepEnd = 2;
|
||||
@@ -456,10 +457,16 @@ async function handleCompress(): Promise<void> {
|
||||
const tail = messages.slice(-keepEnd);
|
||||
const middle = messages.slice(keepStart, messages.length - keepEnd);
|
||||
|
||||
const conversationText = middle.map(m => {
|
||||
// 过滤掉已压缩的消息,避免重复压缩
|
||||
const uncompressedMiddle = middle.filter(m => !m.compressed);
|
||||
if (uncompressedMiddle.length === 0) {
|
||||
showToast('中间消息已全部压缩,无需再次压缩', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationText = uncompressedMiddle.map(m => {
|
||||
const role = m.role === 'user' ? '用户' : 'AI';
|
||||
let content = m.content || '';
|
||||
// 截断过长内容
|
||||
if (content.length > 500) content = content.slice(0, 500) + '...';
|
||||
if (m.toolCalls?.length) content += ` [工具调用: ${m.toolCalls.map(t => t.name).join(', ')}]`;
|
||||
return `${role}: ${content}`;
|
||||
@@ -473,7 +480,7 @@ async function handleCompress(): Promise<void> {
|
||||
model,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: `请将以下对话摘要为一段简洁的上下文总结。保留关键信息:讨论的主题、得出的结论、用户的偏好、未完成的任务。用中文输出,不超过 500 字。\n\n对话记录:\n${conversationText}`
|
||||
content: `请将以下对话摘要为一段简洁的上下文总结。保留关键信息:讨论的主题、得出的结论、用户的偏好、未完成的任务、关键工具调用结果。用中文输出,不超过 500 字。\n\n对话记录:\n${conversationText}`
|
||||
}],
|
||||
stream: true,
|
||||
think: false,
|
||||
@@ -489,14 +496,17 @@ async function handleCompress(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建压缩后的消息列表
|
||||
// 构建压缩后的摘要消息(标记 compressed)
|
||||
const summaryMsg: ChatMessage = {
|
||||
role: 'system',
|
||||
content: `📋 以下是对之前对话的摘要(已压缩 ${middle.length} 条消息):\n\n${summary}`,
|
||||
timestamp: Date.now()
|
||||
content: `📋 以下是对之前对话的摘要(已压缩 ${uncompressedMiddle.length} 条消息):\n\n${summary}`,
|
||||
timestamp: Date.now(),
|
||||
compressed: true
|
||||
};
|
||||
|
||||
currentSession.messages = [...head, summaryMsg, ...tail];
|
||||
// 保留已压缩的中间消息 + 新摘要
|
||||
const alreadyCompressed = middle.filter(m => m.compressed);
|
||||
currentSession.messages = [...head, ...alreadyCompressed, summaryMsg, ...tail];
|
||||
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSession(currentSession);
|
||||
@@ -504,8 +514,8 @@ async function handleCompress(): Promise<void> {
|
||||
clearMessagesDOM();
|
||||
renderMessages();
|
||||
|
||||
logInfo(`上下文压缩完成: ${middle.length} 条 → 1 条摘要`);
|
||||
showToast(`已压缩 ${middle.length} 条消息为摘要`, 'success');
|
||||
logInfo(`上下文压缩完成: ${uncompressedMiddle.length} 条 → 1 条摘要`);
|
||||
showToast(`已压缩 ${uncompressedMiddle.length} 条消息为摘要`, 'success');
|
||||
} catch (err) {
|
||||
logError('上下文压缩失败', (err as Error).message);
|
||||
showToast(`压缩失败: ${(err as Error).message}`, 'error');
|
||||
|
||||
@@ -17,7 +17,7 @@ import { showToast } from '../components/toast.js';
|
||||
import { logInfo, logWarn, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse } from './log-service.js';
|
||||
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
|
||||
import { generateId } from '../utils/utils.js';
|
||||
import { buildContext, estimateTokens } from './context-manager.js';
|
||||
import { buildContext, estimateTokens, shouldAutoCompress, compressWithLLM, AUTO_COMPRESS_THRESHOLD } from './context-manager.js';
|
||||
import type {
|
||||
OllamaMessage,
|
||||
OllamaStreamChunk,
|
||||
@@ -497,6 +497,22 @@ export async function runAgentLoop(
|
||||
messages.length = 0;
|
||||
messages.push(...contextResult);
|
||||
|
||||
// 自动压缩:当上下文 token 超过 context window 的 50% 时,调用 LLM 摘要压缩
|
||||
if (shouldAutoCompress(messages, numCtx)) {
|
||||
logInfo(`自动上下文压缩触发: tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))} > ${Math.floor(numCtx * AUTO_COMPRESS_THRESHOLD)} (50% of ${numCtx})`);
|
||||
try {
|
||||
const compressed = await compressWithLLM(messages, api, model, { abortController });
|
||||
if (compressed.length < messages.length || estimateTokens(compressed.map(m => m.content || '').join('')) < estimateTokens(messages.map(m => m.content || '').join(''))) {
|
||||
messages.length = 0;
|
||||
messages.push(...compressed);
|
||||
logSuccess(`自动上下文压缩完成: 剩余 ${messages.length} 条消息`);
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') throw err;
|
||||
logWarn('自动上下文压缩失败,继续使用当前上下文', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`);
|
||||
|
||||
// 迭代预算:从 state 读取,默认 85
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Context Manager - 智能上下文管理 (v4.0)
|
||||
* 三层策略:滑动窗口 + 消息摘要压缩 + 记忆注入
|
||||
* Context Manager - 智能上下文管理 (v5.0)
|
||||
* 三层策略:滑动窗口 + LLM 摘要压缩 + 记忆注入
|
||||
* 支持自动压缩与手动 /compress 触发
|
||||
*/
|
||||
|
||||
import type { OllamaMessage } from '../types.js';
|
||||
import { logInfo, logWarn } from './log-service.js';
|
||||
import type { OllamaMessage, OllamaStreamChunk } from '../types.js';
|
||||
import { logInfo, logWarn, logSuccess, logError } from './log-service.js';
|
||||
|
||||
/** 粗略估算 token 数 */
|
||||
export function estimateTokens(text: string): number {
|
||||
@@ -19,6 +20,13 @@ export function estimateTokens(text: string): number {
|
||||
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
|
||||
}
|
||||
|
||||
/** 自动压缩阈值:当消息 token 占 context window 比例超过此值时触发自动压缩 */
|
||||
export const AUTO_COMPRESS_THRESHOLD = 0.5;
|
||||
|
||||
/** 压缩后保留首尾消息数 */
|
||||
const COMPRESS_KEEP_HEAD = 2;
|
||||
const COMPRESS_KEEP_TAIL = 2;
|
||||
|
||||
export interface ContextBuildOptions {
|
||||
/** 滑动窗口大小(最近 N 条消息完整保留) */
|
||||
windowSize?: number;
|
||||
@@ -41,10 +49,10 @@ const DEFAULT_OPTIONS: Required<ContextBuildOptions> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建发送给模型的 messages
|
||||
* 构建发送给模型的 messages(同步,滑动窗口)
|
||||
* 三层策略:
|
||||
* a. 滑动窗口:最近 N 条消息完整保留
|
||||
* b. 更早的消息:每 N 条压缩为一段摘要
|
||||
* b. 更早的消息:每 N 条压缩为一段摘要(快速文本截取)
|
||||
* c. 系统 prompt 注入记忆上下文
|
||||
*/
|
||||
export function buildContext(
|
||||
@@ -64,9 +72,9 @@ export function buildContext(
|
||||
}
|
||||
|
||||
// 提取已有的 system 消息
|
||||
const existingSystem = allMessages.find(m => m.role === 'system');
|
||||
if (existingSystem) {
|
||||
systemContent += existingSystem.content;
|
||||
const existingSystem = allMessages.filter(m => m.role === 'system');
|
||||
for (const sys of existingSystem) {
|
||||
systemContent += sys.content + '\n';
|
||||
}
|
||||
|
||||
if (systemContent.trim()) {
|
||||
@@ -77,7 +85,6 @@ export function buildContext(
|
||||
const nonSystemMessages = allMessages.filter(m => m.role !== 'system');
|
||||
|
||||
if (nonSystemMessages.length <= opts.windowSize) {
|
||||
// 消息不多,全部保留
|
||||
result.push(...nonSystemMessages);
|
||||
return result;
|
||||
}
|
||||
@@ -85,11 +92,21 @@ export function buildContext(
|
||||
// 滑动窗口:最近 N 条
|
||||
const recentMessages = nonSystemMessages.slice(-opts.windowSize);
|
||||
|
||||
// 更早的消息:压缩为摘要
|
||||
// 更早的消息:已压缩标记的保留原样,未压缩的做快速摘要
|
||||
const olderMessages = nonSystemMessages.slice(0, -opts.windowSize);
|
||||
const summaries = summarizeOlderMessages(olderMessages, opts.summaryBatchSize);
|
||||
const compressedMsgs = olderMessages.filter(m => m.compressed);
|
||||
const uncompressedMsgs = olderMessages.filter(m => !m.compressed);
|
||||
|
||||
result.push(...summaries, ...recentMessages);
|
||||
// 已压缩的消息直接保留
|
||||
result.push(...compressedMsgs);
|
||||
|
||||
// 未压缩的消息做快速摘要
|
||||
if (uncompressedMsgs.length > 0) {
|
||||
const summaries = summarizeOlderMessages(uncompressedMsgs, opts.summaryBatchSize);
|
||||
result.push(...summaries);
|
||||
}
|
||||
|
||||
result.push(...recentMessages);
|
||||
|
||||
// Token 估算和裁剪
|
||||
const trimmed = trimByTokenLimit(result, opts.maxTokens);
|
||||
@@ -101,7 +118,132 @@ export function buildContext(
|
||||
}
|
||||
|
||||
/**
|
||||
* 将较早的消息每 batchSize 条压缩为一段摘要
|
||||
* 判断是否需要自动压缩
|
||||
* 当总 token 数超过 context window 的 AUTO_COMPRESS_THRESHOLD 比例时返回 true
|
||||
*/
|
||||
export function shouldAutoCompress(messages: OllamaMessage[], numCtx: number): boolean {
|
||||
const totalTokens = estimateTokens(messages.map(m => m.content || '').join(''));
|
||||
const threshold = numCtx * AUTO_COMPRESS_THRESHOLD;
|
||||
return totalTokens > threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 摘要压缩:调用模型对中间消息生成摘要
|
||||
* 保留首尾各 keepHead/keepTail 条消息,中间用 LLM 摘要替换
|
||||
*
|
||||
* @returns 压缩后的消息列表(包含 compressed 标记的摘要消息)
|
||||
*/
|
||||
export async function compressWithLLM(
|
||||
messages: OllamaMessage[],
|
||||
api: { chatStream: (params: Record<string, unknown>, onChunk: (chunk: OllamaStreamChunk) => void, ac?: AbortController) => Promise<void> },
|
||||
model: string,
|
||||
options: {
|
||||
keepHead?: number;
|
||||
keepTail?: number;
|
||||
maxSummaryTokens?: number;
|
||||
abortController?: AbortController;
|
||||
} = {}
|
||||
): Promise<OllamaMessage[]> {
|
||||
const keepHead = options.keepHead ?? COMPRESS_KEEP_HEAD;
|
||||
const keepTail = options.keepTail ?? COMPRESS_KEEP_TAIL;
|
||||
const maxSummaryTokens = options.maxSummaryTokens ?? 500;
|
||||
|
||||
// 分离 system 和非 system 消息
|
||||
const systemMsgs = messages.filter(m => m.role === 'system');
|
||||
const nonSystemMsgs = messages.filter(m => m.role !== 'system');
|
||||
|
||||
if (nonSystemMsgs.length <= keepHead + keepTail + 2) {
|
||||
logInfo('上下文压缩: 消息太少,跳过压缩');
|
||||
return messages;
|
||||
}
|
||||
|
||||
const head = nonSystemMsgs.slice(0, keepHead);
|
||||
const tail = nonSystemMsgs.slice(-keepTail);
|
||||
const middle = nonSystemMsgs.slice(keepHead, nonSystemMsgs.length - keepTail);
|
||||
|
||||
// 过滤掉已经压缩过的消息(避免重复压缩)
|
||||
const uncompressedMiddle = middle.filter(m => !m.compressed);
|
||||
if (uncompressedMiddle.length === 0) {
|
||||
logInfo('上下文压缩: 中间消息已全部压缩,跳过');
|
||||
return messages;
|
||||
}
|
||||
|
||||
// 构建对话文本
|
||||
const conversationText = uncompressedMiddle.map(m => {
|
||||
const role = m.role === 'user' ? '用户' : 'AI';
|
||||
let content = m.content || '';
|
||||
if (content.length > 500) content = content.slice(0, 500) + '...';
|
||||
if (m.tool_calls?.length) {
|
||||
const toolNames = m.tool_calls.map(t => t.function.name).join(', ');
|
||||
content += ` [工具调用: ${toolNames}]`;
|
||||
}
|
||||
return `${role}: ${content}`;
|
||||
}).join('\n');
|
||||
|
||||
logInfo(`上下文压缩: 开始 LLM 摘要,${uncompressedMiddle.length} 条消息待压缩`);
|
||||
|
||||
let summary = '';
|
||||
try {
|
||||
await api.chatStream(
|
||||
{
|
||||
model,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: `请将以下对话摘要为一段简洁的上下文总结。保留关键信息:讨论的主题、得出的结论、用户的偏好、未完成的任务、关键工具调用结果。用中文输出,不超过 ${maxSummaryTokens} 字。\n\n对话记录:\n${conversationText}`
|
||||
}],
|
||||
stream: true,
|
||||
think: false,
|
||||
options: { num_ctx: 8192, temperature: 0.3 }
|
||||
},
|
||||
(chunk: OllamaStreamChunk) => {
|
||||
if (chunk.message?.content) {
|
||||
summary += chunk.message.content;
|
||||
}
|
||||
},
|
||||
options.abortController
|
||||
);
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') {
|
||||
logWarn('上下文压缩: LLM 调用被中止');
|
||||
return messages;
|
||||
}
|
||||
logError('上下文压缩: LLM 调用失败', (err as Error).message);
|
||||
return messages;
|
||||
}
|
||||
|
||||
if (!summary.trim()) {
|
||||
logWarn('上下文压缩: 模型未返回摘要内容');
|
||||
return messages;
|
||||
}
|
||||
|
||||
// 构建压缩后的摘要消息
|
||||
const summaryMsg: OllamaMessage = {
|
||||
role: 'system',
|
||||
content: `📋 以下是对之前对话的摘要(已压缩 ${uncompressedMiddle.length} 条消息):\n\n${summary}`,
|
||||
compressed: true
|
||||
};
|
||||
|
||||
// 保留已压缩的中间消息 + 新摘要
|
||||
const alreadyCompressed = middle.filter(m => m.compressed);
|
||||
|
||||
const result: OllamaMessage[] = [
|
||||
...systemMsgs,
|
||||
...head,
|
||||
...alreadyCompressed,
|
||||
summaryMsg,
|
||||
...tail
|
||||
];
|
||||
|
||||
const beforeTokens = estimateTokens(messages.map(m => m.content || '').join(''));
|
||||
const afterTokens = estimateTokens(result.map(m => m.content || '').join(''));
|
||||
|
||||
logSuccess(`上下文压缩完成: ${messages.length} 条 → ${result.length} 条, tokens: ${beforeTokens} → ${afterTokens}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将较早的消息每 batchSize 条压缩为一段摘要(快速文本截取,不调用 LLM)
|
||||
*/
|
||||
function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): OllamaMessage[] {
|
||||
const summaries: OllamaMessage[] = [];
|
||||
@@ -111,7 +253,8 @@ function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): O
|
||||
const summary = createQuickSummary(batch);
|
||||
summaries.push({
|
||||
role: 'system',
|
||||
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`
|
||||
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`,
|
||||
compressed: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,12 +270,10 @@ function createQuickSummary(messages: OllamaMessage[]): string {
|
||||
for (const msg of messages) {
|
||||
const role = msg.role === 'user' ? '用户' : 'AI';
|
||||
const content = msg.content || '';
|
||||
// 取前 100 字符作为摘要
|
||||
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
||||
if (preview.trim()) {
|
||||
parts.push(`${role}: ${preview}`);
|
||||
}
|
||||
// 如果有工具调用,记录工具名
|
||||
if (msg.tool_calls?.length) {
|
||||
const toolNames = msg.tool_calls.map(t => t.function.name).join(', ');
|
||||
parts.push(` [工具: ${toolNames}]`);
|
||||
@@ -149,7 +290,6 @@ function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaM
|
||||
let totalTokens = 0;
|
||||
const result: OllamaMessage[] = [];
|
||||
|
||||
// 从后往前添加(优先保留最近的消息)
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
const msgTokens = estimateTokens(msg.content || '') +
|
||||
@@ -164,7 +304,7 @@ function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaM
|
||||
}
|
||||
|
||||
if (totalTokens + msgTokens > maxTokens && result.length > 2) {
|
||||
break; // 已到限制,保留至少 system + 1 条消息
|
||||
break;
|
||||
}
|
||||
|
||||
totalTokens += msgTokens;
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Level 0 简短摘要(~100 tokens),用于索引展示 */
|
||||
summary: string;
|
||||
trigger_keywords: string | null;
|
||||
tool_chain: string; // JSON: ToolChainStep[]
|
||||
success_count: number;
|
||||
@@ -83,10 +85,13 @@ export async function extractSkillsFromToolRecords(
|
||||
const name = generateSkillName(chain, userMessage);
|
||||
const description = generateSkillDescription(chain, sessionTitle);
|
||||
|
||||
const summary = generateSkillSummary(chain, userMessage);
|
||||
|
||||
const skill: Skill = {
|
||||
id: `skill_${generateId()}`,
|
||||
name,
|
||||
description,
|
||||
summary,
|
||||
trigger_keywords: keywords,
|
||||
tool_chain: JSON.stringify(chain),
|
||||
success_count: 1,
|
||||
@@ -157,28 +162,21 @@ export async function matchSkills(userMessage: string, limit = 3): Promise<Skill
|
||||
}
|
||||
|
||||
/**
|
||||
* 从匹配的技能构建 system prompt 上下文
|
||||
* 从匹配的技能构建 system prompt 上下文(Level 0:仅名称 + 描述索引)
|
||||
* 完整工具链通过 skill_view 工具按需加载(Level 1)
|
||||
*/
|
||||
export function buildSkillContext(skills: Skill[]): string {
|
||||
if (skills.length === 0) return '';
|
||||
|
||||
const parts = skills.map(skill => {
|
||||
let chain: ToolChainStep[] = [];
|
||||
try { chain = JSON.parse(skill.tool_chain); } catch { return ''; }
|
||||
|
||||
const steps = chain.map((s, i) =>
|
||||
` ${i + 1}. ${s.name}(${s.description})${s.args_hint ? ` — 参数: ${s.args_hint}` : ''}`
|
||||
).join('\n');
|
||||
|
||||
const total = skill.success_count + skill.fail_count;
|
||||
const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100;
|
||||
|
||||
return `【技能】${skill.name}\n${skill.description}\n成功执行 ${skill.success_count} 次(成功率 ${rate}%)\n工具链:\n${steps}`;
|
||||
return `• ${skill.name} — ${skill.description}(成功率 ${rate}%)`;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (parts.length === 0) return '';
|
||||
|
||||
return `【可用技能提示 — 以下技能曾成功完成过类似任务,可作为参考】\n\n${parts.join('\n\n')}\n\n提示:如果当前任务与某个技能匹配,可以参考其工具链步骤,但根据实际情况调整参数。`;
|
||||
return `【可用技能 — 以下技能曾成功完成过类似任务】\n${parts.join('\n')}\n\n提示:使用 skill_view 工具查看某个技能的完整工具链和参数提示。`;
|
||||
}
|
||||
|
||||
// ── 辅助函数 ──
|
||||
@@ -269,12 +267,20 @@ function generateSkillDescription(chain: ToolChainStep[], sessionTitle: string):
|
||||
return `从会话「${sessionTitle}」中提取。步骤:${steps}`;
|
||||
}
|
||||
|
||||
/** 生成技能简短摘要(Level 0,~100 tokens) */
|
||||
function generateSkillSummary(chain: ToolChainStep[], userMessage: string): string {
|
||||
const toolNames = chain.map(s => s.name).join(' → ');
|
||||
const msgPreview = userMessage.slice(0, 30).replace(/[\r\n]/g, ' ');
|
||||
return `工具链: ${toolNames}。场景: ${msgPreview}`;
|
||||
}
|
||||
|
||||
// ── 渐进式技能加载 ──
|
||||
|
||||
/** 列出所有技能(渐进式 Level 0:只返回名称、描述、成功率) */
|
||||
/** 列出所有技能(渐进式 Level 0:只返回名称、描述摘要、成功率) */
|
||||
export async function listSkills(): Promise<Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
summary: string;
|
||||
success_rate: string;
|
||||
chain_preview: string;
|
||||
}>> {
|
||||
@@ -292,6 +298,7 @@ export async function listSkills(): Promise<Array<{
|
||||
return {
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
summary: skill.summary || skill.description,
|
||||
success_rate: `${rate}%`,
|
||||
chain_preview: chainPreview
|
||||
};
|
||||
|
||||
Vendored
+5
-1
@@ -8,6 +8,8 @@ export interface OllamaMessage {
|
||||
reasoning_content?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
tool_name?: string;
|
||||
/** 标记此消息为 LLM 压缩摘要生成 */
|
||||
compressed?: boolean;
|
||||
}
|
||||
|
||||
export interface OllamaChatParams {
|
||||
@@ -94,7 +96,7 @@ export interface FileContent {
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant';
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
model?: string;
|
||||
@@ -107,6 +109,8 @@ export interface ChatMessage {
|
||||
_fileContents?: FileContent[];
|
||||
stopped?: boolean;
|
||||
toolCalls?: ToolCallRecord[];
|
||||
/** 标记此消息为 LLM 压缩摘要生成 */
|
||||
compressed?: boolean;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
|
||||
Reference in New Issue
Block a user