From 653102b5c5a5ade1fa121bc10205827ccc7892cb Mon Sep 17 00:00:00 2001 From: C Jack Date: Mon, 8 Jul 2024 01:49:59 +0800 Subject: [PATCH 1/4] add provider: doubao, qianFan, qwen --- src/lang/locale/en.ts | 3 + src/lang/locale/zh-cn.ts | 5 +- src/providers/doubao.ts | 63 ++++++++++++++++ src/providers/index.ts | 4 + src/providers/qianFan.ts | 159 +++++++++++++++++++++++++++++++++++++++ src/providers/qwen.ts | 77 +++++++++++++++++++ src/settingTab.ts | 44 ++++++++++- src/settings.ts | 12 ++- 8 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 src/providers/doubao.ts create mode 100644 src/providers/qianFan.ts create mode 100644 src/providers/qwen.ts diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index af33a59..5261dd0 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -19,6 +19,8 @@ export default { // providers 'API key is required': 'API key is required', + 'API secret is required': 'API secret is required', + 'Model is required': 'Model is required', // settingTab.ts 'AI assistants': 'AI assistants', @@ -41,6 +43,7 @@ export default { 'Keyword for tag must be unique': 'Keyword for tag must be unique', Model: 'Model', 'Select the model to use': 'Select the model to use', + 'Input the model to use': 'Input the model to use', 'Override input parameters': 'Override input parameters', 'Developer feature, in JSON format, for example, {"model": "gptX"} can override the model input parameter.': 'Developer feature, in JSON format, for example, {"model": "gptX"} can override the model input parameter.', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index ad11199..e7651d2 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -19,6 +19,8 @@ export default { // providers 'API key is required': '请配置对应的 API key', + 'API secret is required': '请配置对应的 API secret', + 'Model is required': '请配置对应的模型', // settingTab.ts 'AI assistants': 'AI 助手', @@ -40,9 +42,10 @@ export default { 'Keyword for tag must be unique': '标签关键字必须唯一', Model: '模型', 'Select the model to use': '选择要使用的模型', + 'Input the model to use': '输入要使用的模型', 'Override input parameters': '覆盖输入参数', 'Developer feature, in JSON format, for example, {"model": "gptX"} can override the model input parameter.': - '开发者功能,json格式, 比如{"model": "gptX"}可以覆盖model输入参数', + '开发者功能,json格式, 比如{"model": "gptX"}可以覆盖model输入参数,如果model下拉框没有对应的模型,想要使用新的模型,可以在这里输入', 'Remove AI assistant': '移除 AI 助手', Remove: '移除', diff --git a/src/providers/doubao.ts b/src/providers/doubao.ts new file mode 100644 index 0000000..7023cb3 --- /dev/null +++ b/src/providers/doubao.ts @@ -0,0 +1,63 @@ +import fetch from 'node-fetch' +import { t } from 'src/lang/helper' +import { BaseOptions, Message, SendRequest, Vendor } from '.' + +const sendRequestFunc = (settings: BaseOptions): SendRequest => + async function* (messages: Message[]) { + const { parameters, ...optionsExcludingParams } = settings + const options = { ...optionsExcludingParams, ...parameters } // 这样的设计,让parameters 可以覆盖掉前面的设置 optionsExcludingParams + const { apiKey, baseURL, model, ...remains } = options + if (!apiKey) throw new Error(t('API key is required')) + if (!model) throw new Error(t('Model is required')) + + const data = { + model, + messages, + stream: true, + ...remains + } + const response = await fetch(baseURL, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + + if (!response || !response.body) { + throw new Error('No response') + } + const decoder = new TextDecoder('utf-8') + for await (const chunk of response.body) { + const lines = decoder.decode(Buffer.from(chunk)) + for (const line of lines.split('\n')) { + if (line.startsWith('{"error"')) { + const data = JSON.parse(line) + throw new Error(data.error.message) + } + if (line.startsWith('data: ')) { + const rawStr = line.slice('data: '.length) + if (rawStr.startsWith('[DONE]')) break + const data = JSON.parse(rawStr) + const content = data.choices[0].delta.content + if (content) { + yield content + } + } + } + } + } + +export const doubaoVendor: Vendor = { + name: 'Doubao', + defaultOptions: { + apiKey: '', + baseURL: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', + model: '', + parameters: {} + }, + sendRequestFunc, + models: [], + websiteToObtainKey: 'https://www.volcengine.com' +} diff --git a/src/providers/index.ts b/src/providers/index.ts index 194723f..6f77f28 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -24,3 +24,7 @@ export interface ProviderSettings { readonly vendor: string options: BaseOptions } + +export interface SecretOptions extends BaseOptions { + apiSecret: string +} diff --git a/src/providers/qianFan.ts b/src/providers/qianFan.ts new file mode 100644 index 0000000..c7a5645 --- /dev/null +++ b/src/providers/qianFan.ts @@ -0,0 +1,159 @@ +import fetch from 'node-fetch' +import { t } from 'src/lang/helper' +import { Message, SecretOptions, SendRequest, Vendor } from '.' + +interface TokenResponse { + access_token: string + expires_in: number +} + +interface Token { + accessToken: string + exp: number + apiKey: string + apiSecret: string +} + +export interface QianFanOptions extends SecretOptions { + token?: Token +} + +const createToken = async (apiKey: string, apiSecret: string) => { + if (!apiKey || !apiSecret) throw new Error('Invalid API key secret') + + const queryParams = { + grant_type: 'client_credentials', + client_id: apiKey, + client_secret: apiSecret + } + + const queryString = new URLSearchParams(queryParams).toString() + + const res = await fetch(`https://aip.baidubce.com/oauth/2.0/token?${queryString}`) + + const result = (await res.json()) as TokenResponse + console.debug('create new token', result) + const now = Date.now() + const exp = now + result.expires_in + + return { + accessToken: result.access_token, + exp: exp, + apiKey: apiKey, + apiSecret: apiSecret + } +} + +const validOrCreate = async (currentToken: Token | undefined, apiKey: string, apiSecret: string) => { + const now = Date.now() + if ( + currentToken && + currentToken.apiKey === apiKey && + currentToken.apiSecret === apiSecret && + currentToken.exp > now + 3 * 60 * 1000 + ) { + return { + isValid: true, + token: currentToken + } + } + const newToken = await createToken(apiKey, apiSecret) + console.debug('create new token', newToken) + return { + isValid: false, + token: newToken + } +} + +const getLines = (buffer: string[], text: string): string[] => { + const trailingNewline = text.endsWith('\n') || text.endsWith('\r') + let lines = text.split(/\r\n|[\n\r]/g) + + if (lines.length === 1 && !trailingNewline) { + buffer.push(lines[0]) + return [] + } + if (buffer.length > 0) { + lines = [buffer.join('') + lines[0], ...lines.slice(1)] + buffer = [] + } + + if (!trailingNewline) { + buffer = [lines.pop() || ''] + } + + return lines +} + +const sendRequestFunc = (settings: QianFanOptions): SendRequest => + async function* (messages: Message[]) { + const { parameters, ...optionsExcludingParams } = settings + const options = { ...optionsExcludingParams, ...parameters } // 这样的设计,让parameters 可以覆盖掉前面的设置 optionsExcludingParams + const { apiKey, apiSecret, baseURL, model, token: currentToken, ...remains } = options + if (!apiKey) throw new Error(t('API key is required')) + if (!apiSecret) throw new Error(t('API secret is required')) + if (!model) throw new Error(t('Model is required')) + + const { token } = await validOrCreate(currentToken, apiKey, apiSecret) + options.token = token // 这里的token没有保存到磁盘,只是在内存中保存 + + const data = { + messages, + stream: true, + remains + } + const response = await fetch(baseURL + `/${model}?access_token=${token.accessToken}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + + if (!response || !response.body) { + throw new Error('No response') + } + const buffer: string[] = [] + const decoder = new TextDecoder('utf-8') + for await (const chunk of response.body) { + const text = decoder.decode(Buffer.from(chunk)) + const lines = getLines(buffer, text) + for (const line of lines) { + if (line.startsWith('data: ')) { + const rawStr = line.slice('data: '.length) + + const data = JSON.parse(rawStr) + const content = data.result + if (content) { + yield content + } + } + } + } + } + +const models = [ + 'ernie-4.0-8k-latest', + 'ernie-4.0-turbo-8k', + 'ernie-3.5-128k', + 'ernie_speed', + 'ernie-speed-128k', + 'gemma_7b_it', + 'yi_34b_chat', + 'mixtral_8x7b_instruct', + 'llama_2_70b' +] + +export const qianFanVendor: Vendor = { + name: 'QianFan', + defaultOptions: { + apiKey: '', + apiSecret: '', + baseURL: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat', + model: models[0], + parameters: {} + } as SecretOptions, + sendRequestFunc, + models: models, + websiteToObtainKey: 'https://qianfan.cloud.baidu.com' +} diff --git a/src/providers/qwen.ts b/src/providers/qwen.ts new file mode 100644 index 0000000..bcea0a0 --- /dev/null +++ b/src/providers/qwen.ts @@ -0,0 +1,77 @@ +import fetch from 'node-fetch' +import { t } from 'src/lang/helper' +import { BaseOptions, Message, SendRequest, Vendor } from '.' + +const sendRequestFunc = (settings: BaseOptions): SendRequest => + async function* (messages: Message[]) { + const { parameters, ...optionsExcludingParams } = settings + const options = { ...optionsExcludingParams, ...parameters } // 这样的设计,让parameters 可以覆盖掉前面的设置 optionsExcludingParams + const { apiKey, baseURL, model, ...remains } = options + if (!apiKey) throw new Error(t('API key is required')) + + const data = { + model, + input: { + messages + }, + parameters: { + incremental_output: 'true' + }, + remains + } + const response = await fetch(baseURL, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'X-DashScope-SSE': 'enable' + }, + body: JSON.stringify(data) + }) + + if (!response || !response.body) { + throw new Error('No response') + } + const decoder = new TextDecoder('utf-8') + let isError = false + let _statusCode = 500 + for await (const chunk of response.body) { + const lines = decoder.decode(Buffer.from(chunk)) + for (const line of lines.split('\n')) { + if (line.startsWith('event:error')) { + isError = true + } else if (line.startsWith('status:')) { + _statusCode = parseInt(line.slice('status:'.length).trim(), 10) + } else if (line.startsWith('data:')) { + const data = line.slice('data:'.length) + const msg = JSON.parse(data) + const text = msg.output.text + yield text + if (isError) break + } + } + } + } + +const models = [ + 'qwen-turbo', + 'qwen-plus', + 'qwen-max', + 'qwen-max-0428', + 'qwen-max-0403', + 'qwen-max-0107', + 'qwen-max-longcontext' +] + +export const qwenVendor: Vendor = { + name: 'Qwen', + defaultOptions: { + apiKey: '', + baseURL: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', + model: models[0], + parameters: {} + }, + sendRequestFunc, + models, + websiteToObtainKey: 'https://dashscope.console.aliyun.com' +} diff --git a/src/settingTab.ts b/src/settingTab.ts index 8ac8a32..4d5c151 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -1,7 +1,7 @@ import { App, Notice, PluginSettingTab, Setting } from 'obsidian' import { t } from './lang/helper' import TarsPlugin from './main' -import { BaseOptions, ProviderSettings } from './providers' +import { BaseOptions, ProviderSettings, SecretOptions } from './providers' import { DEFAULT_SETTINGS, availableVendors } from './settings' export class TarsSettingTab extends PluginSettingTab { @@ -115,10 +115,18 @@ export class TarsSettingTab extends PluginSettingTab { settings.options, vendor.websiteToObtainKey ? t('Obtain key from ') + vendor.websiteToObtainKey : '' ) - this.addModelSection(details, settings.options, vendor.models) + + if ('apiSecret' in settings.options) { + this.addAPISecretSection(details, settings.options as SecretOptions) + } + if (vendor.models.length > 0) { + this.addModelDropDownSection(details, settings.options, vendor.models) + } else { + this.addModelTextSection(details, settings.options) + } this.addParametersSection(details, settings.options) - new Setting(details).setName(t('Remove AI assistant')).addButton((btn) => { + new Setting(details).setName(t('Remove') + ' ' + vendor.name).addButton((btn) => { btn .setWarning() .setButtonText(t('Remove')) @@ -170,7 +178,21 @@ export class TarsSettingTab extends PluginSettingTab { }) ) - addModelSection = (details: HTMLDetailsElement, options: BaseOptions, models: string[]) => + addAPISecretSection = (details: HTMLDetailsElement, options: SecretOptions, desc: string = '') => + new Setting(details) + .setName('API Secret') + .setDesc(desc) + .addText((text) => + text + .setPlaceholder('') + .setValue(options.apiSecret) + .onChange(async (value) => { + options.apiSecret = value + await this.plugin.saveSettings() + }) + ) + + addModelDropDownSection = (details: HTMLDetailsElement, options: BaseOptions, models: string[]) => new Setting(details) .setName(t('Model')) .setDesc(t('Select the model to use')) @@ -189,6 +211,20 @@ export class TarsSettingTab extends PluginSettingTab { }) ) + addModelTextSection = (details: HTMLDetailsElement, options: BaseOptions) => + new Setting(details) + .setName(t('Model')) + .setDesc(t('Input the model to use')) + .addText((text) => + text + .setPlaceholder('') + .setValue(options.model) + .onChange(async (value) => { + options.model = value + await this.plugin.saveSettings() + }) + ) + addParametersSection = (details: HTMLDetailsElement, options: BaseOptions) => new Setting(details) .setName(t('Override input parameters')) diff --git a/src/settings.ts b/src/settings.ts index 5279ab3..eb6196f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,6 +1,9 @@ import { ProviderSettings, Vendor } from './providers' +import { doubaoVendor } from './providers/doubao' import { kimiVendor } from './providers/kimi' import { openAIVendor } from './providers/openAI' +import { qianFanVendor } from './providers/qianFan' +import { qwenVendor } from './providers/qwen' import { zhipuVendor } from './providers/zhipu' export interface PluginSettings { @@ -19,4 +22,11 @@ export const DEFAULT_SETTINGS: PluginSettings = { userTags: ['User', '我'] } -export const availableVendors: Vendor[] = [kimiVendor, zhipuVendor, openAIVendor] +export const availableVendors: Vendor[] = [ + doubaoVendor, + kimiVendor, + openAIVendor, + qianFanVendor, + qwenVendor, + zhipuVendor +] From 37d4746f12b8f80874b2b2718acb4ff1324ed978 Mon Sep 17 00:00:00 2001 From: C Jack Date: Mon, 8 Jul 2024 01:50:24 +0800 Subject: [PATCH 2/4] chore: Update version to 0.2.0 in manifest and package.json --- esbuild.config.mjs | 65 +++++++++---------- manifest.json | 4 +- package-lock.json | 151 ++++++++++++++++++++++++++++++++------------- package.json | 7 ++- versions.json | 3 +- 5 files changed, 148 insertions(+), 82 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 859fa6e..4c639bb 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,48 +1,49 @@ -import esbuild from "esbuild"; -import process from "process"; -import builtins from "builtin-modules"; +import builtins from 'builtin-modules' +import esbuild from 'esbuild' +import process from 'process' -const banner = -`/* +const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ -`; +` -const prod = (process.argv[2] === "production"); +const prod = process.argv[2] === 'production' const context = await esbuild.context({ banner: { - js: banner, + js: banner }, - entryPoints: ["src/main.ts"], + entryPoints: ['src/main.ts'], bundle: true, external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins], - format: "cjs", - target: "es2018", - logLevel: "info", - sourcemap: prod ? false : "inline", + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins + ], + format: 'cjs', + target: 'es2018', + logLevel: 'info', + sourcemap: prod ? false : 'inline', treeShaking: true, - outfile: "main.js", -}); + platform: 'node', + outfile: 'main.js' +}) if (prod) { - await context.rebuild(); - process.exit(0); + await context.rebuild() + process.exit(0) } else { - await context.watch(); + await context.watch() } diff --git a/manifest.json b/manifest.json index a3da00c..e3e0250 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "tars", "name": "Tars", - "version": "0.1.2", + "version": "0.2.0", "minAppVersion": "1.5.8", "description": "Use Kimi and other Chinese LLMs for text generation based on tag suggestions.", "author": "Tarslab", "authorUrl": "https://github.com/tarslab", - "isDesktopOnly": false + "isDesktopOnly": true } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8720c58..e5ee006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,25 @@ { "name": "obsidian-tars", - "version": "0.1.0", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-tars", - "version": "0.1.0", + "version": "0.1.2", "license": "MIT", "devDependencies": { - "@types/node": "^16.11.6", + "@types/node": "^16.18.101", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "builtin-modules": "3.3.0", "esbuild": "0.17.3", "jose": "^5.2.4", + "node-fetch": "^3.3.2", "obsidian": "latest", "openai": "^4.33.0", "tslib": "^2.4.0", - "typescript": "5.4" + "typescript": "^5.5.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -553,9 +554,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.18.94", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.94.tgz", - "integrity": "sha512-X8q3DoKq8t/QhA0Rk/9wJUajxtXRDiCK+cVaONKLxpsjPhu+xX6uZuEj4UKGLQ4p0obTdFxa0cP/BMvf9mOYZA==", + "version": "16.18.101", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.101.tgz", + "integrity": "sha512-AAsx9Rgz2IzG8KJ6tXd6ndNkVcu+GYB6U/SnFAaokSPNx2N7dcIIfnighYUNumvj6YS2q39Dejz5tT0NCV7CWA==", "dev": true }, "node_modules/@types/node-fetch": { @@ -1038,6 +1039,15 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1382,6 +1392,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1488,6 +1521,18 @@ "node": ">= 14" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1878,45 +1923,21 @@ } }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/obsidian": { @@ -1971,6 +1992,26 @@ "undici-types": "~5.26.4" } }, + "node_modules/openai/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -2314,6 +2355,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -2359,9 +2406,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2403,6 +2450,22 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index cece06f..8b5c735 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-tars", - "version": "0.1.2", + "version": "0.2.0", "description": "Use Kimi and other Chinese LLMs for text generation based on tag suggestions.", "main": "main.js", "scripts": { @@ -12,15 +12,16 @@ "author": "C Jack", "license": "MIT", "devDependencies": { - "@types/node": "^16.11.6", + "@types/node": "^16.18.101", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "builtin-modules": "3.3.0", "esbuild": "0.17.3", "jose": "^5.2.4", + "node-fetch": "^3.3.2", "obsidian": "latest", "openai": "^4.33.0", "tslib": "^2.4.0", - "typescript": "5.4" + "typescript": "^5.5.2" } } diff --git a/versions.json b/versions.json index d706d6b..e32a791 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,6 @@ { "0.1.0": "1.5.8", "0.1.1": "1.5.8", - "0.1.2": "1.5.8" + "0.1.2": "1.5.8", + "0.2.0": "1.5.8" } \ No newline at end of file From 5c4bf4c60e88c30b757bcf59eea771978be55ea3 Mon Sep 17 00:00:00 2001 From: C Jack Date: Mon, 8 Jul 2024 10:38:36 +0800 Subject: [PATCH 3/4] chore: prettier --- src/providers/qianFan.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/providers/qianFan.ts b/src/providers/qianFan.ts index c7a5645..4f528eb 100644 --- a/src/providers/qianFan.ts +++ b/src/providers/qianFan.ts @@ -26,22 +26,21 @@ const createToken = async (apiKey: string, apiSecret: string) => { client_id: apiKey, client_secret: apiSecret } - const queryString = new URLSearchParams(queryParams).toString() - const res = await fetch(`https://aip.baidubce.com/oauth/2.0/token?${queryString}`) const result = (await res.json()) as TokenResponse console.debug('create new token', result) + const accessToken = result.access_token const now = Date.now() const exp = now + result.expires_in return { - accessToken: result.access_token, - exp: exp, - apiKey: apiKey, - apiSecret: apiSecret - } + accessToken, + exp, + apiKey, + apiSecret + } as Token } const validOrCreate = async (currentToken: Token | undefined, apiKey: string, apiSecret: string) => { @@ -58,7 +57,6 @@ const validOrCreate = async (currentToken: Token | undefined, apiKey: string, ap } } const newToken = await createToken(apiKey, apiSecret) - console.debug('create new token', newToken) return { isValid: false, token: newToken @@ -77,11 +75,9 @@ const getLines = (buffer: string[], text: string): string[] => { lines = [buffer.join('') + lines[0], ...lines.slice(1)] buffer = [] } - if (!trailingNewline) { buffer = [lines.pop() || ''] } - return lines } @@ -121,7 +117,6 @@ const sendRequestFunc = (settings: QianFanOptions): SendRequest => for (const line of lines) { if (line.startsWith('data: ')) { const rawStr = line.slice('data: '.length) - const data = JSON.parse(rawStr) const content = data.result if (content) { From 504460764888310a45471b204aa9d97f45b9ee48 Mon Sep 17 00:00:00 2001 From: C Jack Date: Mon, 8 Jul 2024 10:39:25 +0800 Subject: [PATCH 4/4] Add support for Doubao, QianFan, and Qwen AI providers --- README.md | 17 ++++++++++++----- README_en.md | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 48c3874..6b87e8b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # 简介 -Tars 是一个 Obsidian 插件,它使用 Kimi 和其他中文大型语言模型(LLMs)基于标签建议进行文本生成。Tars 这个名字来源于电影《星际穿越》中的机器人 Tars。 +Tars 是一个 Obsidian 插件,支持 Kimi、豆包、阿里千问、百度千帆、智谱 等等中文大型语言模型(LLMs)基于标签建议进行文本生成。Tars 这个名字来源于电影《星际穿越》中的机器人 Tars。 ## 特性 @@ -23,15 +23,21 @@ Tars 是一个 Obsidian 插件,它使用 Kimi 和其他中文大型语言模 ## AI 服务提供商 -- [x] [Kimi](https://www.moonshot.cn) -- [x] [Zhipu](https://open.bigmodel.cn/) -- [x] [OpenAI](https://platform.openai.com/api-keys) -- [ ] [Doubao](https://www.volcengine.com/product/doubao) +- [Kimi](https://www.moonshot.cn) +- [Doubao 豆包](https://www.volcengine.com/product/doubao) +- [OpenAI](https://platform.openai.com/api-keys) +- [Qianfan 百度千帆](https://qianfan.cloud.baidu.com) +- [Qwen 阿里千问](https://dashscope.console.aliyun.com) +- [Zhipu 智谱](https://open.bigmodel.cn/) + +如果上面列表没有你想要的 AI 服务提供商,可以在 issue 中提出具体方案。 ## 如何使用 在设置页面添加一个 AI 助手,设置 API 密钥,然后在编辑器中使用相应的标签来触发 AI 助手。 +如果在设置页面的 AI 助手中没有你想要的 model 类型,可以在设置中的“覆盖输入参数”进行配置,输入 JSON 格式,例如 `{"model":"你想要的model"}`。 + ## 对话语法 一个段落不能包含多条消息。多条消息应该通过空行分隔开来。 @@ -40,6 +46,7 @@ Tars 是一个 Obsidian 插件,它使用 Kimi 和其他中文大型语言模 - 对话消息将发送到配置的 AI 服务提供商。 - 块引用和 callout 部分将被忽略。你可以利用块引用写注释,而不将其发送到 AI 助手。 +- 开始新对话,使用 `新对话标签`。 ## 建议 diff --git a/README_en.md b/README_en.md index 4613479..37ca2a7 100644 --- a/README_en.md +++ b/README_en.md @@ -7,7 +7,7 @@ # Introduction -Tars is an Obsidian plugin that uses Kimi and other Chinese LLMs for text generation based on tag suggestions. The name Tars comes from the robot Tars in Interstellar. +Tars is an Obsidian plugin that supports text generation by Kimi, Doubao, Ali Qianwen, Baidu Qianfan, Zhipu, and other Chinese large language models (LLMs) based on tag suggestions. The name Tars comes from the robot Tars in the movie "Interstellar". ## Features @@ -23,15 +23,21 @@ Tars is an Obsidian plugin that uses Kimi and other Chinese LLMs for text genera ## AI providers -- [x] [Kimi](https://www.moonshot.cn) -- [x] [Zhipu](https://open.bigmodel.cn/) -- [x] [OpenAI](https://platform.openai.com/api-keys) -- [ ] [Doubao](https://www.volcengine.com/product/doubao) +- [Kimi](https://www.moonshot.cn) +- [Doubao](https://www.volcengine.com/product/doubao) +- [OpenAI](https://platform.openai.com/api-keys) +- [Qianfan](https://qianfan.cloud.baidu.com) +- [Qwen](https://dashscope.console.aliyun.com) +- [Zhipu](https://open.bigmodel.cn/) + +If the AI provider you want is not in the list above, you can propose a specific plan in the issue. ## How to use Add an AI assistant in the settings page, set the API key, and then use the corresponding tag in the editor to trigger the AI assistant. +If the model type you want is not in the AI assistant on the settings page, you can configure it in the "Override input parameters" in the settings, input JSON format, for example `{"model":"your desired model"}`. + ## Conversations syntax A paragraph cannot contain multiple messages. Messages should be separated by blank lines. @@ -40,6 +46,7 @@ A paragraph cannot contain multiple messages. Messages should be separated by bl - The conversation messages will send to the configured AI assistant. - Blockquote and callout sections are ignored. You can make annotations without sending them to the AI assistant. +- Start a new conversation with `new conversation tag`. ## Recommended