完成全部配置的保存

This commit is contained in:
huangzhelong.byte 2025-04-05 17:34:22 +08:00
parent 05080e2b72
commit 3d372dafae
22 changed files with 335 additions and 203 deletions

View File

@ -15,6 +15,7 @@ import { setDefaultCss } from './hook/css';
import { pinkLog } from './views/setting/util';
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge';
import { connectionArgs, connectionMethods, connectionResult, doConnect } from './views/connect/connection';
import { loadSetting } from './hook/setting';
const bridge = useMessageBridge();
@ -25,21 +26,10 @@ bridge.addCommandListener('hello', data => {
}, { once: true });
//
const sendPing = () => {
bridge.postMessage({
command: 'ping',
data: { timestamp: Date.now() }
});
};
onMounted(() => {
// css
setDefaultCss();
//
document.addEventListener('click', () => {
Connection.showPanel = false;
});
@ -58,6 +48,9 @@ onMounted(() => {
}, { once: true });
setTimeout(() => {
//
loadSetting();
doConnect();
}, 200);
}

View File

@ -13,7 +13,7 @@ export type CommandHandler = (data: any) => void;
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
interface AddCommandListenerOption {
once: boolean // 只调用一次就销毁
once: boolean // 只调用一次就销毁
}
class MessageBridge {
@ -97,19 +97,37 @@ class MessageBridge {
}
/**
* @description
* @returns
* @description
* @example
* // 基本用法(持久监听)
* const removeListener = bridge.addCommandListener('message', (data) => {
* console.log('收到消息:', data.msg.text);
* }, { once: false });
*
* // 稍后取消监听
* removeListener();
*
* @example
* // 一次性监听(自动移除)
* bridge.addCommandListener('connect', (data) => {
* const { code, msg } = data;
* console.log(`连接结果: ${code === 200 ? '成功' : '失败'}`);
* }, { once: true });
*/
public addCommandListener(command: string, commandHandler: CommandHandler, option: AddCommandListenerOption) {
public addCommandListener(
command: string,
commandHandler: CommandHandler,
option: AddCommandListenerOption
): () => boolean {
if (!this.handlers.has(command)) {
this.handlers.set(command, new Set<CommandHandler>());
}
const commandHandlers = this.handlers.get(command)!;
const wrapperCommandHandler = option.once ? (data: any) => {
commandHandler(data);
commandHandlers.delete(wrapperCommandHandler);
} : commandHandler;
const wrapperCommandHandler = option.once ? (data: any) => {
commandHandler(data);
commandHandlers.delete(wrapperCommandHandler);
} : commandHandler;
commandHandlers.add(wrapperCommandHandler);
return () => commandHandlers.delete(wrapperCommandHandler);

View File

@ -12,7 +12,7 @@
>
<span>
<span :class="`iconfont ${tab.icon}`"></span>
<span>{{ tab.name }}</span>
<span class="tab-name">{{ tab.name }}</span>
</span>
<span
class="iconfont icon-close"
@ -33,8 +33,6 @@
<div class="main-panel">
<router-view />
</div>
</div>
</template>
@ -83,6 +81,7 @@ defineComponent({ name: 'main-panel' });
}
.tabs-container .tab {
white-space: nowrap;
margin: 5px;
font-size: 13px;
width: 120px;
@ -101,6 +100,12 @@ defineComponent({ name: 'main-panel' });
align-items: center;
}
.tabs-container .tab .tab-name {
max-width: 70px;
overflow: hidden;
text-overflow: ellipsis;
}
.tabs-container .tab:hover {
background-color: var(--input-active-background);
}

View File

@ -4,46 +4,61 @@ import Resource from './resource/index.vue';
import Chat from './chat/index.vue';
import Prompt from './prompt/index.vue';
import Tool from './tool/index.vue';
import I18n from '@/i18n/index';
const { t } = I18n.global;
interface Tab {
name: string;
icon: string;
type: string;
component: any;
storage: Record<string, any>;
component: any;
storage: Record<string, any>;
}
export const debugModes = [
Resource, Prompt, Tool, Chat
Resource, Prompt, Tool, Chat
]
// TODO: 实现对于 tabs 这个数据的可持久化
export const tabs = reactive({
export const tabs = reactive<{
content: Tab[]
activeIndex: number
activeTab: Tab
}>({
content: [
{
name: '空白测试 1',
icon: 'icon-blank',
type: 'blank',
component: undefined,
storage: {}
}
] as Tab[],
createTab('blank', 1)
],
activeIndex: 0,
get activeTab() {
return this.content[this.activeIndex];
}
get activeTab() {
return this.content[this.activeIndex];
}
});
let tabCounter = 1;
export function addNewTab() {
const newTab = {
name: `空白测试 ${++ tabCounter}`,
function createTab(type: string, index: number): Tab {
let customName: string | null = null;
return {
get name() {
if (customName !== null) {
return customName;
}
return t('blank-test') + ` ${index}`;
},
set name(value: string) {
customName = value; // 允许外部修改 name
},
icon: 'icon-blank',
type: 'blank',
component: undefined,
storage: {}
type,
component: undefined,
storage: {},
};
}
export function addNewTab() {
const newTab = createTab('blank', ++tabCounter);
tabs.content.push(newTab);
tabs.activeIndex = tabs.content.length - 1;
}

View File

@ -3,7 +3,7 @@
<div class="left">
<h2>
<span class="iconfont icon-file"></span>
资源模块
{{ t("resources") + t("module") }}
</h2>
<h3><code>resources/templates/list</code></h3>
@ -26,10 +26,13 @@
<script setup lang="ts">
import { defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
import ResourceTemplates from './resource-templates.vue';
import ResourceReader from './resouce-reader.vue';
import ResourceLogger from './resource-logger.vue';
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,

View File

@ -1,14 +1,48 @@
import { useMessageBridge } from "@/api/message-bridge";
import { llmManager, llms } from "@/views/setting/llm";
import { pinkLog } from "@/views/setting/util";
import I18n from '@/i18n/index';
export function loadSetting() {
const bridge = useMessageBridge();
bridge.addCommandListener('setting/load', () => {
bridge.addCommandListener('setting/load', data => {
const persistConfig = data.msg;
llmManager.currentModelIndex = persistConfig.MODEL_INDEX;
I18n.global.locale.value = persistConfig.LANG;
persistConfig.LLM_INFO.forEach((element: any) => {
llms.push(element);
});
}, { once: true });
bridge.postMessage({
command: 'setting/load'
});
}
export function saveSetting() {
export function saveSetting(saveHandler?: () => void) {
const bridge = useMessageBridge();
const saveConfig = {
MODEL_INDEX: llmManager.currentModelIndex,
LLM_INFO: JSON.parse(JSON.stringify(llms)),
LANG: I18n.global.locale.value
};
bridge.addCommandListener('setting/save', data => {
const saveStatusCode = data.code;
pinkLog('配置保存状态:' + saveStatusCode);
if (saveHandler) {
saveHandler();
}
}, { once: true });
bridge.postMessage({
command: 'setting/save',
data: saveConfig
});
}

View File

@ -109,9 +109,12 @@
"reset": "إعادة تعيين",
"read-resource": "قراءة الموارد",
"enter": "إدخال",
"refresh": "تحديث",
"finish-refresh": "تم التحديث",
"blank-test": "اختبار فارغ",
"connect.appearance.reconnect": "إعادة الاتصال",
"connect.appearance.connect": "اتصال",
"response": "الاستجابة",
"read-prompt": "استخراج الكلمات الرئيسية",
"execute-tool": "تشغيل الأداة"
"refresh": "تحديث",
"read-prompt": "قراءة المطالبة",
"execute-tool": "تشغيل",
"save": "حفظ"
}

View File

@ -109,9 +109,12 @@
"reset": "Zurücksetzen",
"read-resource": "Ressourcen lesen",
"enter": "Eingabe",
"refresh": "Aktualisieren",
"finish-refresh": "Aktualisierung abgeschlossen",
"blank-test": "Leertest",
"connect.appearance.reconnect": "Neuverbindung",
"connect.appearance.connect": "Verbindung",
"response": "Antwort",
"read-prompt": "Stichwörter extrahieren",
"execute-tool": "Werkzeug ausführen"
"refresh": "Aktualisieren",
"read-prompt": "Prompt lesen",
"execute-tool": "Ausführen",
"save": "Speichern"
}

View File

@ -109,9 +109,12 @@
"reset": "Reset",
"read-resource": "Read resources",
"enter": "Input",
"refresh": "Refresh",
"finish-refresh": "Refresh completed",
"blank-test": "Blank test",
"connect.appearance.reconnect": "Reconnect",
"connect.appearance.connect": "Connection",
"response": "Response",
"read-prompt": "Extract keywords",
"execute-tool": "Run tool"
"refresh": "Refresh",
"read-prompt": "Read prompt",
"execute-tool": "Run",
"save": "Save"
}

View File

@ -109,9 +109,12 @@
"reset": "Réinitialiser",
"read-resource": "Lire les ressources",
"enter": "Entrée",
"refresh": "Rafraîchir",
"finish-refresh": "Actualisation terminée",
"blank-test": "Test vide",
"connect.appearance.reconnect": "Reconnexion",
"connect.appearance.connect": "Connexion",
"response": "Réponse",
"read-prompt": "Extraire des mots-clés",
"execute-tool": "Exécuter l'outil"
"refresh": "Rafraîchir",
"read-prompt": "Lire l'invite",
"execute-tool": "Exécuter",
"save": "Enregistrer"
}

View File

@ -109,9 +109,12 @@
"reset": "リセット",
"read-resource": "リソースを読み込む",
"enter": "入力",
"refresh": "更新",
"finish-refresh": "更新が完了しました",
"blank-test": "空白テスト",
"connect.appearance.reconnect": "再接続",
"connect.appearance.connect": "接続",
"response": "応答",
"read-prompt": "キーワードを抽出",
"execute-tool": "ツールを実行"
"refresh": "更新",
"read-prompt": "プロンプトを読み取る",
"execute-tool": "実行",
"save": "保存"
}

View File

@ -109,9 +109,12 @@
"reset": "재설정",
"read-resource": "리소스 읽기",
"enter": "입력",
"refresh": "새로 고침",
"finish-refresh": "새로 고침 완료",
"blank-test": "빈 테스트",
"connect.appearance.reconnect": "재연결",
"connect.appearance.connect": "연결",
"response": "응답",
"read-prompt": "키워드 추출",
"execute-tool": "도구 실행"
"refresh": "새로 고침",
"read-prompt": "프롬프트 읽기",
"execute-tool": "실행",
"save": "저장"
}

View File

@ -109,9 +109,12 @@
"reset": "Сброс",
"read-resource": "Чтение ресурсов",
"enter": "Ввод",
"refresh": "Обновить",
"finish-refresh": "Обновление завершено",
"blank-test": "Пустой тест",
"connect.appearance.reconnect": "Переподключение",
"connect.appearance.connect": "Соединение",
"response": "Ответ",
"read-prompt": "Извлечь ключевые слова",
"execute-tool": "Запустить инструмент"
"refresh": "Обновить",
"read-prompt": "Чтение подсказки",
"execute-tool": "Запуск",
"save": "Сохранить"
}

View File

@ -109,9 +109,12 @@
"reset": "重置",
"read-resource": "读取资源",
"enter": "输入",
"refresh": "刷新",
"finish-refresh": "刷新完成",
"blank-test": "空白测试",
"connect.appearance.reconnect": "重新连接",
"connect.appearance.connect": "连接",
"response": "响应",
"read-prompt": "提取提词",
"execute-tool": "运行工具"
"refresh": "刷新",
"read-prompt": "读取 prompt",
"execute-tool": "运行",
"save": "保存"
}

View File

@ -109,9 +109,12 @@
"reset": "重置",
"read-resource": "讀取資源",
"enter": "輸入",
"refresh": "重新整理",
"finish-refresh": "刷新完成",
"blank-test": "空白測試",
"connect.appearance.reconnect": "重新連線",
"connect.appearance.connect": "連接",
"response": "響應",
"read-prompt": "提取關鍵詞",
"execute-tool": "執行工具"
"refresh": "重新整理",
"read-prompt": "讀取提示",
"execute-tool": "執行",
"save": "儲存"
}

View File

@ -21,7 +21,7 @@
<script setup lang="ts">
import { debugModes, tabs } from '@/components/main-panel/panel';
import { defineComponent, markRaw } from 'vue';
import { defineComponent, markRaw, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { connectionResult } from '../connect/connection';
import { ElMessage } from 'element-plus';
@ -33,22 +33,22 @@ const { t } = useI18n();
const debugOptions = [
{
icon: 'icon-file',
name: t("resources"),
name: computed(() => t("resources")),
ident: 'resources'
},
{
icon: 'icon-chat',
name: t("prompts"),
name: computed(() => t("prompts")),
ident: 'prompts'
},
{
icon: 'icon-tool',
name: t("tools"),
name: computed(() => t("tools")),
ident: 'tool'
},
{
icon: 'icon-robot',
name: t("interaction-test"),
name: computed(() => t("interaction-test")),
ident: 'interaction'
}
];
@ -60,7 +60,9 @@ function chooseDebugMode(index: number) {
const activeTab = tabs.activeTab;
activeTab.component = markRaw(debugModes[index]);
activeTab.icon = debugOptions[index].icon;
activeTab.name = debugOptions[index].name;
// activeTab tab name
activeTab.name = debugOptions[index].name as any;
} else {
const message = t('warning.click-to-connect')
.replace('$1', t('connect'));

View File

@ -34,7 +34,9 @@
<span class="option-title">{{ t('model') }}</span>
</span>
<div style="width: 160px;">
<el-select name="language-setting" class="language-setting"
<el-select
v-if="llms[llmManager.currentModelIndex]"
name="language-setting" class="language-setting"
v-model="llms[llmManager.currentModelIndex].userModel"
@change="onmodelchange"
>
@ -54,6 +56,7 @@
</span>
<div style="width: 240px;">
<el-input
v-if="llms[llmManager.currentModelIndex]"
v-model="llms[llmManager.currentModelIndex].baseUrl"
placeholder="https://"
/>
@ -67,12 +70,21 @@
</span>
<div style="width: 240px;">
<el-input
v-if="llms[llmManager.currentModelIndex]"
v-model="llms[llmManager.currentModelIndex].userToken"
show-password
/>
</div>
</div>
</div>
<br>
<div class="setting-save-container">
<el-button type="primary" @click="saveLlmSetting">
{{ t('save') }}
</el-button>
</div>
</div>
</template>
@ -81,14 +93,25 @@
import { defineComponent } from 'vue';
import { llmManager, llms, onmodelchange } from './llm';
import { useI18n } from 'vue-i18n';
import { saveSetting } from '@/hook/setting';
import { ElMessage } from 'element-plus';
defineComponent({ name: 'api' });
const { t } = useI18n();
function saveLlmSetting() {
saveSetting(() => {
ElMessage({
message: '成功保存',
type: 'success'
});
});
}
</script>
<style>
.setting-save-container {
margin: 5px;
}
</style>

View File

@ -21,6 +21,7 @@
import { defineComponent, ref } from 'vue';
import { languageSetting } from './language';
import { useI18n } from 'vue-i18n';
import { saveSetting } from '@/hook/setting';
defineComponent({ name: 'appearance' });
@ -34,6 +35,8 @@ function onlanguagechange(code: string) {
currentLanguage.value = option.text;
}
// languageDialogShow.value = true;
saveSetting();
}
</script>

View File

@ -1,104 +1,13 @@
import { reactive } from 'vue';
import { pinkLog } from './util';
import { saveSetting } from '@/hook/setting';
export const llms = reactive([
{
id: 'deepseek',
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com/v1',
models: ['deepseek-chat', 'deepseek-coder', 'deepseek-math'],
icon: '/images/deepseek.com.ico',
provider: 'DeepSeek',
isOpenAICompatible: true,
description: '深度求索推出的大模型,擅长中文和代码',
website: 'https://www.deepseek.com',
userToken: '',
userModel: 'deepseek-chat'
},
{
id: 'openai',
name: 'OpenAI',
baseUrl: 'https://api.openai.com/v1',
models: ['gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'],
icon: '/images/openai.com.ico',
provider: 'OpenAI',
isOpenAICompatible: true,
description: 'OpenAI官方API',
website: 'https://openai.com',
userToken: '',
userModel: 'gpt-4-turbo'
},
{
id: 'mistral',
name: 'Mistral',
baseUrl: 'https://api.mistral.ai/v1',
models: ['mistral-tiny', 'mistral-small', 'mistral-medium'],
icon: '/images/mistral.ai.ico',
provider: 'Mistral AI',
isOpenAICompatible: true,
description: '欧洲开源大模型代表',
website: 'https://mistral.ai',
userToken: '',
userModel: 'mistral-tiny'
},
{
id: 'ollama',
name: 'Ollama (Local)',
baseUrl: 'http://localhost:11434/v1',
models: ['llama2', 'mistral', 'codellama'],
icon: '/images/ollama.png',
provider: 'Ollama',
isOpenAICompatible: true,
description: '本地运行的大模型',
website: 'https://ollama.com',
userToken: '',
userModel: 'llama2'
},
{
id: 'groq',
name: 'Groq',
baseUrl: 'https://api.groq.com/openai/v1',
models: ['mixtral-8x7b-32768', 'llama2-70b-4096'],
icon: '/images/grok.com.png',
provider: 'Groq',
isOpenAICompatible: true,
description: '超高速推理API',
website: 'https://groq.com',
userToken: '',
userModel: 'mixtral-8x7b-32768'
},
{
id: 'perplexity',
name: 'Perplexity',
baseUrl: 'https://api.perplexity.ai/v1',
models: ['pplx-7b-online', 'pplx-70b-online'],
icon: '/images/perplexity.ai.ico',
provider: 'Perplexity AI',
isOpenAICompatible: true,
description: '联网搜索增强的大模型',
website: 'https://www.perplexity.ai',
userToken: '',
userModel: 'pplx-7b-online'
},
{
id: 'kimi',
name: 'Kimi Chat',
baseUrl: 'https://api.moonshot.cn/v1',
models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'],
icon: '/images/kimichat.cn.png',
provider: '月之暗面 (Moonshot AI)',
isOpenAICompatible: true,
description: '支持超长上下文的中文大模型上下文窗口高达128K',
website: 'https://kimi.moonshot.cn',
userToken: '',
userModel: 'moonshot-v1-8k'
}
]);
export const llms = reactive<any[]>([]);
export const llmManager = reactive({
currentModelIndex: 0,
});
export function onmodelchange() {
console.log();
pinkLog('切换模型到:' + llms[llmManager.currentModelIndex].id);
}

View File

@ -1,4 +1,4 @@
import { reactive } from 'vue';
import { reactive, computed } from 'vue';
import I18n from '@/i18n/index';
@ -13,11 +13,11 @@ export const settingSections = reactive({
},
{
value: 'general',
label: t('general-setting')
label: computed(() => t('general-setting'))
},
{
value: 'appearance',
label: t('appearance-setting')
label: computed(() => t('appearance-setting'))
}
]
});

