实现客户端部分的 prompt 支持

This commit is contained in:
锦恢 2025-05-06 21:53:43 +08:00
parent cf16cff7cf
commit 5a2a699a51
15 changed files with 173 additions and 187 deletions

View File

@ -52,7 +52,7 @@
<footer class="chat-footer" ref="footerRef"> <footer class="chat-footer" ref="footerRef">
<div class="input-area"> <div class="input-area">
<div class="input-wrapper"> <div class="input-wrapper">
<Setting :tabId="tabId" /> <Setting :tabId="tabId" v-model="userInput" />
<KCuteTextarea <KCuteTextarea
v-model="userInput" v-model="userInput"

View File

@ -1,37 +1,74 @@
<template> <template>
<el-tooltip :content="t('context-length')" placement="top"> <el-tooltip :content="t('prompts')" placement="top">
<div class="setting-button" @click="showContextLengthDialog = true"> <div class="setting-button" @click="showChoosePrompt = true">
<span class="iconfont icon-length"></span> <span class="iconfont icon-chat"></span>
<span class="value-badge">{{ tabStorage.settings.contextLength }}</span>
</div> </div>
</el-tooltip> </el-tooltip>
<!-- 上下文长度设置 - 改为滑块形式 --> <!-- 上下文长度设置 - 改为滑块形式 -->
<el-dialog v-model="showContextLengthDialog" :title="t('context-length') + ' ' + tabStorage.settings.contextLength" <el-dialog v-model="showChoosePrompt" :title="t('prompts')" width="400px">
width="400px">
<div class="slider-container"> <div class="prompt-template-container-scrollbar" v-if="!selectPrompt">
<el-slider v-model="tabStorage.settings.contextLength" :min="1" :max="99" :step="1" /> <PromptTemplates
<div class="slider-tips"> :tab-id="-1"
<span> 1: {{ t('single-dialog') }}</span> @prompt-selected="prompt => selectPrompt = prompt"
<span> >1: {{ t('multi-dialog') }}</span> />
</div> </div>
<div v-else>
<PromptReader
:tab-id="-1"
:current-prompt-name="selectPrompt!.name"
@prompt-get-response="msg => whenGetPromptResponse(msg)"
/>
</div> </div>
<template #footer> <template #footer>
<el-button @click="showContextLengthDialog = false">{{ t("cancel") }}</el-button> <el-button v-if="selectPrompt" @click="selectPrompt = undefined;">{{ t('return') }}</el-button>
<el-button @click="showChoosePrompt = false; selectPrompt = undefined;">{{ t("cancel") }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, inject, ref } from 'vue'; import { inject, ref, defineProps, PropType, defineEmits } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat'; import { ChatStorage } from '../chat';
import { PromptsGetResponse, PromptTemplate } from '@/hook/type';
import PromptTemplates from '../../prompt/prompt-templates.vue';
import PromptReader from '../../prompt/prompt-reader.vue';
import { ElMessage } from 'element-plus';
const { t } = useI18n(); const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage; const tabStorage = inject('tabStorage') as ChatStorage;
const showContextLengthDialog = ref(false); const props = defineProps({
modelValue: {
type: String,
required: true
}
});
const emits = defineEmits([ 'update:modelValue' ]);
let selectPrompt = ref<PromptTemplate | undefined>(undefined);
const showChoosePrompt = ref(false);
function whenGetPromptResponse(msg: PromptsGetResponse) {
try {
const content = msg.messages[0].content;
if (content) {
emits('update:modelValue', props.modelValue + content);
}
showChoosePrompt.value = false;
} catch (error) {
ElMessage.error((error as Error).message);
}
}
</script> </script>

View File

@ -3,6 +3,7 @@
<Model /> <Model />
<SystemPrompt /> <SystemPrompt />
<ToolUse /> <ToolUse />
<Prompt v-model="val" />
<Websearch /> <Websearch />
<Temperature /> <Temperature />
<ContextLength /> <ContextLength />
@ -10,7 +11,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, provide } from 'vue'; import { defineProps, provide, ref } from 'vue';
import { llmManager } from '@/views/setting/llm'; import { llmManager } from '@/views/setting/llm';
import { tabs } from '../../panel'; import { tabs } from '../../panel';
import type { ChatSetting, ChatStorage } from '../chat'; import type { ChatSetting, ChatStorage } from '../chat';
@ -18,17 +19,24 @@ import type { ChatSetting, ChatStorage } from '../chat';
import Model from './model.vue'; import Model from './model.vue';
import SystemPrompt from './system-prompt.vue'; import SystemPrompt from './system-prompt.vue';
import ToolUse from './tool-use.vue'; import ToolUse from './tool-use.vue';
import Prompt from './prompt.vue';
import Websearch from './websearch.vue'; import Websearch from './websearch.vue';
import Temperature from './temperature.vue'; import Temperature from './temperature.vue';
import ContextLength from './context-length.vue'; import ContextLength from './context-length.vue';
const props = defineProps({ const props = defineProps({
modelValue: {
type: String,
required: true
},
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
} }
}); });
const val = ref('');
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting }; const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting };

