From 0fd3b66a402c79044d41640b1c2a27bcd2e3848f Mon Sep 17 00:00:00 2001 From: ni00 Date: Sun, 19 May 2024 00:33:54 +0800 Subject: [PATCH] feat(copilot): support custom copilot api --- src/background.ts | 1 + src/components/Copilot.vue | 3 +- src/database/database.config.ts | 2 + .../1716044120271-supportOpenAIAPIHost.ts | 226 ++++++++++++++++++ src/database/models/SettingEntity.ts | 3 + src/store/getter.ts | 1 + src/store/modules/app.ts | 11 + src/types/global.d.ts | 2 + src/views/settings/index.vue | 97 +++++--- 9 files changed, 315 insertions(+), 31 deletions(-) create mode 100644 src/database/migration/1716044120271-supportOpenAIAPIHost.ts diff --git a/src/background.ts b/src/background.ts index a6e45d918..7d0328cd2 100644 --- a/src/background.ts +++ b/src/background.ts @@ -144,6 +144,7 @@ async function createWindow() { multiTopics: setting.multiTopics, jsonHighlight: setting.jsonHighlight, enableCopilot: setting.enableCopilot, + openAIAPIHost: setting.openAIAPIHost, openAIAPIKey: setting.openAIAPIKey, model: setting.model, logLevel: setting.logLevel, diff --git a/src/components/Copilot.vue b/src/components/Copilot.vue index d9055afda..24524ceb4 100644 --- a/src/components/Copilot.vue +++ b/src/components/Copilot.vue @@ -107,6 +107,7 @@ export default class Copilot extends Vue { @Prop({ required: true }) public mode!: 'connections' | 'scripts' | 'help' @Action('SET_INSERT_BUTTON_ADDED') private setisPrismButtonAdded!: (payload: { isPrismButtonAdded: boolean }) => void + @Getter('openAIAPIHost') private openAIAPIHost!: string @Getter('openAIAPIKey') private openAIAPIKey!: string @Getter('model') private model!: AIModel @Getter('isPrismButtonAdded') private isPrismButtonAdded!: boolean @@ -232,7 +233,7 @@ export default class Copilot extends Vue { } this.isResponseStream = true - const response = await fetch('https://api.openai.com/v1/chat/completions', fetchOptions) + const response = await fetch(`${this.openAIAPIHost}/chat/completions`, fetchOptions) if (response && response.status === 200 && response.ok) { this.isSending = false const throttledScroll = throttle(() => { diff --git a/src/database/database.config.ts b/src/database/database.config.ts index e57bb1649..35292a5a2 100644 --- a/src/database/database.config.ts +++ b/src/database/database.config.ts @@ -44,6 +44,7 @@ import { aiTables1701936842016 } from './migration/1701936842016-aiTables' import { enableCopilot1703659148195 } from './migration/1703659148195-enableCopilot' import { logLevel1704941582350 } from './migration/1704941582350-logLevel' import { updatePayloadTypeToVarchar1630403733965 } from './migration/1705478422620-updatePayloadTypeToVarchar' +import { supportOpenAIAPIHost1716044120271 } from './migration/1716044120271-supportOpenAIAPIHost' const STORE_PATH = getAppDataPath('MQTTX') try { @@ -94,6 +95,7 @@ const ORMConfig = { enableCopilot1703659148195, logLevel1704941582350, updatePayloadTypeToVarchar1630403733965, + supportOpenAIAPIHost1716044120271, ], migrationsTableName: 'temp_migration_table', entities: [ diff --git a/src/database/migration/1716044120271-supportOpenAIAPIHost.ts b/src/database/migration/1716044120271-supportOpenAIAPIHost.ts new file mode 100644 index 000000000..76bac9e1e --- /dev/null +++ b/src/database/migration/1716044120271-supportOpenAIAPIHost.ts @@ -0,0 +1,226 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class supportOpenAIAPIHost1716044120271 implements MigrationInterface { + name = 'supportOpenAIAPIHost1716044120271' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "temporary_historyMessagePayloadEntity" ( + "id" uuid PRIMARY KEY NOT NULL, + "payload" varchar NOT NULL, + "payloadType" varchar NOT NULL DEFAULT ('JSON'), + "createAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) + ) + `) + await queryRunner.query(` + INSERT INTO "temporary_historyMessagePayloadEntity"("id", "payload", "payloadType", "createAt") + SELECT "id", + "payload", + "payloadType", + "createAt" + FROM "historyMessagePayloadEntity" + `) + await queryRunner.query(` + DROP TABLE "historyMessagePayloadEntity" + `) + await queryRunner.query(` + ALTER TABLE "temporary_historyMessagePayloadEntity" + RENAME TO "historyMessagePayloadEntity" + `) + await queryRunner.query(` + CREATE TABLE "temporary_SettingEntity" ( + "id" varchar PRIMARY KEY NOT NULL, + "width" integer NOT NULL DEFAULT (1025), + "height" integer NOT NULL DEFAULT (749), + "autoCheck" boolean NOT NULL DEFAULT (1), + "currentLang" varchar CHECK(currentLang IN ('zh', 'en', 'ja', 'tr', 'hu')) NOT NULL DEFAULT ('en'), + "currentTheme" varchar CHECK(currentTheme IN ('light', 'dark', 'night')) NOT NULL DEFAULT ('light'), + "maxReconnectTimes" integer NOT NULL DEFAULT (10), + "autoResub" boolean NOT NULL DEFAULT (1), + "syncOsTheme" boolean NOT NULL DEFAULT (0), + "multiTopics" boolean NOT NULL DEFAULT (1), + "jsonHighlight" boolean NOT NULL DEFAULT (1), + "openAIAPIKey" varchar NOT NULL DEFAULT (''), + "model" varchar NOT NULL DEFAULT ('gpt-3.5-turbo'), + "enableCopilot" boolean NOT NULL DEFAULT (1), + "logLevel" varchar CHECK(logLevel IN ('debug', 'info', 'warn', 'error')) NOT NULL DEFAULT ('info'), + "openAIAPIHost" varchar NOT NULL DEFAULT ('https://api.openai.com/v1') + ) + `) + await queryRunner.query(` + INSERT INTO "temporary_SettingEntity"( + "id", + "width", + "height", + "autoCheck", + "currentLang", + "currentTheme", + "maxReconnectTimes", + "autoResub", + "syncOsTheme", + "multiTopics", + "jsonHighlight", + "openAIAPIKey", + "model", + "enableCopilot", + "logLevel" + ) + SELECT "id", + "width", + "height", + "autoCheck", + "currentLang", + "currentTheme", + "maxReconnectTimes", + "autoResub", + "syncOsTheme", + "multiTopics", + "jsonHighlight", + "openAIAPIKey", + "model", + "enableCopilot", + "logLevel" + FROM "SettingEntity" + `) + await queryRunner.query(` + DROP TABLE "SettingEntity" + `) + await queryRunner.query(` + ALTER TABLE "temporary_SettingEntity" + RENAME TO "SettingEntity" + `) + await queryRunner.query(` + CREATE TABLE "temporary_historyMessagePayloadEntity" ( + "id" varchar PRIMARY KEY NOT NULL, + "payload" varchar NOT NULL, + "payloadType" varchar NOT NULL DEFAULT ('JSON'), + "createAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) + ) + `) + await queryRunner.query(` + INSERT INTO "temporary_historyMessagePayloadEntity"("id", "payload", "payloadType", "createAt") + SELECT "id", + "payload", + "payloadType", + "createAt" + FROM "historyMessagePayloadEntity" + `) + await queryRunner.query(` + DROP TABLE "historyMessagePayloadEntity" + `) + await queryRunner.query(` + ALTER TABLE "temporary_historyMessagePayloadEntity" + RENAME TO "historyMessagePayloadEntity" + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "historyMessagePayloadEntity" + RENAME TO "temporary_historyMessagePayloadEntity" + `) + await queryRunner.query(` + CREATE TABLE "historyMessagePayloadEntity" ( + "id" uuid PRIMARY KEY NOT NULL, + "payload" varchar NOT NULL, + "payloadType" varchar NOT NULL DEFAULT ('JSON'), + "createAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) + ) + `) + await queryRunner.query(` + INSERT INTO "historyMessagePayloadEntity"("id", "payload", "payloadType", "createAt") + SELECT "id", + "payload", + "payloadType", + "createAt" + FROM "temporary_historyMessagePayloadEntity" + `) + await queryRunner.query(` + DROP TABLE "temporary_historyMessagePayloadEntity" + `) + await queryRunner.query(` + ALTER TABLE "SettingEntity" + RENAME TO "temporary_SettingEntity" + `) + await queryRunner.query(` + CREATE TABLE "SettingEntity" ( + "id" varchar PRIMARY KEY NOT NULL, + "width" integer NOT NULL DEFAULT (1025), + "height" integer NOT NULL DEFAULT (749), + "autoCheck" boolean NOT NULL DEFAULT (1), + "currentLang" varchar CHECK(currentLang IN ('zh', 'en', 'ja', 'tr', 'hu')) NOT NULL DEFAULT ('en'), + "currentTheme" varchar CHECK(currentTheme IN ('light', 'dark', 'night')) NOT NULL DEFAULT ('light'), + "maxReconnectTimes" integer NOT NULL DEFAULT (10), + "autoResub" boolean NOT NULL DEFAULT (1), + "syncOsTheme" boolean NOT NULL DEFAULT (0), + "multiTopics" boolean NOT NULL DEFAULT (1), + "jsonHighlight" boolean NOT NULL DEFAULT (1), + "openAIAPIKey" varchar NOT NULL DEFAULT (''), + "model" varchar NOT NULL DEFAULT ('gpt-3.5-turbo'), + "enableCopilot" boolean NOT NULL DEFAULT (1), + "logLevel" varchar CHECK(logLevel IN ('debug', 'info', 'warn', 'error')) NOT NULL DEFAULT ('info') + ) + `) + await queryRunner.query(` + INSERT INTO "SettingEntity"( + "id", + "width", + "height", + "autoCheck", + "currentLang", + "currentTheme", + "maxReconnectTimes", + "autoResub", + "syncOsTheme", + "multiTopics", + "jsonHighlight", + "openAIAPIKey", + "model", + "enableCopilot", + "logLevel" + ) + SELECT "id", + "width", + "height", + "autoCheck", + "currentLang", + "currentTheme", + "maxReconnectTimes", + "autoResub", + "syncOsTheme", + "multiTopics", + "jsonHighlight", + "openAIAPIKey", + "model", + "enableCopilot", + "logLevel" + FROM "temporary_SettingEntity" + `) + await queryRunner.query(` + DROP TABLE "temporary_SettingEntity" + `) + await queryRunner.query(` + ALTER TABLE "historyMessagePayloadEntity" + RENAME TO "temporary_historyMessagePayloadEntity" + `) + await queryRunner.query(` + CREATE TABLE "historyMessagePayloadEntity" ( + "id" uuid PRIMARY KEY NOT NULL, + "payload" varchar NOT NULL, + "payloadType" varchar NOT NULL DEFAULT ('JSON'), + "createAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) + ) + `) + await queryRunner.query(` + INSERT INTO "historyMessagePayloadEntity"("id", "payload", "payloadType", "createAt") + SELECT "id", + "payload", + "payloadType", + "createAt" + FROM "temporary_historyMessagePayloadEntity" + `) + await queryRunner.query(` + DROP TABLE "temporary_historyMessagePayloadEntity" + `) + } +} diff --git a/src/database/models/SettingEntity.ts b/src/database/models/SettingEntity.ts index 8a275e3b8..0c3589502 100644 --- a/src/database/models/SettingEntity.ts +++ b/src/database/models/SettingEntity.ts @@ -38,6 +38,9 @@ export default class SettingEntity { @Column({ type: 'boolean', default: true }) enableCopilot!: boolean + @Column({ type: 'varchar', default: 'https://api.openai.com/v1' }) + openAIAPIHost!: string + @Column({ type: 'varchar', default: '' }) openAIAPIKey!: string diff --git a/src/store/getter.ts b/src/store/getter.ts index 20aa6df4b..3e2451b50 100644 --- a/src/store/getter.ts +++ b/src/store/getter.ts @@ -16,6 +16,7 @@ const getters = { multiTopics: (state: State) => state.app.multiTopics, jsonHighlight: (state: State) => state.app.jsonHighlight, enableCopilot: (state: State) => state.app.enableCopilot, + openAIAPIHost: (state: State) => state.app.openAIAPIHost, openAIAPIKey: (state: State) => state.app.openAIAPIKey, model: (state: State) => state.app.model, isPrismButtonAdded: (state: State) => state.app.isPrismButtonAdded, diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index e4a79be6d..1f0c75cfb 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -20,6 +20,7 @@ const SET_CURRENT_CONNECTION_ID = 'SET_CURRENT_CONNECTION_ID' const TOGGLE_SYNC_OS_THEME = 'TOGGLE_SYNC_OS_THEME' const TOGGLE_MULTI_TOPICS = 'TOGGLE_MULTI_TOPICS' const TOGGLE_JSON_HIGHLIGHT = 'TOGGLE_JSON_HIGHLIGHT' +const SET_OPEN_AI_HOST = 'SET_OPEN_AI_HOST' const SET_OPEN_AI_API_KEY = 'SET_OPEN_AI_API_KEY' const SET_MODEL = 'SET_MODEL' const SET_INSERT_BUTTON_ADDED = 'SET_INSERT_BUTTON_ADDED' @@ -56,6 +57,7 @@ const app = { currentScript: null, currentConnectionId: null, enableCopilot: settingData.enableCopilot, + openAIAPIHost: settingData.openAIAPIHost || 'https://api.openai.com/v1', openAIAPIKey: settingData.openAIAPIKey || '', model: settingData.model || 'gpt-3.5-turbo', isPrismButtonAdded: false, @@ -141,6 +143,9 @@ const app = { [SET_CURRENT_CONNECTION_ID](state: App, currentConnectionId: string) { state.currentConnectionId = currentConnectionId }, + [SET_OPEN_AI_HOST](state: App, openAIHost: string) { + state.openAIAPIHost = openAIHost + }, [SET_OPEN_AI_API_KEY](state: App, openAIAPIKey: string) { state.openAIAPIKey = openAIAPIKey }, @@ -246,6 +251,12 @@ const app = { settingData.enableCopilot = payload.enableCopilot await settingService.update(payload) }, + async SET_OPEN_AI_HOST({ commit }: any, payload: App) { + const { settingService } = useServices() + commit(SET_OPEN_AI_HOST, payload.openAIAPIHost) + settingData.openAIAPIHost = payload.openAIAPIHost + await settingService.update(payload) + }, async SET_OPEN_AI_API_KEY({ commit }: any, payload: App) { const { settingService } = useServices() commit(SET_OPEN_AI_API_KEY, payload.openAIAPIKey) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 59e24f311..ac2adf1a9 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -105,6 +105,7 @@ declare global { connectionTreeState: ConnectionTreeStateMap jsonHighlight: boolean enableCopilot: boolean + openAIAPIHost: string openAIAPIKey: string model: AIModel isPrismButtonAdded: boolean @@ -388,6 +389,7 @@ declare global { | 'gpt-4-32k-0613' | 'gpt-4-turbo' | 'gpt-4o' + | string interface AreaLineSeriesData { xData: string[] diff --git a/src/views/settings/index.vue b/src/views/settings/index.vue index d6427c55c..d07dfbd43 100644 --- a/src/views/settings/index.vue +++ b/src/views/settings/index.vue @@ -292,6 +292,25 @@ + + + + + + + + + + @@ -299,7 +318,7 @@ {{ $t('settings.model') }} - - - + > @@ -355,6 +376,7 @@ export default class Settings extends Vue { @Action('TOGGLE_MULTI_TOPICS') private actionToggleMultiTopics!: (payload: { multiTopics: boolean }) => void @Action('TOGGLE_JSON_HIGHLIGHT') private actionToggleJsonHighlight!: (payload: { jsonHighlight: boolean }) => void @Action('TOGGLE_ENABLE_COPILOT') private actionToggleEnableCopilot!: (payload: { enableCopilot: boolean }) => void + @Action('SET_OPEN_AI_HOST') private actionSetOpenAIAPIHost!: (payload: { openAIAPIHost: string }) => void @Action('SET_OPEN_AI_API_KEY') private actionSetOpenAIAPIKey!: (payload: { openAIAPIKey: string }) => void @Action('SET_MODEL') private actionSetModel!: (payload: { model: AIModel }) => void @Action('SET_LOG_LEVEL') private actionSetLogLevel!: (payload: { logLevel: LogLevel }) => void @@ -368,10 +390,10 @@ export default class Settings extends Vue { @Getter('multiTopics') private multiTopics!: boolean @Getter('jsonHighlight') private jsonHighlight!: boolean @Getter('enableCopilot') private enableCopilot!: boolean + @Getter('openAIAPIHost') private openAIAPIHost!: string @Getter('openAIAPIKey') private openAIAPIKey!: string @Getter('model') private model!: AIModel @Getter('logLevel') private logLevel!: LogLevel - private langOptions: Options[] = [ { label: '简体中文', value: 'zh' }, { label: 'English', value: 'en' }, @@ -385,16 +407,16 @@ export default class Settings extends Vue { { label: 'Night', value: 'night' }, ] private AImodelsOptions = [ - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0125', - 'gpt-3.5-turbo-1106', - 'gpt-3.5-turbo-16k', - 'gpt-4', - 'gpt-4-32k', - 'gpt-4-0613', - 'gpt-4-32k-0613', - 'gpt-4-turbo', - 'gpt-4o', + { value: 'gpt-3.5-turbo' }, + { value: 'gpt-3.5-turbo-0125' }, + { value: 'gpt-3.5-turbo-1106' }, + { value: 'gpt-3.5-turbo-16k' }, + { value: 'gpt-4' }, + { value: 'gpt-4-32k' }, + { value: 'gpt-4-0613' }, + { value: 'gpt-4-32k-0613' }, + { value: 'gpt-4-turbo' }, + { value: 'gpt-4o' }, ] private showImportData = false private showExportData = false @@ -403,9 +425,11 @@ export default class Settings extends Vue { private aiConfig: { model: AIModel openAIAPIKey: string + openAIAPIHost: string } = { model: 'gpt-3.5-turbo', openAIAPIKey: '', + openAIAPIHost: 'https://api.openai.com/v1', } private handleSelectChange(type: 'lang' | 'theme', value: string | number | boolean): void { @@ -461,21 +485,37 @@ export default class Settings extends Vue { this.actionToggleEnableCopilot({ enableCopilot: value }) } - private handleAIConfigChanged(action: 'apiKey' | 'model') { - if (action === 'apiKey') { - let saveKey = '' - if (this.aiConfig.openAIAPIKey !== '') { - saveKey = CryptoJS.AES.encrypt(this.aiConfig.openAIAPIKey.trim(), ENCRYPT_KEY).toString() - } - this.actionSetOpenAIAPIKey({ openAIAPIKey: saveKey }) - } else if (action === 'model') { - this.actionSetModel({ model: this.aiConfig.model }) + private handleAIConfigChanged(action: 'apiKey' | 'model' | 'host') { + switch (action) { + case 'apiKey': + const encryptedKey = + this.aiConfig.openAIAPIKey.trim() !== '' + ? CryptoJS.AES.encrypt(this.aiConfig.openAIAPIKey.trim(), ENCRYPT_KEY).toString() + : '' + this.actionSetOpenAIAPIKey({ openAIAPIKey: encryptedKey }) + break + case 'model': + this.actionSetModel({ model: this.aiConfig.model || 'gpt-3.5-turbo' }) + break + case 'host': + this.actionSetOpenAIAPIHost({ openAIAPIHost: this.aiConfig.openAIAPIHost || 'https://api.openai.com/v1' }) + break } } + private handleAIConfigModelSelected({ value }: { value: string }) { + this.aiConfig.model = value + this.actionSetModel({ model: value }) + } + + private queryAImodels(queryString: AIModel, cb: (r: any[]) => {}) { + cb(queryString ? this.AImodelsOptions.filter((item) => item.value.includes(queryString)) : this.AImodelsOptions) + } + private getAIConfigs() { this.aiConfig.model = this.model this.aiConfig.openAIAPIKey = this.openAIAPIKey + this.aiConfig.openAIAPIHost = this.openAIAPIHost } private handleLevelChange(val: LogLevel) { @@ -543,9 +583,6 @@ export default class Settings extends Vue { &.el-select { width: 108px; - &.ai-model-select { - width: 150px; - } } &.el-select .el-input .el-select__caret {