93
test/src/llm.ts Normal file
View File

@ -0,0 +1,93 @@
export const llms = [
{
id: 'deepseek',
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com/v1',
models: ['deepseek-chat', 'deepseek-coder', 'deepseek-math'],
icon: '/images/deepseek.com.ico',
provider: 'DeepSeek',
isOpenAICompatible: true,
description: '深度求索推出的大模型,擅长中文和代码',
website: 'https://www.deepseek.com',
userToken: '',
userModel: 'deepseek-chat'
},
{
id: 'openai',
name: 'OpenAI',
baseUrl: 'https://api.openai.com/v1',
models: ['gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'],
icon: '/images/openai.com.ico',
provider: 'OpenAI',
isOpenAICompatible: true,
description: 'OpenAI官方API',
website: 'https://openai.com',
userToken: '',
userModel: 'gpt-4-turbo'
},
{
id: 'mistral',
name: 'Mistral',
baseUrl: 'https://api.mistral.ai/v1',
models: ['mistral-tiny', 'mistral-small', 'mistral-medium'],
icon: '/images/mistral.ai.ico',
provider: 'Mistral AI',
isOpenAICompatible: true,
description: '欧洲开源大模型代表',
website: 'https://mistral.ai',
userToken: '',
userModel: 'mistral-tiny'
},
{
id: 'ollama',
name: 'Ollama (Local)',
baseUrl: 'http://localhost:11434/v1',
models: ['llama2', 'mistral', 'codellama'],
icon: '/images/ollama.png',
provider: 'Ollama',
isOpenAICompatible: true,
description: '本地运行的大模型',
website: 'https://ollama.com',
userToken: '',
userModel: 'llama2'
},
{
id: 'groq',
name: 'Groq',
baseUrl: 'https://api.groq.com/openai/v1',
models: ['mixtral-8x7b-32768', 'llama2-70b-4096'],
icon: '/images/grok.com.png',
provider: 'Groq',
isOpenAICompatible: true,
description: '超高速推理API',
website: 'https://groq.com',
userToken: '',
userModel: 'mixtral-8x7b-32768'
},
{
id: 'perplexity',
name: 'Perplexity',
baseUrl: 'https://api.perplexity.ai/v1',
models: ['pplx-7b-online', 'pplx-70b-online'],
icon: '/images/perplexity.ai.ico',
provider: 'Perplexity AI',
isOpenAICompatible: true,
description: '联网搜索增强的大模型',
website: 'https://www.perplexity.ai',
userToken: '',
userModel: 'pplx-7b-online'
},
{
id: 'kimi',
name: 'Kimi Chat',
baseUrl: 'https://api.moonshot.cn/v1',
models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'],
icon: '/images/kimichat.cn.png',
provider: '月之暗面 (Moonshot AI)',
isOpenAICompatible: true,
description: '支持超长上下文的中文大模型上下文窗口高达128K',
website: 'https://kimi.moonshot.cn',
userToken: '',
userModel: 'moonshot-v1-8k'
}
];

View File

@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { llms } from './llm';
function getConfigurationPath() {
// 如果是 vscode 插件下,则修改为 ~/.openmcp/config.json
@ -16,17 +17,23 @@ function getConfigurationPath() {
return 'config.json';
}
function getDefaultLanguage() {
if (process.env.VSCODE_PID) {
// TODO: 获取 vscode 内部的语言
}
return 'zh';
}
interface IConfig {
MODEL_BASE_URL: string;
MODEL_NAME: string;
API_TOKEN: string;
MODEL_INDEX: number;
[key: string]: any;
}
const DEFAULT_CONFIG: IConfig = {
MODEL_BASE_URL: '',
MODEL_NAME: '',
API_TOKEN: ''
MODEL_INDEX: 0,
LLM_INFO: llms,
LANG: getDefaultLanguage()
};
function createConfig(): IConfig {