View File

@ -32,12 +32,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue'; import { defineComponent, defineProps, defineEmits, watch, ref, computed, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { parsePromptTemplate, promptsManager, PromptStorage } from './prompts'; import { promptsManager, PromptStorage } from './prompts';
import { CasualRestAPI, PromptsGetResponse } from '@/hook/type'; import { PromptsGetResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp'; import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
@ -49,11 +49,26 @@ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
},
currentPromptName: {
type: String,
required: false
} }
}); });
const tab = tabs.content[props.tabId]; const emits = defineEmits(['prompt-get-response']);
const tabStorage = tab.storage as PromptStorage;
let tabStorage: PromptStorage;
if (props.tabId >= 0) {
tabStorage = tabs.content[props.tabId].storage as PromptStorage;
} else {
tabStorage = reactive({
currentPromptName: props.currentPromptName || '',
formData: {},
lastPromptGetResponse: undefined
});
}
if (!tabStorage.formData) { if (!tabStorage.formData) {
tabStorage.formData = {}; tabStorage.formData = {};
@ -108,7 +123,6 @@ const initFormData = () => {
newSchemaDataForm[param.name] = tabStorage.formData[param.name]; newSchemaDataForm[param.name] = tabStorage.formData[param.name];
} }
}); });
} }
const resetForm = () => { const resetForm = () => {
@ -116,24 +130,25 @@ const resetForm = () => {
responseData.value = undefined; responseData.value = undefined;
} }
function handleSubmit() { async function handleSubmit() {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
bridge.addCommandListener('prompts/get', (data: CasualRestAPI<PromptsGetResponse>) => { const { code, msg } = await bridge.commandRequest('prompts/get', {
tabStorage.lastPromptGetResponse = data.msg; promptId: currentPrompt.value.name,
}, { once: true }); args: JSON.parse(JSON.stringify(tabStorage.formData))
bridge.postMessage({
command: 'prompts/get',
data: { promptId: currentPrompt.value.name, args: JSON.parse(JSON.stringify(tabStorage.formData)) }
}); });
tabStorage.lastPromptGetResponse = msg;
emits('prompt-get-response', msg);
} }
if (props.tabId >= 0) {
watch(() => tabStorage.currentPromptName, () => { watch(() => tabStorage.currentPromptName, () => {
initFormData(); initFormData();
resetForm(); resetForm();
}, { immediate: true }); }, { immediate: true });
}
</script> </script>
<style> <style>

View File

@ -12,7 +12,7 @@
<div class="prompt-template-container"> <div class="prompt-template-container">
<div <div
class="item" class="item"
:class="{ 'active': tabStorage.currentPromptName === template.name }" :class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
v-for="template of promptsManager.templates" v-for="template of promptsManager.templates"
:key="template.name" :key="template.name"
@click="handleClick(template)" @click="handleClick(template)"
@ -28,7 +28,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type'; import { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue'; import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { promptsManager, PromptStorage } from './prompts'; import { promptsManager, PromptStorage } from './prompts';
import { tabs } from '../panel'; import { tabs } from '../panel';
@ -44,8 +44,20 @@ const props = defineProps({
} }
}); });
const emits = defineEmits([ 'prompt-selected' ]);
let tabStorage: PromptStorage;
if (props.tabId >= 0) {
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage; tabStorage = tab.storage as PromptStorage;
} else {
tabStorage = reactive({
currentPromptName: '',
formData: {},
lastPromptGetResponse: undefined
});
}
function reloadPrompts(option: { first: boolean }) { function reloadPrompts(option: { first: boolean }) {
bridge.postMessage({ bridge.postMessage({
@ -62,9 +74,11 @@ function reloadPrompts(option: { first: boolean }) {
} }
} }
function handleClick(template: PromptTemplate) { function handleClick(prompt: PromptTemplate) {
tabStorage.currentPromptName = template.name; tabStorage.currentPromptName = prompt.name;
tabStorage.lastPromptGetResponse = undefined; tabStorage.lastPromptGetResponse = undefined;
emits('prompt-selected', prompt);
} }
let commandCancel: (() => void); let commandCancel: (() => void);

View File

@ -151,5 +151,6 @@
"generate-answer": "جارٍ إنشاء الإجابة", "generate-answer": "جارٍ إنشاء الإجابة",
"choose-presetting": "اختر الإعداد المسبق", "choose-presetting": "اختر الإعداد المسبق",
"cwd": "دليل التنفيذ", "cwd": "دليل التنفيذ",
"mcp-server-timeout": "أطول وقت لاستدعاء أداة MCP" "mcp-server-timeout": "أطول وقت لاستدعاء أداة MCP",
"return": "عودة"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "Antwort wird generiert", "generate-answer": "Antwort wird generiert",
"choose-presetting": "Voreinstellung auswählen", "choose-presetting": "Voreinstellung auswählen",
"cwd": "Ausführungsverzeichnis", "cwd": "Ausführungsverzeichnis",
"mcp-server-timeout": "Maximale Aufrufzeit des MCP-Tools" "mcp-server-timeout": "Maximale Aufrufzeit des MCP-Tools",
"return": "Zurück"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "Generating answer", "generate-answer": "Generating answer",
"choose-presetting": "Select preset", "choose-presetting": "Select preset",
"cwd": "Execution directory", "cwd": "Execution directory",
"mcp-server-timeout": "Maximum call time of MCP tool" "mcp-server-timeout": "Maximum call time of MCP tool",
"return": "Back"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "Génération de la réponse", "generate-answer": "Génération de la réponse",
"choose-presetting": "Sélectionner un préréglage", "choose-presetting": "Sélectionner un préréglage",
"cwd": "Répertoire d'exécution", "cwd": "Répertoire d'exécution",
"mcp-server-timeout": "Temps d'appel maximum de l'outil MCP" "mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "Retour"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "回答を生成中", "generate-answer": "回答を生成中",
"choose-presetting": "プリセットを選択", "choose-presetting": "プリセットを選択",
"cwd": "実行ディレクトリ", "cwd": "実行ディレクトリ",
"mcp-server-timeout": "MCPツールの最大呼び出し時間" "mcp-server-timeout": "MCPツールの最大呼び出し時間",
"return": "戻る"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "답변 생성 중", "generate-answer": "답변 생성 중",
"choose-presetting": "프리셋 선택", "choose-presetting": "프리셋 선택",
"cwd": "실행 디렉터리", "cwd": "실행 디렉터리",
"mcp-server-timeout": "MCP 도구 최대 호출 시간" "mcp-server-timeout": "MCP 도구 최대 호출 시간",
"return": "돌아가기"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "Генерация ответа", "generate-answer": "Генерация ответа",
"choose-presetting": "Выбрать预设", "choose-presetting": "Выбрать预设",
"cwd": "Каталог выполнения", "cwd": "Каталог выполнения",
"mcp-server-timeout": "Максимальное время вызова инструмента MCP" "mcp-server-timeout": "Максимальное время вызова инструмента MCP",
"return": "Назад"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "正在生成答案", "generate-answer": "正在生成答案",
"choose-presetting": "选择预设", "choose-presetting": "选择预设",
"cwd": "执行目录", "cwd": "执行目录",
"mcp-server-timeout": "MCP工具最长调用时间" "mcp-server-timeout": "MCP工具最长调用时间",
"return": "返回"
} }

View File

@ -151,5 +151,6 @@
"generate-answer": "正在生成答案", "generate-answer": "正在生成答案",
"choose-presetting": "選擇預設", "choose-presetting": "選擇預設",
"cwd": "執行目錄", "cwd": "執行目錄",
"mcp-server-timeout": "MCP工具最長調用時間" "mcp-server-timeout": "MCP工具最長調用時間",
"return": "返回"
} }

