diff --git a/.vscode/launch.json b/.vscode/launch.json index 14db135..7b36a75 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,8 +11,8 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - // "--disable-extensions" + "--extensionDevelopmentPath=${workspaceFolder}", + "--disable-extensions" ], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "npm: watch", diff --git a/package.nls.en.json b/package.nls.en.json index 95d472d..b5863c9 100644 --- a/package.nls.en.json +++ b/package.nls.en.json @@ -45,6 +45,7 @@ "error.xclipNotFound": "xclip is not installed. Please install it using your package manager (e.g., sudo apt-get install xclip)", "error.fileNotFound": "File not found", "error.invalidInput": "Invalid input", + "error.aideKeyUsageInfoOnlySupportAideModels": "We currently only support viewing the usage information of the Aide model aggregation service. Please check: [https://aide.nicepkg.cn/guide/use-another-llm/aide-models](https://aide.nicepkg.cn/guide/use-another-llm/aide-models)", "info.copied": "File contents have been copied to clipboard", "info.noAiSuggestionsVariableName": "AI thinks your variable name is already good", "info.processing": "Aide is processing...", @@ -55,6 +56,16 @@ "info.commandCopiedToClipboard": "AI command has been copied to clipboard", "info.fileReplaceSuccess": "File content has been replaced successfully", "info.batchProcessorSuccess": "AI batch processor success!\n\nTotal {0} files generated, you can review and replace manually.\n\nTasks completed:\n{1}", + "info.loading": "Loading...", + "info.aideKey.neverExpires": "never expires", + "info.aideKey.usageInfo": "Aide key usage information (unit: virtual dollars)", + "info.aideKey.total": "Key total", + "info.aideKey.used": "Key used", + "info.aideKey.remain": "Key balance", + "info.aideKey.callCount": "Call count", + "info.aideKey.validUntil": "Valid until", + "info.aideKeyUsageStatusBar.text": "Aide Usage", + "info.aideKeyUsageStatusBar.tooltip": "Click to view Aide Key Usage Information", "input.array.promptEnding": "Enter comma separated values", "input.json.promptEnding": "Enter JSON formatted value", "input.aiCommand.prompt": "Enter question for AI command", diff --git a/package.nls.json b/package.nls.json index 95d472d..b5863c9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -45,6 +45,7 @@ "error.xclipNotFound": "xclip is not installed. Please install it using your package manager (e.g., sudo apt-get install xclip)", "error.fileNotFound": "File not found", "error.invalidInput": "Invalid input", + "error.aideKeyUsageInfoOnlySupportAideModels": "We currently only support viewing the usage information of the Aide model aggregation service. Please check: [https://aide.nicepkg.cn/guide/use-another-llm/aide-models](https://aide.nicepkg.cn/guide/use-another-llm/aide-models)", "info.copied": "File contents have been copied to clipboard", "info.noAiSuggestionsVariableName": "AI thinks your variable name is already good", "info.processing": "Aide is processing...", @@ -55,6 +56,16 @@ "info.commandCopiedToClipboard": "AI command has been copied to clipboard", "info.fileReplaceSuccess": "File content has been replaced successfully", "info.batchProcessorSuccess": "AI batch processor success!\n\nTotal {0} files generated, you can review and replace manually.\n\nTasks completed:\n{1}", + "info.loading": "Loading...", + "info.aideKey.neverExpires": "never expires", + "info.aideKey.usageInfo": "Aide key usage information (unit: virtual dollars)", + "info.aideKey.total": "Key total", + "info.aideKey.used": "Key used", + "info.aideKey.remain": "Key balance", + "info.aideKey.callCount": "Call count", + "info.aideKey.validUntil": "Valid until", + "info.aideKeyUsageStatusBar.text": "Aide Usage", + "info.aideKeyUsageStatusBar.tooltip": "Click to view Aide Key Usage Information", "input.array.promptEnding": "Enter comma separated values", "input.json.promptEnding": "Enter JSON formatted value", "input.aiCommand.prompt": "Enter question for AI command", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index d7065d0..4fed887 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -45,6 +45,7 @@ "error.xclipNotFound": "xclip 未安装。请使用你的包管理器安装它 (例如,sudo apt-get install xclip)", "error.fileNotFound": "文件未找到", "error.invalidInput": "无效的输入", + "error.aideKeyUsageInfoOnlySupportAideModels": "我们目前仅支持查看 Aide 模型聚合服务的使用信息。请查看:[https://aide.nicepkg.cn/zh/guide/use-another-llm/aide-models](https://aide.nicepkg.cn/zh/guide/use-another-llm/aide-models)", "info.copied": "文件内容已复制到剪贴板", "info.noAiSuggestionsVariableName": " AI 觉得你这个变量名字已经很好了", "info.processing": "Aide 正在处理中...", @@ -55,6 +56,16 @@ "info.commandCopiedToClipboard": "AI 命令已复制到剪贴板", "info.fileReplaceSuccess": "文件内容已成功替换", "info.batchProcessorSuccess": "AI 批量处理成功!\n\n共生成了 {0} 个文件, 你可以自己 review 手动替换。\n\n已完成任务:\n{1}", + "info.loading": "加载中...", + "info.aideKey.neverExpires": "永不过期", + "info.aideKey.usageInfo": "Aide 密钥使用信息(单位:虚拟美元)", + "info.aideKey.total": "密钥总额", + "info.aideKey.used": "密钥消耗", + "info.aideKey.remain": "密钥余额", + "info.aideKey.callCount": "调用次数", + "info.aideKey.validUntil": "有效期至", + "info.aideKeyUsageStatusBar.text": "Aide 消耗", + "info.aideKeyUsageStatusBar.tooltip": "点击查看 Aide 密钥消耗信息", "input.array.promptEnding": "输入逗号分隔的值", "input.json.promptEnding": "输入 JSON 格式的值", "input.aiCommand.prompt": "输入 AI 命令的问题", diff --git a/src/ai/aide-key-request.ts b/src/ai/aide-key-request.ts new file mode 100644 index 0000000..c8b750b --- /dev/null +++ b/src/ai/aide-key-request.ts @@ -0,0 +1,170 @@ +// this file is for Aide third-party API query +type BaseRes = { + success: boolean + data: T + message: string +} + +type AideKeyUsageSearchParams = { + key: string + p: number // page + pageSize: number +} + +type AideKeyUsageSearchResDataItem = { + id: number + request_id: string + user_id: number + created_at: number + type: number + content: string + username: string + token_name: string + model_name: string + channel_name: string + quota: number + prompt_tokens: number + completion_tokens: number + channel: number + token_key: string + request_duration: number + response_first_byte_duration: number + total_duration: number + duration_for_view: number + is_stream: boolean + ip: string +} + +type AideKeyUsageSearchRes = BaseRes<{ + total: number + data: AideKeyUsageSearchResDataItem[] +}> + +export const aideKeyUsageSearch = (params: AideKeyUsageSearchParams) => + fetch( + `https://api.zyai.online/public/log/self/search?key=${params.key}&p=${params.p}&pageSize=${params.pageSize}`, + { + method: 'GET' + } + ).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.json() as Promise + }) + +type AideKeyUsageCountParams = { + key: string +} + +type AideKeyUsageCountResData = { + count: number +} + +type AideKeyUsageCountRes = BaseRes + +export const aideKeyUsageCount = (params: AideKeyUsageCountParams) => + fetch(`https://api.zyai.online/public/log/self/count?key=${params.key}`, { + method: 'GET' + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.json() as Promise + }) + +type AideKeyUsageStatParams = { + key: string +} + +type AideKeyUsageStatResData = { + mpm: number + quota: number + rpm: number + tpm: number +} + +type AideKeyUsageStatRes = BaseRes + +export const aideKeyUsageStat = (params: AideKeyUsageStatParams) => + fetch(`https://api.zyai.online/public/log/self/stat?key=${params.key}`, { + method: 'GET' + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.json() as Promise + }) + +export type AideKeyUsageSubscriptionParams = { + key: string +} + +export type AideKeyUsageSubscriptionRes = { + object: string + has_payment_method: boolean + soft_limit_usd: number + hard_limit_usd: number + system_hard_limit_usd: number + access_until: number + used_quota: number + remain_quota: number + used_count: number +} + +export const aideKeyUsageSubscription = ( + params: AideKeyUsageSubscriptionParams +) => + fetch(`https://api.zyai.online/public/dashboard/billing/subscription`, { + method: 'GET', + headers: { + Authorization: `Bearer ${params.key}` + } + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.json() as Promise + }) + +type AideKeyUsageInfoParams = { + key: string +} + +type AideKeyUsageInfoResData = { + count: AideKeyUsageCountResData + subscription: AideKeyUsageSubscriptionRes +} + +type AideKeyUsageInfoRes = BaseRes + +export const aideKeyUsageInfo = ( + params: AideKeyUsageInfoParams +): Promise => + Promise.all([aideKeyUsageCount(params), aideKeyUsageSubscription(params)]) + .then(([countRes, subscriptionRes]) => ({ + success: countRes.success, + data: { + count: countRes.data, + subscription: subscriptionRes + }, + message: 'Combined usage info retrieved successfully' + })) + .catch(error => ({ + success: false, + data: { + count: { count: 0 }, + subscription: { + object: '', + has_payment_method: false, + soft_limit_usd: 0, + hard_limit_usd: 0, + system_hard_limit_usd: 0, + access_until: 0, + used_quota: 0, + remain_quota: 0, + used_count: 0 + } + }, + message: `Error fetching usage info: ${error.message}` + })) diff --git a/src/commands/index.ts b/src/commands/index.ts index a6be285..71ad723 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -9,6 +9,7 @@ import { handleCopyAsPrompt } from './copy-as-prompt' import { handleCopyFileText } from './private/copy-file-text' import { handleQuickCloseFileWithoutSave } from './private/quick-close-file-without-save' import { handleReplaceFile } from './private/replace-file' +import { handleShowAideKeyUsageInfo } from './private/show-aide-key-usage-info' import { handleShowDiff } from './private/show-diff' import { handleRenameVariable } from './rename-variable' import { handleSmartPaste } from './smart-paste' @@ -72,6 +73,12 @@ export const registerCommands = async (context: vscode.ExtensionContext) => { commandErrorCatcher(handleShowDiff) ) + // private command + const showAideKeyUsageInfoDisposable = vscode.commands.registerCommand( + 'aide.showAideKeyUsageInfo', + commandErrorCatcher(handleShowAideKeyUsageInfo) + ) + context.subscriptions.push( copyDisposable, askAIDisposable, @@ -83,6 +90,7 @@ export const registerCommands = async (context: vscode.ExtensionContext) => { copyFileTextDisposable, quickCloseFileWithoutSaveDisposable, replaceFileDisposable, - showDiffDisposable + showDiffDisposable, + showAideKeyUsageInfoDisposable ) } diff --git a/src/commands/private/show-aide-key-usage-info.ts b/src/commands/private/show-aide-key-usage-info.ts new file mode 100644 index 0000000..103219c --- /dev/null +++ b/src/commands/private/show-aide-key-usage-info.ts @@ -0,0 +1,59 @@ +import { aideKeyUsageInfo } from '@/ai/aide-key-request' +import { getConfigKey } from '@/config' +import { t } from '@/i18n' +import { updateAideKeyUsageStatusBar } from '@/providers/aide-key-usage-statusbar' +import { formatNumber } from '@/utils' +import * as vscode from 'vscode' + +export const handleShowAideKeyUsageInfo = async () => { + const openaiBaseUrl = await getConfigKey('openaiBaseUrl') + const openaiKey = await getConfigKey('openaiKey') + + if (!openaiBaseUrl.includes('api.zyai.online')) + throw new Error(t('error.aideKeyUsageInfoOnlySupportAideModels')) + + // show loading + updateAideKeyUsageStatusBar(`$(sync~spin) ${t('info.loading')}`) + + try { + const result = await aideKeyUsageInfo({ key: openaiKey }) + + if (result.success) { + // create a nice message to show the result + const { count, subscription } = result.data + + const totalUSD = subscription.hard_limit_usd + const usedUSD = + (subscription.used_quota / subscription.remain_quota) * totalUSD + const remainUSD = totalUSD - usedUSD + const formatUSD = (amount: number) => `$${formatNumber(amount, 2)}` + const formatDate = (timestamp: number) => { + if (timestamp === 0) return t('info.aideKey.neverExpires') + return new Date(timestamp * 1000).toLocaleDateString() + } + + const message = `${t('info.aideKey.usageInfo')}: + +${t('info.aideKey.total')}: ${formatUSD(totalUSD)} + +${t('info.aideKey.used')}: ${formatUSD(usedUSD)} + +${t('info.aideKey.remain')}: ${formatUSD(remainUSD)} + +${t('info.aideKey.callCount')}: ${count.count} + +${t('info.aideKey.validUntil')}: ${formatDate(subscription.access_until)}` + + vscode.window.showInformationMessage(message, { + modal: true + }) + } else { + throw new Error(`Failed to fetch usage info: ${result.message}`) + } + } finally { + // restore the original text of the status bar item + updateAideKeyUsageStatusBar( + `$(info) ${t('info.aideKeyUsageStatusBar.text')}` + ) + } +} diff --git a/src/index.ts b/src/index.ts index e85e605..22276b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { initializeLocalization } from './i18n' import { logger } from './logger' import { enablePolyfill } from './polyfill' import { registerProviders } from './providers' +import { initAideKeyUsageStatusBar } from './providers/aide-key-usage-statusbar' import { redisStorage, stateStorage } from './storage' export const activate = async (context: vscode.ExtensionContext) => { @@ -26,6 +27,7 @@ export const activate = async (context: vscode.ExtensionContext) => { await registerCommands(context) await registerProviders(context) + await initAideKeyUsageStatusBar(context) await autoOpenCorrespondingFiles(context) await cleanup(context) } catch (err) { diff --git a/src/providers/aide-key-usage-statusbar.ts b/src/providers/aide-key-usage-statusbar.ts new file mode 100644 index 0000000..4024c1f --- /dev/null +++ b/src/providers/aide-key-usage-statusbar.ts @@ -0,0 +1,21 @@ +import { t } from '@/i18n' +import * as vscode from 'vscode' + +let aideKeyUsageStatusBar: vscode.StatusBarItem + +export const initAideKeyUsageStatusBar = (context: vscode.ExtensionContext) => { + aideKeyUsageStatusBar = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100 + ) + aideKeyUsageStatusBar.text = `$(info) ${t('info.aideKeyUsageStatusBar.text')}` + aideKeyUsageStatusBar.tooltip = t('info.aideKeyUsageStatusBar.tooltip') + aideKeyUsageStatusBar.command = 'aide.showAideKeyUsageInfo' + aideKeyUsageStatusBar.show() + + context.subscriptions.push(aideKeyUsageStatusBar) +} + +export const updateAideKeyUsageStatusBar = (text: string) => { + aideKeyUsageStatusBar.text = text +} diff --git a/src/utils.ts b/src/utils.ts index bf6b657..ca09588 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -122,6 +122,11 @@ export const getCurrentWorkspaceFolderEditor = ( // } // } +export const formatNumber = (num: number, fixed: number): string => { + const numString = num.toFixed(fixed) + return numString.replace(/\.?0+$/, '') +} + export const removeCodeBlockSyntax = (str: string): string => { if (!str) return '' return str diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts index 3b62f16..371a9ed 100644 --- a/website/.vitepress/theme/index.ts +++ b/website/.vitepress/theme/index.ts @@ -4,7 +4,7 @@ import type { App } from 'vue' import '@nolebase/vitepress-plugin-inline-link-preview/client/style.css' -import AideModelPrice from '../../components/AideModels/AideModelPrice.vue' +import AideModelPrice from '../../components/AideModels/AideModelPrice/index.vue' import AidePay from '../../components/AideModels/AidePay.vue' import Video from '../../components/Video.vue' diff --git a/website/components/AideModels/components/PriceTable.vue b/website/components/AideModels/AideModelPrice/PriceTable.vue similarity index 95% rename from website/components/AideModels/components/PriceTable.vue rename to website/components/AideModels/AideModelPrice/PriceTable.vue index 34f75b4..0fa9be7 100644 --- a/website/components/AideModels/components/PriceTable.vue +++ b/website/components/AideModels/AideModelPrice/PriceTable.vue @@ -22,7 +22,7 @@