View File

@ -7,128 +7,9 @@
"type": "blank", "type": "blank",
"componentIndex": 3, "componentIndex": 3,
"storage": { "storage": {
"messages": [ "messages": [],
{
"role": "user",
"content": "请问杭州的天气",
"extraInfo": {
"created": 1745258229256,
"serverName": "deepseek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_0_57ad3eab-cd9d-4403-bdf0-31d00b83b98c",
"index": 0,
"type": "function",
"function": {
"name": "get_weather_by_city_code",
"arguments": "{\"city_code\":101210101}"
}
}
],
"extraInfo": {
"created": 1745258234470,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 570,
"completion_tokens": 26,
"total_tokens": 596,
"prompt_tokens_details": {
"cached_tokens": 512
},
"prompt_cache_hit_tokens": 512,
"prompt_cache_miss_tokens": 58
}
}
},
{
"role": "tool",
"tool_call_id": "call_0_57ad3eab-cd9d-4403-bdf0-31d00b83b98c",
"content": "[{\"type\":\"text\",\"text\":\"CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='21', wd='', ws='', sd='92%', aqi='13', weather='阴')\"}]",
"extraInfo": {
"created": 1745258234522,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 570,
"completion_tokens": 26,
"total_tokens": 596,
"prompt_tokens_details": {
"cached_tokens": 512
},
"prompt_cache_hit_tokens": 512,
"prompt_cache_miss_tokens": 58
}
}
},
{
"role": "assistant",
"content": "杭州的天气信息如下:\n\n- 城市:杭州\n- 温度21°C\n- 天气状况:阴\n- 湿度92%\n- 空气质量指数 (AQI)13优秀\n\n天气较为舒适适合出行",
"extraInfo": {
"created": 1745258240571,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 660,
"completion_tokens": 52,
"total_tokens": 712,
"prompt_tokens_details": {
"cached_tokens": 576
},
"prompt_cache_hit_tokens": 576,
"prompt_cache_miss_tokens": 84
}
}
},
{
"role": "user",
"content": "再试一次",
"extraInfo": {
"created": 1745495349299,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_jpm4v1e92v4nq650wlqkhm56",
"index": 0,
"type": "function",
"function": {
"name": "get_weather_by_city_code",
"arguments": "{\"city_code\":101210101}"
}
}
],
"extraInfo": {
"created": 1745495350097,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "tool",
"tool_call_id": "call_jpm4v1e92v4nq650wlqkhm56",
"content": "[{\"type\":\"text\",\"text\":\"CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='18.3', wd='', ws='', sd='89%', aqi='57', weather='阴')\"}]",
"extraInfo": {
"created": 1745495350319,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "杭州的最新天气信息如下:\n\n- 城市:杭州\n- 温度18.3°C\n- 天气状况:阴\n- 湿度89%\n- 空气质量指数 (AQI)57良好\n\n天气较为凉爽适合外出活动",
"extraInfo": {
"created": 1745495352693,
"serverName": "Huoshan DeepSeek"
}
}
],
"settings": { "settings": {
"modelIndex": 0, "modelIndex": 15,
"enableTools": [ "enableTools": [
{ {
"name": "add", "name": "add",
@ -158,29 +39,51 @@
], ],
"enableWebSearch": false, "enableWebSearch": false,
"temperature": 0.7, "temperature": 0.7,
"contextLength": 10, "contextLength": 20,
"systemPrompt": "" "systemPrompt": ""
} }
} }
}, },
{ {
"name": "工具", "name": "交互测试",
"icon": "icon-tool", "icon": "icon-robot",
"type": "tool", "type": "blank",
"componentIndex": 2, "componentIndex": 3,
"storage": { "storage": {
"currentToolName": "get_weather_by_city_code", "messages": [],
"formData": { "settings": {
"city_code": 0 "modelIndex": 15,
}, "enableTools": [
"lastToolCallResponse": {
"content": [
{ {
"type": "text", "name": "add",
"text": "CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='20.3', wd='', ws='', sd='89%', aqi='38', weather='阴')" "description": "对两个数字进行实数域的加法",
"enabled": true
},
{
"name": "multiply",
"description": "对两个数字进行实数域的乘法运算",
"enabled": true
},
{
"name": "is_even",
"description": "判断一个整数是否为偶数",
"enabled": true
},
{
"name": "capitalize",
"description": "将字符串首字母大写",
"enabled": true
},
{
"name": "get_weather_by_city_code",
"description": "根据城市天气预报的城市编码 (int),获取指定城市的天气信息",
"enabled": true
} }
], ],
"isError": false "enableWebSearch": false,
"temperature": 0.7,
"contextLength": 20,
"systemPrompt": ""
} }
} }
} }