mirror of
https://github.com/erbanku/obsidian-memos-ai-sync.git
synced 2026-07-22 06:53:32 +00:00
feat(i18n): complete UI and content localization
Add a centralized locale system for en-US, zh-CN, tr-TR, and ja-JP. Default UI language to English (US) and add migration normalization for legacy values. Localize settings UI, runtime status/notice text, prompts, and generated memo/weekly-digest labels.
This commit is contained in:
parent
8848cd1343
commit
b8f8ea59c5
9 changed files with 798 additions and 256 deletions
|
|
@ -25,6 +25,7 @@ Original plugin by [leoleelxh](https://github.com/leoleelxh):
|
|||
|
||||
| Setting | Description |
|
||||
|---|---|
|
||||
| UI language | Plugin UI language (English (US), Chinese (Simplified), Turkish, Japanese). Default: `English (US)` |
|
||||
| Memos URL | Base URL of your Memos instance, e.g. `https://demo.usememos.com/` |
|
||||
| Access token | Personal access token from Memos Settings |
|
||||
| Sync directory | Vault folder where notes are saved (default: `memos`) |
|
||||
|
|
|
|||
59
main.ts
59
main.ts
|
|
@ -1,5 +1,5 @@
|
|||
import { Plugin } from 'obsidian';
|
||||
import type { MemosPluginSettings } from 'src/models/settings';
|
||||
import type { MemosPluginSettings, UiLanguage } from 'src/models/settings';
|
||||
import { DEFAULT_SETTINGS } from 'src/models/settings';
|
||||
import { MemosSyncSettingTab } from 'src/ui/settings-tab';
|
||||
import { MemosService } from 'src/services/memos-service';
|
||||
|
|
@ -8,6 +8,7 @@ import { ContentService } from 'src/services/content-service';
|
|||
import { StatusService } from 'src/services/status-service';
|
||||
import type { AIService } from 'src/services/ai-service';
|
||||
import { createAIService, createDummyAIService } from 'src/services/ai-service';
|
||||
import { t } from 'src/i18n';
|
||||
|
||||
export default class MemosSyncPlugin extends Plugin {
|
||||
settings: MemosPluginSettings;
|
||||
|
|
@ -19,9 +20,8 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// 创建状态栏项
|
||||
const statusBarItem = this.addStatusBarItem();
|
||||
this.statusService = new StatusService(statusBarItem);
|
||||
this.statusService = new StatusService(statusBarItem, this.settings.uiLanguage);
|
||||
|
||||
this.initializeServices();
|
||||
|
||||
|
|
@ -47,19 +47,17 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
let aiService: AIService | null = null;
|
||||
if (this.settings.ai.enabled) {
|
||||
try {
|
||||
// 如果选择了自定义模型,使用自定义模型名称
|
||||
const modelName = this.settings.ai.modelName === 'custom'
|
||||
? this.settings.ai.customModelName
|
||||
: this.settings.ai.modelName;
|
||||
|
||||
// 对于 Ollama,使用 ollamaBaseUrl 作为 apiKey
|
||||
const apiKey = this.settings.ai.modelType === 'ollama'
|
||||
? this.settings.ai.ollamaBaseUrl
|
||||
: this.settings.ai.apiKey;
|
||||
|
||||
if (this.settings.ai.modelType !== 'ollama' && !apiKey) {
|
||||
aiService = createDummyAIService();
|
||||
this.statusService.setWarning('AI 服务需要配置 API 密钥,请在设置中完成配置');
|
||||
this.statusService.setWarning(t(this.settings.uiLanguage, 'main.aiKeyRequired'));
|
||||
} else {
|
||||
aiService = createAIService(
|
||||
this.settings.ai.modelType,
|
||||
|
|
@ -70,8 +68,7 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize AI service:', error);
|
||||
this.statusService.setWarning('AI 服务初始化失败,请检查配置');
|
||||
// 不禁用 AI 功能,使用空服务
|
||||
this.statusService.setWarning(t(this.settings.uiLanguage, 'main.aiInitFailed'));
|
||||
aiService = createDummyAIService();
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +79,7 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
this.settings.ai.intelligentSummary,
|
||||
this.settings.ai.autoTags,
|
||||
this.settings.ai.summaryLanguage,
|
||||
this.settings.uiLanguage,
|
||||
this.app.vault,
|
||||
this.settings.syncDirectory
|
||||
);
|
||||
|
|
@ -89,17 +87,18 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
this.fileService = new FileService(
|
||||
this.app.vault,
|
||||
this.settings.syncDirectory,
|
||||
this.memosService
|
||||
this.memosService,
|
||||
this.settings.uiLanguage
|
||||
);
|
||||
}
|
||||
|
||||
async syncMemos() {
|
||||
try {
|
||||
if (!this.settings.memosApiUrl) {
|
||||
throw new Error('未配置 Memos API URL');
|
||||
throw new Error(t(this.settings.uiLanguage, 'main.missingMemosUrl'));
|
||||
}
|
||||
if (!this.settings.memosAccessToken) {
|
||||
throw new Error('未配置访问令牌');
|
||||
throw new Error(t(this.settings.uiLanguage, 'main.missingAccessToken'));
|
||||
}
|
||||
|
||||
this.statusService.startSync(0);
|
||||
|
|
@ -116,19 +115,21 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
}
|
||||
|
||||
if (this.settings.ai.enabled && this.settings.ai.weeklyDigest) {
|
||||
this.statusService.updateProgress(syncCount, '正在生成每周总结...');
|
||||
this.statusService.updateProgress(syncCount, t(this.settings.uiLanguage, 'main.generatingWeeklyDigest'));
|
||||
await this.contentService.generateWeeklyDigest(memos);
|
||||
}
|
||||
|
||||
this.statusService.setSuccess(`同步完成,共同步 ${syncCount} 条记录`);
|
||||
this.statusService.setSuccess(t(this.settings.uiLanguage, 'main.syncCompleted', { count: syncCount }));
|
||||
} catch (error) {
|
||||
console.error('同步失败:', error);
|
||||
this.statusService.setError(error.message);
|
||||
console.error('Sync failed:', error);
|
||||
this.statusService.setError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.settings.uiLanguage = this.normalizeUiLanguage(this.settings.uiLanguage);
|
||||
this.settings.ai.summaryLanguage = this.normalizeSummaryLanguage(this.settings.ai.summaryLanguage);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
@ -140,4 +141,32 @@ export default class MemosSyncPlugin extends Plugin {
|
|||
const interval = this.settings.autoSyncInterval * 60 * 1000;
|
||||
setInterval(() => { void this.syncMemos(); }, interval);
|
||||
}
|
||||
|
||||
private normalizeSummaryLanguage(language: unknown): MemosPluginSettings['ai']['summaryLanguage'] {
|
||||
const normalized = this.normalizeUiLanguage(language);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private normalizeUiLanguage(language: unknown): UiLanguage {
|
||||
if (typeof language !== 'string') {
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
const legacyMap: Record<string, UiLanguage> = {
|
||||
zh: 'zh-CN',
|
||||
en: 'en-US',
|
||||
tr: 'tr-TR',
|
||||
ja: 'ja-JP',
|
||||
ko: 'en-US'
|
||||
};
|
||||
if (legacyMap[language]) {
|
||||
return legacyMap[language];
|
||||
}
|
||||
|
||||
if (language === 'en-US' || language === 'zh-CN' || language === 'tr-TR' || language === 'ja-JP') {
|
||||
return language;
|
||||
}
|
||||
|
||||
return 'en-US';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
472
src/i18n/index.ts
Normal file
472
src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
import type { UiLanguage } from '../models/settings';
|
||||
|
||||
type TranslationKey =
|
||||
| 'language.en-US'
|
||||
| 'language.zh-CN'
|
||||
| 'language.tr-TR'
|
||||
| 'language.ja-JP'
|
||||
| 'settings.uiLanguage.name'
|
||||
| 'settings.uiLanguage.desc'
|
||||
| 'settings.memosUrl.name'
|
||||
| 'settings.memosUrl.desc'
|
||||
| 'settings.accessToken.name'
|
||||
| 'settings.accessToken.desc'
|
||||
| 'settings.syncDirectory.name'
|
||||
| 'settings.syncDirectory.desc'
|
||||
| 'settings.syncMode.name'
|
||||
| 'settings.syncMode.desc'
|
||||
| 'settings.syncMode.manual'
|
||||
| 'settings.syncMode.auto'
|
||||
| 'settings.syncInterval.name'
|
||||
| 'settings.syncInterval.desc'
|
||||
| 'settings.syncLimit.name'
|
||||
| 'settings.syncLimit.desc'
|
||||
| 'settings.syncAfter.name'
|
||||
| 'settings.syncAfter.desc'
|
||||
| 'settings.aiEnabled.name'
|
||||
| 'settings.aiEnabled.desc'
|
||||
| 'settings.aiModel.name'
|
||||
| 'settings.aiModel.desc'
|
||||
| 'settings.apiKey.name'
|
||||
| 'settings.apiKey.desc'
|
||||
| 'settings.weeklyDigest.name'
|
||||
| 'settings.weeklyDigest.desc'
|
||||
| 'settings.autoTags.name'
|
||||
| 'settings.autoTags.desc'
|
||||
| 'settings.intelligentSummary.name'
|
||||
| 'settings.intelligentSummary.desc'
|
||||
| 'settings.summaryLanguage.name'
|
||||
| 'settings.summaryLanguage.desc'
|
||||
| 'settings.geminiModel.name'
|
||||
| 'settings.geminiModel.desc'
|
||||
| 'settings.openaiModel.name'
|
||||
| 'settings.openaiModel.desc'
|
||||
| 'settings.claudeModel.name'
|
||||
| 'settings.claudeModel.desc'
|
||||
| 'settings.ollamaBaseUrl.name'
|
||||
| 'settings.ollamaBaseUrl.desc'
|
||||
| 'settings.ollamaModel.name'
|
||||
| 'settings.ollamaModel.desc'
|
||||
| 'settings.customModel.name'
|
||||
| 'settings.customModel.desc'
|
||||
| 'settings.openaiBaseUrl.name'
|
||||
| 'settings.openaiBaseUrl.desc'
|
||||
| 'settings.placeholder.enterAccessToken'
|
||||
| 'settings.placeholder.enterApiKey'
|
||||
| 'status.syncing'
|
||||
| 'status.syncFailed'
|
||||
| 'status.syncComplete'
|
||||
| 'status.warning'
|
||||
| 'status.idle'
|
||||
| 'notice.syncingMemos'
|
||||
| 'notice.syncFailed'
|
||||
| 'main.aiKeyRequired'
|
||||
| 'main.aiInitFailed'
|
||||
| 'main.missingMemosUrl'
|
||||
| 'main.missingAccessToken'
|
||||
| 'main.generatingWeeklyDigest'
|
||||
| 'main.syncCompleted'
|
||||
| 'file.attachments'
|
||||
| 'file.memoProperties'
|
||||
| 'file.created'
|
||||
| 'file.updated'
|
||||
| 'file.type'
|
||||
| 'file.tags'
|
||||
| 'file.visibility'
|
||||
| 'file.pinned'
|
||||
| 'content.summaryHeading'
|
||||
| 'content.tagsHeading'
|
||||
| 'content.weeklyFileName'
|
||||
| 'content.weeklyTitle'
|
||||
| 'content.highlights'
|
||||
| 'content.stats'
|
||||
| 'content.memoCount'
|
||||
| 'content.timeRange'
|
||||
| 'content.nextWeekOutlook'
|
||||
| 'content.quoteTitle'
|
||||
| 'content.quoteText'
|
||||
| 'content.generatedAt';
|
||||
|
||||
type TranslationSet = Record<TranslationKey, string>;
|
||||
|
||||
const translations: Record<UiLanguage, TranslationSet> = {
|
||||
'en-US': {
|
||||
'language.en-US': 'English (US)',
|
||||
'language.zh-CN': 'Chinese (Simplified)',
|
||||
'language.tr-TR': 'Turkish',
|
||||
'language.ja-JP': 'Japanese',
|
||||
'settings.uiLanguage.name': 'UI language',
|
||||
'settings.uiLanguage.desc': 'Select the display language for plugin UI text',
|
||||
'settings.memosUrl.name': 'Memos URL',
|
||||
'settings.memosUrl.desc': 'Memos server base URL',
|
||||
'settings.accessToken.name': 'Access token',
|
||||
'settings.accessToken.desc': 'Your memos API access token',
|
||||
'settings.syncDirectory.name': 'Sync directory',
|
||||
'settings.syncDirectory.desc': 'Folder in Obsidian where memos content will be stored',
|
||||
'settings.syncMode.name': 'Sync mode',
|
||||
'settings.syncMode.desc': 'Choose manual or automatic sync',
|
||||
'settings.syncMode.manual': 'Manual',
|
||||
'settings.syncMode.auto': 'Automatic',
|
||||
'settings.syncInterval.name': 'Sync interval',
|
||||
'settings.syncInterval.desc': 'Interval between automatic syncs (minutes)',
|
||||
'settings.syncLimit.name': 'Sync limit',
|
||||
'settings.syncLimit.desc': 'Maximum number of memos to fetch per sync.',
|
||||
'settings.syncAfter.name': 'Sync after date',
|
||||
'settings.syncAfter.desc': 'Only sync memos created on or after this date (yyyy-mm-dd). Leave empty to sync all.',
|
||||
'settings.aiEnabled.name': 'Enable AI features',
|
||||
'settings.aiEnabled.desc': 'Toggle AI-enhanced processing',
|
||||
'settings.aiModel.name': 'AI model',
|
||||
'settings.aiModel.desc': 'Select the AI model provider',
|
||||
'settings.apiKey.name': 'API key',
|
||||
'settings.apiKey.desc': 'Your AI service API key',
|
||||
'settings.weeklyDigest.name': 'Weekly digest',
|
||||
'settings.weeklyDigest.desc': 'Automatically generate a weekly summary',
|
||||
'settings.autoTags.name': 'Auto tags',
|
||||
'settings.autoTags.desc': 'Automatically generate tags based on content',
|
||||
'settings.intelligentSummary.name': 'Intelligent summary',
|
||||
'settings.intelligentSummary.desc': 'Automatically generate content summaries',
|
||||
'settings.summaryLanguage.name': 'Summary language',
|
||||
'settings.summaryLanguage.desc': 'Select the language for generated summaries',
|
||||
'settings.geminiModel.name': 'Gemini model',
|
||||
'settings.geminiModel.desc': 'Select the Gemini model to use',
|
||||
'settings.openaiModel.name': 'OpenAI model',
|
||||
'settings.openaiModel.desc': 'Select the OpenAI model to use',
|
||||
'settings.claudeModel.name': 'Claude model',
|
||||
'settings.claudeModel.desc': 'Select the Claude model to use',
|
||||
'settings.ollamaBaseUrl.name': 'Ollama base URL',
|
||||
'settings.ollamaBaseUrl.desc': 'Base URL for the Ollama service (default: http://localhost:11434)',
|
||||
'settings.ollamaModel.name': 'Ollama model',
|
||||
'settings.ollamaModel.desc': 'Select the Ollama model to use',
|
||||
'settings.customModel.name': 'Custom model name',
|
||||
'settings.customModel.desc': 'Enter the name of the custom model',
|
||||
'settings.openaiBaseUrl.name': 'OpenAI API base URL',
|
||||
'settings.openaiBaseUrl.desc': 'Base URL for custom API services',
|
||||
'settings.placeholder.enterAccessToken': 'Enter access token',
|
||||
'settings.placeholder.enterApiKey': 'Enter API key',
|
||||
'status.syncing': 'Syncing',
|
||||
'status.syncFailed': 'Sync failed',
|
||||
'status.syncComplete': 'Sync complete',
|
||||
'status.warning': 'Warning',
|
||||
'status.idle': 'Idle',
|
||||
'notice.syncingMemos': 'Syncing memos',
|
||||
'notice.syncFailed': 'Sync failed: {error}',
|
||||
'main.aiKeyRequired': 'AI service requires an API key. Configure it in settings.',
|
||||
'main.aiInitFailed': 'Failed to initialize AI service. Check your configuration.',
|
||||
'main.missingMemosUrl': 'Memos API URL is not configured',
|
||||
'main.missingAccessToken': 'Access token is not configured',
|
||||
'main.generatingWeeklyDigest': 'Generating weekly digest...',
|
||||
'main.syncCompleted': 'Sync complete, synced {count} memos',
|
||||
'file.attachments': 'Attachments',
|
||||
'file.memoProperties': 'Memo Properties',
|
||||
'file.created': 'Created',
|
||||
'file.updated': 'Updated',
|
||||
'file.type': 'Type',
|
||||
'file.tags': 'Tags',
|
||||
'file.visibility': 'Visibility',
|
||||
'file.pinned': 'Pinned',
|
||||
'content.summaryHeading': 'Summary',
|
||||
'content.tagsHeading': 'Tags',
|
||||
'content.weeklyFileName': 'Week {week} Digest.md',
|
||||
'content.weeklyTitle': 'Week {week} Review ({range})',
|
||||
'content.highlights': 'Weekly Highlights',
|
||||
'content.stats': 'Statistics',
|
||||
'content.memoCount': 'Memos: {count}',
|
||||
'content.timeRange': 'Range: {range}',
|
||||
'content.nextWeekOutlook': 'Next Week Outlook',
|
||||
'content.quoteTitle': 'Quote',
|
||||
'content.quoteText': 'Every present moment is the starting point of the future.',
|
||||
'content.generatedAt': 'Generated at: {time}'
|
||||
},
|
||||
'zh-CN': {
|
||||
'language.en-US': '英语(美国)',
|
||||
'language.zh-CN': '简体中文',
|
||||
'language.tr-TR': '土耳其语',
|
||||
'language.ja-JP': '日语',
|
||||
'settings.uiLanguage.name': '界面语言',
|
||||
'settings.uiLanguage.desc': '选择插件界面的显示语言',
|
||||
'settings.memosUrl.name': 'Memos 地址',
|
||||
'settings.memosUrl.desc': 'Memos 服务基础地址',
|
||||
'settings.accessToken.name': '访问令牌',
|
||||
'settings.accessToken.desc': '你的 Memos API 访问令牌',
|
||||
'settings.syncDirectory.name': '同步目录',
|
||||
'settings.syncDirectory.desc': '在 Obsidian 中存放 Memo 内容的文件夹',
|
||||
'settings.syncMode.name': '同步模式',
|
||||
'settings.syncMode.desc': '选择手动或自动同步',
|
||||
'settings.syncMode.manual': '手动',
|
||||
'settings.syncMode.auto': '自动',
|
||||
'settings.syncInterval.name': '同步间隔',
|
||||
'settings.syncInterval.desc': '自动同步的时间间隔(分钟)',
|
||||
'settings.syncLimit.name': '同步数量上限',
|
||||
'settings.syncLimit.desc': '每次同步最多拉取的 Memo 数量。',
|
||||
'settings.syncAfter.name': '起始同步日期',
|
||||
'settings.syncAfter.desc': '仅同步该日期及之后创建的 Memo(yyyy-mm-dd)。留空则同步全部。',
|
||||
'settings.aiEnabled.name': '启用 AI 功能',
|
||||
'settings.aiEnabled.desc': '启用 AI 增强处理',
|
||||
'settings.aiModel.name': 'AI 模型',
|
||||
'settings.aiModel.desc': '选择 AI 模型提供方',
|
||||
'settings.apiKey.name': 'API 密钥',
|
||||
'settings.apiKey.desc': '你的 AI 服务 API 密钥',
|
||||
'settings.weeklyDigest.name': '每周总结',
|
||||
'settings.weeklyDigest.desc': '自动生成每周总结',
|
||||
'settings.autoTags.name': '自动标签',
|
||||
'settings.autoTags.desc': '根据内容自动生成标签',
|
||||
'settings.intelligentSummary.name': '智能摘要',
|
||||
'settings.intelligentSummary.desc': '自动生成内容摘要',
|
||||
'settings.summaryLanguage.name': '摘要语言',
|
||||
'settings.summaryLanguage.desc': '选择 AI 生成摘要的语言',
|
||||
'settings.geminiModel.name': 'Gemini 模型',
|
||||
'settings.geminiModel.desc': '选择要使用的 Gemini 模型',
|
||||
'settings.openaiModel.name': 'OpenAI 模型',
|
||||
'settings.openaiModel.desc': '选择要使用的 OpenAI 模型',
|
||||
'settings.claudeModel.name': 'Claude 模型',
|
||||
'settings.claudeModel.desc': '选择要使用的 Claude 模型',
|
||||
'settings.ollamaBaseUrl.name': 'Ollama 服务地址',
|
||||
'settings.ollamaBaseUrl.desc': 'Ollama 服务基础地址(默认: http://localhost:11434)',
|
||||
'settings.ollamaModel.name': 'Ollama 模型',
|
||||
'settings.ollamaModel.desc': '选择要使用的 Ollama 模型',
|
||||
'settings.customModel.name': '自定义模型名称',
|
||||
'settings.customModel.desc': '输入自定义模型名称',
|
||||
'settings.openaiBaseUrl.name': 'OpenAI API 基础地址',
|
||||
'settings.openaiBaseUrl.desc': '自定义 API 服务地址',
|
||||
'settings.placeholder.enterAccessToken': '输入访问令牌',
|
||||
'settings.placeholder.enterApiKey': '输入 API 密钥',
|
||||
'status.syncing': '同步中',
|
||||
'status.syncFailed': '同步失败',
|
||||
'status.syncComplete': '同步完成',
|
||||
'status.warning': '警告',
|
||||
'status.idle': '空闲',
|
||||
'notice.syncingMemos': '正在同步 Memo',
|
||||
'notice.syncFailed': '同步失败:{error}',
|
||||
'main.aiKeyRequired': 'AI 服务需要 API 密钥,请在设置中配置。',
|
||||
'main.aiInitFailed': 'AI 服务初始化失败,请检查配置。',
|
||||
'main.missingMemosUrl': '未配置 Memos API URL',
|
||||
'main.missingAccessToken': '未配置访问令牌',
|
||||
'main.generatingWeeklyDigest': '正在生成每周总结...',
|
||||
'main.syncCompleted': '同步完成,共同步 {count} 条记录',
|
||||
'file.attachments': '附件',
|
||||
'file.memoProperties': 'Memo 属性',
|
||||
'file.created': '创建时间',
|
||||
'file.updated': '更新时间',
|
||||
'file.type': '类型',
|
||||
'file.tags': '标签',
|
||||
'file.visibility': '可见性',
|
||||
'file.pinned': '置顶',
|
||||
'content.summaryHeading': '内容摘要',
|
||||
'content.tagsHeading': '相关标签',
|
||||
'content.weeklyFileName': '第{week}周总结.md',
|
||||
'content.weeklyTitle': '第 {week} 周回顾({range})',
|
||||
'content.highlights': '本周亮点',
|
||||
'content.stats': '统计数据',
|
||||
'content.memoCount': '记录数量:{count} 条',
|
||||
'content.timeRange': '时间范围:{range}',
|
||||
'content.nextWeekOutlook': '下周展望',
|
||||
'content.quoteTitle': '激励语录',
|
||||
'content.quoteText': '每一个当下都是未来的起点,让我们继续前行。',
|
||||
'content.generatedAt': '生成时间:{time}'
|
||||
},
|
||||
'tr-TR': {
|
||||
'language.en-US': 'İngilizce (ABD)',
|
||||
'language.zh-CN': 'Çince (Basitleştirilmiş)',
|
||||
'language.tr-TR': 'Türkçe',
|
||||
'language.ja-JP': 'Japonca',
|
||||
'settings.uiLanguage.name': 'Arayüz dili',
|
||||
'settings.uiLanguage.desc': 'Eklenti arayüz metin dili',
|
||||
'settings.memosUrl.name': 'Memos URL',
|
||||
'settings.memosUrl.desc': 'Memos sunucu temel URL adresi',
|
||||
'settings.accessToken.name': 'Erişim belirteci',
|
||||
'settings.accessToken.desc': 'Memos API erişim belirteciniz',
|
||||
'settings.syncDirectory.name': 'Senkronizasyon klasörü',
|
||||
'settings.syncDirectory.desc': 'Memos içeriğinin Obsidian içinde kaydedileceği klasör',
|
||||
'settings.syncMode.name': 'Senkronizasyon modu',
|
||||
'settings.syncMode.desc': 'Manuel veya otomatik senkronizasyon seçin',
|
||||
'settings.syncMode.manual': 'Manuel',
|
||||
'settings.syncMode.auto': 'Otomatik',
|
||||
'settings.syncInterval.name': 'Senkronizasyon aralığı',
|
||||
'settings.syncInterval.desc': 'Otomatik senkronizasyon aralığı (dakika)',
|
||||
'settings.syncLimit.name': 'Senkronizasyon limiti',
|
||||
'settings.syncLimit.desc': 'Her senkronizasyonda çekilecek en fazla memo sayısı.',
|
||||
'settings.syncAfter.name': 'Bu tarihten sonra senkronize et',
|
||||
'settings.syncAfter.desc': 'Sadece bu tarihte veya sonrasında oluşturulan memoları senkronize et (yyyy-mm-dd).',
|
||||
'settings.aiEnabled.name': 'AI özelliklerini etkinleştir',
|
||||
'settings.aiEnabled.desc': 'AI destekli işlemeyi aç/kapat',
|
||||
'settings.aiModel.name': 'AI modeli',
|
||||
'settings.aiModel.desc': 'AI model sağlayıcısını seçin',
|
||||
'settings.apiKey.name': 'API anahtarı',
|
||||
'settings.apiKey.desc': 'AI servis API anahtarınız',
|
||||
'settings.weeklyDigest.name': 'Haftalık özet',
|
||||
'settings.weeklyDigest.desc': 'Haftalık özeti otomatik oluştur',
|
||||
'settings.autoTags.name': 'Otomatik etiketler',
|
||||
'settings.autoTags.desc': 'İçeriğe göre otomatik etiket üret',
|
||||
'settings.intelligentSummary.name': 'Akıllı özet',
|
||||
'settings.intelligentSummary.desc': 'İçerik özetlerini otomatik oluştur',
|
||||
'settings.summaryLanguage.name': 'Özet dili',
|
||||
'settings.summaryLanguage.desc': 'Üretilen özetlerin dilini seçin',
|
||||
'settings.geminiModel.name': 'Gemini modeli',
|
||||
'settings.geminiModel.desc': 'Kullanılacak Gemini modelini seçin',
|
||||
'settings.openaiModel.name': 'OpenAI modeli',
|
||||
'settings.openaiModel.desc': 'Kullanılacak OpenAI modelini seçin',
|
||||
'settings.claudeModel.name': 'Claude modeli',
|
||||
'settings.claudeModel.desc': 'Kullanılacak Claude modelini seçin',
|
||||
'settings.ollamaBaseUrl.name': 'Ollama temel URL',
|
||||
'settings.ollamaBaseUrl.desc': 'Ollama servis temel URL adresi (varsayılan: http://localhost:11434)',
|
||||
'settings.ollamaModel.name': 'Ollama modeli',
|
||||
'settings.ollamaModel.desc': 'Kullanılacak Ollama modelini seçin',
|
||||
'settings.customModel.name': 'Özel model adı',
|
||||
'settings.customModel.desc': 'Özel model adını girin',
|
||||
'settings.openaiBaseUrl.name': 'OpenAI API temel URL',
|
||||
'settings.openaiBaseUrl.desc': 'Özel API servisleri için temel URL',
|
||||
'settings.placeholder.enterAccessToken': 'Erişim belirtecini girin',
|
||||
'settings.placeholder.enterApiKey': 'API anahtarını girin',
|
||||
'status.syncing': 'Senkronize ediliyor',
|
||||
'status.syncFailed': 'Senkronizasyon başarısız',
|
||||
'status.syncComplete': 'Senkronizasyon tamamlandı',
|
||||
'status.warning': 'Uyarı',
|
||||
'status.idle': 'Boşta',
|
||||
'notice.syncingMemos': 'Memolar senkronize ediliyor',
|
||||
'notice.syncFailed': 'Senkronizasyon başarısız: {error}',
|
||||
'main.aiKeyRequired': 'AI servisi API anahtarı gerektirir. Ayarlardan yapılandırın.',
|
||||
'main.aiInitFailed': 'AI servisi başlatılamadı. Yapılandırmayı kontrol edin.',
|
||||
'main.missingMemosUrl': 'Memos API URL yapılandırılmamış',
|
||||
'main.missingAccessToken': 'Erişim belirteci yapılandırılmamış',
|
||||
'main.generatingWeeklyDigest': 'Haftalık özet oluşturuluyor...',
|
||||
'main.syncCompleted': 'Senkronizasyon tamamlandı, {count} memo senkronize edildi',
|
||||
'file.attachments': 'Ekler',
|
||||
'file.memoProperties': 'Memo Özellikleri',
|
||||
'file.created': 'Oluşturulma',
|
||||
'file.updated': 'Güncelleme',
|
||||
'file.type': 'Tür',
|
||||
'file.tags': 'Etiketler',
|
||||
'file.visibility': 'Görünürlük',
|
||||
'file.pinned': 'Sabitlenmiş',
|
||||
'content.summaryHeading': 'Özet',
|
||||
'content.tagsHeading': 'Etiketler',
|
||||
'content.weeklyFileName': 'Hafta-{week}-Ozeti.md',
|
||||
'content.weeklyTitle': '{week}. Hafta Değerlendirmesi ({range})',
|
||||
'content.highlights': 'Haftanın Öne Çıkanları',
|
||||
'content.stats': 'İstatistikler',
|
||||
'content.memoCount': 'Memo sayısı: {count}',
|
||||
'content.timeRange': 'Aralık: {range}',
|
||||
'content.nextWeekOutlook': 'Gelecek Hafta Görünümü',
|
||||
'content.quoteTitle': 'Alıntı',
|
||||
'content.quoteText': 'Her an, geleceğin başlangıç noktasıdır.',
|
||||
'content.generatedAt': 'Oluşturulma zamanı: {time}'
|
||||
},
|
||||
'ja-JP': {
|
||||
'language.en-US': '英語(米国)',
|
||||
'language.zh-CN': '中国語(簡体字)',
|
||||
'language.tr-TR': 'トルコ語',
|
||||
'language.ja-JP': '日本語',
|
||||
'settings.uiLanguage.name': 'UI 言語',
|
||||
'settings.uiLanguage.desc': 'プラグイン UI テキストの表示言語を選択',
|
||||
'settings.memosUrl.name': 'Memos URL',
|
||||
'settings.memosUrl.desc': 'Memos サーバーのベース URL',
|
||||
'settings.accessToken.name': 'アクセストークン',
|
||||
'settings.accessToken.desc': 'Memos API のアクセストークン',
|
||||
'settings.syncDirectory.name': '同期ディレクトリ',
|
||||
'settings.syncDirectory.desc': 'Obsidian 内でメモ内容を保存するフォルダ',
|
||||
'settings.syncMode.name': '同期モード',
|
||||
'settings.syncMode.desc': '手動または自動同期を選択',
|
||||
'settings.syncMode.manual': '手動',
|
||||
'settings.syncMode.auto': '自動',
|
||||
'settings.syncInterval.name': '同期間隔',
|
||||
'settings.syncInterval.desc': '自動同期の間隔(分)',
|
||||
'settings.syncLimit.name': '同期上限',
|
||||
'settings.syncLimit.desc': '1 回の同期で取得するメモの最大数。',
|
||||
'settings.syncAfter.name': '同期開始日',
|
||||
'settings.syncAfter.desc': 'この日付以降に作成されたメモのみ同期(yyyy-mm-dd)。空欄で全件。',
|
||||
'settings.aiEnabled.name': 'AI 機能を有効化',
|
||||
'settings.aiEnabled.desc': 'AI 強化処理を切り替え',
|
||||
'settings.aiModel.name': 'AI モデル',
|
||||
'settings.aiModel.desc': 'AI モデル提供元を選択',
|
||||
'settings.apiKey.name': 'API キー',
|
||||
'settings.apiKey.desc': 'AI サービス API キー',
|
||||
'settings.weeklyDigest.name': '週次ダイジェスト',
|
||||
'settings.weeklyDigest.desc': '週次サマリーを自動生成',
|
||||
'settings.autoTags.name': '自動タグ',
|
||||
'settings.autoTags.desc': '内容に基づいてタグを自動生成',
|
||||
'settings.intelligentSummary.name': 'インテリジェント要約',
|
||||
'settings.intelligentSummary.desc': '内容要約を自動生成',
|
||||
'settings.summaryLanguage.name': '要約言語',
|
||||
'settings.summaryLanguage.desc': '生成される要約の言語を選択',
|
||||
'settings.geminiModel.name': 'Gemini モデル',
|
||||
'settings.geminiModel.desc': '使用する Gemini モデルを選択',
|
||||
'settings.openaiModel.name': 'OpenAI モデル',
|
||||
'settings.openaiModel.desc': '使用する OpenAI モデルを選択',
|
||||
'settings.claudeModel.name': 'Claude モデル',
|
||||
'settings.claudeModel.desc': '使用する Claude モデルを選択',
|
||||
'settings.ollamaBaseUrl.name': 'Ollama ベース URL',
|
||||
'settings.ollamaBaseUrl.desc': 'Ollama サービスのベース URL(既定: http://localhost:11434)',
|
||||
'settings.ollamaModel.name': 'Ollama モデル',
|
||||
'settings.ollamaModel.desc': '使用する Ollama モデルを選択',
|
||||
'settings.customModel.name': 'カスタムモデル名',
|
||||
'settings.customModel.desc': 'カスタムモデル名を入力',
|
||||
'settings.openaiBaseUrl.name': 'OpenAI API ベース URL',
|
||||
'settings.openaiBaseUrl.desc': 'カスタム API サービス用ベース URL',
|
||||
'settings.placeholder.enterAccessToken': 'アクセストークンを入力',
|
||||
'settings.placeholder.enterApiKey': 'API キーを入力',
|
||||
'status.syncing': '同期中',
|
||||
'status.syncFailed': '同期失敗',
|
||||
'status.syncComplete': '同期完了',
|
||||
'status.warning': '警告',
|
||||
'status.idle': '待機中',
|
||||
'notice.syncingMemos': 'メモを同期中',
|
||||
'notice.syncFailed': '同期失敗: {error}',
|
||||
'main.aiKeyRequired': 'AI サービスには API キーが必要です。設定で構成してください。',
|
||||
'main.aiInitFailed': 'AI サービスの初期化に失敗しました。設定を確認してください。',
|
||||
'main.missingMemosUrl': 'Memos API URL が未設定です',
|
||||
'main.missingAccessToken': 'アクセストークンが未設定です',
|
||||
'main.generatingWeeklyDigest': '週次ダイジェストを生成中...',
|
||||
'main.syncCompleted': '同期完了、{count} 件のメモを同期しました',
|
||||
'file.attachments': '添付ファイル',
|
||||
'file.memoProperties': 'メモのプロパティ',
|
||||
'file.created': '作成日時',
|
||||
'file.updated': '更新日時',
|
||||
'file.type': 'タイプ',
|
||||
'file.tags': 'タグ',
|
||||
'file.visibility': '公開範囲',
|
||||
'file.pinned': 'ピン留め',
|
||||
'content.summaryHeading': '要約',
|
||||
'content.tagsHeading': 'タグ',
|
||||
'content.weeklyFileName': '第{week}週ダイジェスト.md',
|
||||
'content.weeklyTitle': '第 {week} 週レビュー({range})',
|
||||
'content.highlights': '今週のハイライト',
|
||||
'content.stats': '統計',
|
||||
'content.memoCount': 'メモ件数: {count}',
|
||||
'content.timeRange': '期間: {range}',
|
||||
'content.nextWeekOutlook': '来週の見通し',
|
||||
'content.quoteTitle': '引用',
|
||||
'content.quoteText': '今この瞬間は、未来の出発点です。',
|
||||
'content.generatedAt': '生成時刻: {time}'
|
||||
}
|
||||
};
|
||||
|
||||
export function t(language: UiLanguage, key: TranslationKey, vars?: Record<string, string | number>): string {
|
||||
const template = translations[language][key] ?? translations['en-US'][key] ?? key;
|
||||
if (!vars) {
|
||||
return template;
|
||||
}
|
||||
|
||||
return template.replace(/\{(\w+)\}/g, (_, token: string) => {
|
||||
const value = vars[token];
|
||||
return value === undefined ? `{${token}}` : String(value);
|
||||
});
|
||||
}
|
||||
|
||||
export function formatLocaleDateTime(language: UiLanguage, date: Date): string {
|
||||
return new Intl.DateTimeFormat(language, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatMonthDay(language: UiLanguage, date: Date): string {
|
||||
return new Intl.DateTimeFormat(language, {
|
||||
month: 'numeric',
|
||||
day: 'numeric'
|
||||
}).format(date);
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
export type RowStatus = 'NORMAL' | 'ARCHIVED' | 'DELETED';
|
||||
export type Visibility = 'PRIVATE' | 'PROTECTED' | 'PUBLIC';
|
||||
export type AIModelType = 'openai' | 'gemini' | 'claude' | 'ollama';
|
||||
export type SummaryLanguage = 'en-US' | 'zh-CN' | 'tr-TR' | 'ja-JP';
|
||||
export type UiLanguage = SummaryLanguage;
|
||||
|
||||
// v0.21 resource (attachment)
|
||||
export interface MemoResource {
|
||||
|
|
@ -39,7 +41,7 @@ export interface AISettings {
|
|||
weeklyDigest: boolean;
|
||||
autoTags: boolean;
|
||||
intelligentSummary: boolean;
|
||||
summaryLanguage: 'zh' | 'en' | 'ja' | 'ko';
|
||||
summaryLanguage: SummaryLanguage;
|
||||
}
|
||||
|
||||
// Plugin settings
|
||||
|
|
@ -47,6 +49,7 @@ export interface MemosPluginSettings {
|
|||
// Base URL of the Memos instance, e.g. https://demo.usememos.com/
|
||||
memosApiUrl: string;
|
||||
memosAccessToken: string;
|
||||
uiLanguage: UiLanguage;
|
||||
syncDirectory: string;
|
||||
syncFrequency: 'manual' | 'auto';
|
||||
autoSyncInterval: number;
|
||||
|
|
@ -59,6 +62,7 @@ export interface MemosPluginSettings {
|
|||
export const DEFAULT_SETTINGS: MemosPluginSettings = {
|
||||
memosApiUrl: '',
|
||||
memosAccessToken: '',
|
||||
uiLanguage: 'en-US',
|
||||
syncDirectory: 'memos',
|
||||
syncFrequency: 'manual',
|
||||
autoSyncInterval: 30,
|
||||
|
|
@ -75,6 +79,6 @@ export const DEFAULT_SETTINGS: MemosPluginSettings = {
|
|||
weeklyDigest: true,
|
||||
autoTags: true,
|
||||
intelligentSummary: true,
|
||||
summaryLanguage: 'zh'
|
||||
summaryLanguage: 'en-US'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import type { GenerativeModel } from '@google/generative-ai';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import OpenAI from 'openai';
|
||||
import { Notice, requestUrl } from 'obsidian';
|
||||
import { requestUrl } from 'obsidian';
|
||||
import type { SummaryLanguage } from '../models/settings';
|
||||
import { Logger } from './logger';
|
||||
|
||||
export interface AIService {
|
||||
generateSummary(content: string, language?: string): Promise<string>;
|
||||
generateTags(content: string): Promise<string[]>;
|
||||
generateWeeklyDigest(contents: string[]): Promise<string>;
|
||||
generateSummary(content: string, language?: SummaryLanguage): Promise<string>;
|
||||
generateTags(content: string, language?: SummaryLanguage): Promise<string[]>;
|
||||
generateWeeklyDigest(contents: string[], language?: SummaryLanguage): Promise<string>;
|
||||
initialize(apiKey: string, modelName?: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -18,11 +19,10 @@ export const GEMINI_MODELS = {
|
|||
'Gemini 1.0 Pro': 'gemini-1.0-pro',
|
||||
'Text Embedding': 'text-embedding-004',
|
||||
'AQA': 'aqa',
|
||||
'自定义模型': 'custom' // 新增:自定义模型选项
|
||||
'Custom model': 'custom'
|
||||
} as const;
|
||||
|
||||
export const OPENAI_MODELS = {
|
||||
// GPT-4o 系列
|
||||
'GPT-4o': 'gpt-4o',
|
||||
'GPT-4o (2024-11-20)': 'gpt-4o-2024-11-20',
|
||||
'GPT-4o Mini': 'gpt-4o-mini',
|
||||
|
|
@ -30,7 +30,7 @@ export const OPENAI_MODELS = {
|
|||
'GPT-4o Realtime': 'gpt-4o-realtime-preview',
|
||||
'GPT-4o Realtime (2024-10-01)': 'gpt-4o-realtime-preview-2024-10-01',
|
||||
'ChatGPT-4o Latest': 'chatgpt-4o-latest',
|
||||
'自定义模型': 'custom' // 已添加
|
||||
'Custom model': 'custom'
|
||||
} as const;
|
||||
|
||||
export const OLLAMA_MODELS = {
|
||||
|
|
@ -40,39 +40,99 @@ export const OLLAMA_MODELS = {
|
|||
'CodeLlama': 'codellama',
|
||||
'Phi': 'phi',
|
||||
'Neural Chat': 'neural-chat',
|
||||
'自定义模型': 'custom'
|
||||
'Custom model': 'custom'
|
||||
} as const;
|
||||
|
||||
export const MODEL_DESCRIPTIONS = {
|
||||
// Gemini Models
|
||||
'gemini-1.5-flash': '音频、图片、视频和文本',
|
||||
'gemini-1.5-flash-8b': '音频、图片、视频和文本',
|
||||
'gemini-1.5-pro': '音频、图片、视频和文本',
|
||||
'gemini-1.0-pro': '文本 (将于 2025 年 2 月 15 日弃用)',
|
||||
'text-embedding-004': '文本',
|
||||
'aqa': '文本',
|
||||
'custom': '自定义模型',
|
||||
|
||||
// OpenAI Models
|
||||
'gpt-4o': '标准版 GPT-4o,强大的推理能力',
|
||||
'gpt-4o-2024-11-20': '11月快照版本,稳定可靠',
|
||||
'gpt-4o-mini': '轻量级版本,性价比高',
|
||||
'gpt-4o-mini-2024-07-18': 'Mini 模型的稳定快照版本',
|
||||
'gpt-4o-realtime-preview': '实时预览版本,支持最新特性',
|
||||
'gpt-4o-realtime-preview-2024-10-01': '实时预览的稳定快照版本',
|
||||
'chatgpt-4o-latest': 'ChatGPT 使用的最新版本,持续更新',
|
||||
|
||||
// Ollama Models
|
||||
'llama2': 'Llama 2 - 通用大语言模型',
|
||||
'mistral': 'Mistral - 高性能开源模型',
|
||||
'mixtral': 'Mixtral - 混合专家模型',
|
||||
'codellama': 'CodeLlama - 代码生成专用模型',
|
||||
'phi': 'Phi - 轻量级模型',
|
||||
'neural-chat': 'Neural Chat - 对话优化模型'
|
||||
'gemini-1.5-flash': 'Audio, image, video, and text',
|
||||
'gemini-1.5-flash-8b': 'Audio, image, video, and text',
|
||||
'gemini-1.5-pro': 'Audio, image, video, and text',
|
||||
'gemini-1.0-pro': 'Text (deprecated on 2025-02-15)',
|
||||
'text-embedding-004': 'Text',
|
||||
'aqa': 'Text',
|
||||
'custom': 'Custom model',
|
||||
'gpt-4o': 'Standard GPT-4o model with strong reasoning',
|
||||
'gpt-4o-2024-11-20': 'Stable November snapshot',
|
||||
'gpt-4o-mini': 'Lightweight and cost-effective',
|
||||
'gpt-4o-mini-2024-07-18': 'Stable mini model snapshot',
|
||||
'gpt-4o-realtime-preview': 'Realtime preview with latest features',
|
||||
'gpt-4o-realtime-preview-2024-10-01': 'Stable realtime snapshot',
|
||||
'chatgpt-4o-latest': 'Continuously updated ChatGPT model',
|
||||
'llama2': 'General-purpose open model',
|
||||
'mistral': 'High-performance open model',
|
||||
'mixtral': 'Mixture-of-experts model',
|
||||
'codellama': 'Code-focused generation model',
|
||||
'phi': 'Lightweight model',
|
||||
'neural-chat': 'Dialogue-optimized model'
|
||||
} as const;
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY = 1000; // 1秒
|
||||
const RETRY_DELAY = 1000;
|
||||
const SUMMARY_LANGUAGE_LABELS: Record<SummaryLanguage, string> = {
|
||||
'en-US': 'English (US)',
|
||||
'zh-CN': '简体中文',
|
||||
'tr-TR': 'Türkçe',
|
||||
'ja-JP': '日本語'
|
||||
};
|
||||
|
||||
function createSummaryPrompt(content: string, language: SummaryLanguage = 'en-US'): string {
|
||||
const prompts: Record<SummaryLanguage, string> = {
|
||||
'en-US': `Summarize the key points of the following content in ${SUMMARY_LANGUAGE_LABELS[language]}:\n\n${content}`,
|
||||
'zh-CN': `请使用${SUMMARY_LANGUAGE_LABELS[language]}总结以下内容的要点:\n\n${content}`,
|
||||
'tr-TR': `Lutfen asagidaki icerigin ana noktalarini ${SUMMARY_LANGUAGE_LABELS[language]} dilinde ozetle:\n\n${content}`,
|
||||
'ja-JP': `次の内容の要点を${SUMMARY_LANGUAGE_LABELS[language]}で要約してください:\n\n${content}`
|
||||
};
|
||||
return prompts[language];
|
||||
}
|
||||
|
||||
function createTagsPrompt(content: string, language: SummaryLanguage = 'en-US'): string {
|
||||
const prompts: Record<SummaryLanguage, string> = {
|
||||
'en-US': `Generate 3-5 relevant tags for the following content (without #):\n\n${content}`,
|
||||
'zh-CN': `请为以下内容生成 3-5 个相关标签(不要带 #):\n\n${content}`,
|
||||
'tr-TR': `Asagidaki icerik icin 3-5 ilgili etiket uret ( # olmadan ):\n\n${content}`,
|
||||
'ja-JP': `次の内容に関連するタグを 3-5 個生成してください(# なし):\n\n${content}`
|
||||
};
|
||||
return prompts[language];
|
||||
}
|
||||
|
||||
function createWeeklyDigestPrompt(contents: string[], language: SummaryLanguage = 'en-US'): string {
|
||||
const combinedContent = contents.join('\n---\n');
|
||||
const prompts: Record<SummaryLanguage, string> = {
|
||||
'en-US': `Create a weekly report from the following content. Focus on:
|
||||
1. Main work items and outcomes
|
||||
2. Important updates and progress
|
||||
3. Problems and solutions
|
||||
4. Next week plan and outlook
|
||||
|
||||
Content:
|
||||
${combinedContent}`,
|
||||
'zh-CN': `请根据以下内容生成一份周报,重点包括:
|
||||
1. 主要工作内容和成果
|
||||
2. 重要事项和进展
|
||||
3. 问题和解决方案
|
||||
4. 下周计划和展望
|
||||
|
||||
内容:
|
||||
${combinedContent}`,
|
||||
'tr-TR': `Asagidaki icerikten haftalik rapor olustur. Sunlara odaklan:
|
||||
1. Ana isler ve ciktilar
|
||||
2. Onemli gelismeler ve ilerleme
|
||||
3. Sorunlar ve cozumler
|
||||
4. Gelecek hafta plani ve gorunum
|
||||
|
||||
Icerik:
|
||||
${combinedContent}`,
|
||||
'ja-JP': `次の内容から週報を作成してください。次の点に注目してください:
|
||||
1. 主な作業内容と成果
|
||||
2. 重要事項と進捗
|
||||
3. 課題と解決策
|
||||
4. 来週の計画と見通し
|
||||
|
||||
内容:
|
||||
${combinedContent}`
|
||||
};
|
||||
return prompts[language];
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
|
@ -88,22 +148,18 @@ async function retryWithBackoff<T>(
|
|||
return await operation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('429')) {
|
||||
// 配额限制错误,等待更长时间
|
||||
const delay = initialDelay * (2 ** i);
|
||||
// console.log(`配额限制,等待 ${delay}ms 后重试...`);
|
||||
await sleep(delay);
|
||||
continue;
|
||||
}
|
||||
if (i === maxRetries - 1) {
|
||||
throw error; // 最后一次重试失败,抛出错误
|
||||
throw error;
|
||||
}
|
||||
// 其他错误,继续重试
|
||||
const delay = initialDelay * (2 ** i);
|
||||
// console.log(`操作失败,等待 ${delay}ms 后重试...`);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
throw new Error('重试次数已达上限');
|
||||
throw new Error('Retry limit reached');
|
||||
}
|
||||
|
||||
class GeminiService implements AIService {
|
||||
|
|
@ -117,24 +173,24 @@ class GeminiService implements AIService {
|
|||
}
|
||||
|
||||
initialize(apiKey?: string, modelName?: string): Promise<void> {
|
||||
this.logger.debug('Gemini 服务初始化成功,使用模型:', this.model);
|
||||
this.logger.debug('Gemini service initialized with model:', this.model);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private handleRateLimit(error: Error, retryCount: number): number {
|
||||
const delay = Math.min(1000 * (2 ** retryCount), 30000);
|
||||
this.logger.warn(`配额限制,等待 ${delay}ms 后重试...`);
|
||||
this.logger.warn(`Rate limited, retrying in ${delay}ms...`);
|
||||
return delay;
|
||||
}
|
||||
|
||||
private handleError(error: Error, retryCount: number): number {
|
||||
const delay = Math.min(1000 * (2 ** retryCount), 30000);
|
||||
this.logger.error(`操作失败,等待 ${delay}ms 后重试...`, error);
|
||||
this.logger.error(`Operation failed, retrying in ${delay}ms...`, error);
|
||||
return delay;
|
||||
}
|
||||
|
||||
async generateSummary(content: string, language = 'zh'): Promise<string> {
|
||||
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
|
||||
async generateSummary(content: string, language: SummaryLanguage = 'en-US'): Promise<string> {
|
||||
const prompt = createSummaryPrompt(content, language);
|
||||
return retryWithBackoff(async () => {
|
||||
const result = await this.model.generateContent(prompt);
|
||||
const response = result.response;
|
||||
|
|
@ -142,8 +198,8 @@ class GeminiService implements AIService {
|
|||
});
|
||||
}
|
||||
|
||||
async generateTags(content: string): Promise<string[]> {
|
||||
const prompt = `请为以下内容生成3-5个相关标签(不要带#号):\n\n${content}`;
|
||||
async generateTags(content: string, language: SummaryLanguage = 'en-US'): Promise<string[]> {
|
||||
const prompt = createTagsPrompt(content, language);
|
||||
return retryWithBackoff(async () => {
|
||||
const result = await this.model.generateContent(prompt);
|
||||
const response = result.response;
|
||||
|
|
@ -151,15 +207,8 @@ class GeminiService implements AIService {
|
|||
});
|
||||
}
|
||||
|
||||
async generateWeeklyDigest(contents: string[]): Promise<string> {
|
||||
const combinedContent = contents.join('\n---\n');
|
||||
const prompt = `请对下一周的内容进行总结和分析,生成一份周报。重点关注:
|
||||
1. 主要工作内容和成果
|
||||
2. 重要事项和进展
|
||||
3. 问题和解决方案
|
||||
4. 下周计划和展望
|
||||
|
||||
内容:\n${combinedContent}`;
|
||||
async generateWeeklyDigest(contents: string[], language: SummaryLanguage = 'en-US'): Promise<string> {
|
||||
const prompt = createWeeklyDigestPrompt(contents, language);
|
||||
|
||||
return retryWithBackoff(async () => {
|
||||
const result = await this.model.generateContent(prompt);
|
||||
|
|
@ -175,12 +224,10 @@ export class OpenAIService implements AIService {
|
|||
private encryptionKey: Uint8Array;
|
||||
private logger: Logger;
|
||||
|
||||
// 生成随机 IV
|
||||
private generateIV(): Uint8Array {
|
||||
return crypto.getRandomValues(new Uint8Array(12));
|
||||
}
|
||||
|
||||
// 生成加密密钥
|
||||
private async generateKey(): Promise<CryptoKey> {
|
||||
return crypto.subtle.generateKey(
|
||||
{
|
||||
|
|
@ -192,7 +239,6 @@ export class OpenAIService implements AIService {
|
|||
);
|
||||
}
|
||||
|
||||
// 加密 API 密钥
|
||||
private async encryptApiKey(apiKey: string): Promise<string> {
|
||||
const iv = this.generateIV();
|
||||
const key = await this.generateKey();
|
||||
|
|
@ -211,7 +257,6 @@ export class OpenAIService implements AIService {
|
|||
return `${this.arrayBufferToBase64(iv)}:${this.arrayBufferToBase64(encryptedArray)}:${this.arrayBufferToBase64(await crypto.subtle.exportKey('raw', key))}`;
|
||||
}
|
||||
|
||||
// 解密 API 密钥
|
||||
private async decryptApiKey(encryptedKey: string): Promise<string> {
|
||||
const [ivStr, encryptedStr, keyStr] = encryptedKey.split(':');
|
||||
const iv = this.base64ToArrayBuffer(ivStr);
|
||||
|
|
@ -264,15 +309,13 @@ export class OpenAIService implements AIService {
|
|||
async initialize(apiKey?: string, modelName?: string, openaiBaseUrl?: string): Promise<void> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
throw new Error('API 密钥不能为空');
|
||||
throw new Error('API key is required');
|
||||
}
|
||||
|
||||
// 基本的 API 密钥格式验证
|
||||
if (!apiKey.startsWith('sk-') || apiKey.length < 20) {
|
||||
throw new Error('无效的 API 密钥格式');
|
||||
throw new Error('Invalid API key format');
|
||||
}
|
||||
|
||||
// 加密存储 API 密钥
|
||||
const encryptedKey = await this.encryptApiKey(apiKey);
|
||||
|
||||
this.client = new OpenAI({
|
||||
|
|
@ -281,26 +324,22 @@ export class OpenAIService implements AIService {
|
|||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
// 验证 API 密钥
|
||||
try {
|
||||
await this.client.models.list();
|
||||
} catch {
|
||||
throw new Error('API 密钥验证失败');
|
||||
throw new Error('API key validation failed');
|
||||
}
|
||||
|
||||
// 如果是自定义模型,使用 customModelName
|
||||
this.model = modelName || OPENAI_MODELS['GPT-4o'];
|
||||
this.logger.debug('OpenAI 服务初始化成功,使用模型:', this.model);
|
||||
new Notice(`AI 服务初始化成功`);
|
||||
this.logger.debug('OpenAI service initialized with model:', this.model);
|
||||
} catch (error) {
|
||||
this.logger.error('OpenAI 服务初始化失败:', error);
|
||||
new Notice(`AI 服务初始化失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
this.logger.error('OpenAI service initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async generateSummary(content: string, language = 'zh'): Promise<string> {
|
||||
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
|
||||
async generateSummary(content: string, language: SummaryLanguage = 'en-US'): Promise<string> {
|
||||
const prompt = createSummaryPrompt(content, language);
|
||||
return retryWithBackoff(async () => {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
|
|
@ -312,8 +351,8 @@ export class OpenAIService implements AIService {
|
|||
});
|
||||
}
|
||||
|
||||
async generateTags(content: string): Promise<string[]> {
|
||||
const prompt = `请为以下内容生成3-5个相关标签(不要带#号):\n\n${content}`;
|
||||
async generateTags(content: string, language: SummaryLanguage = 'en-US'): Promise<string[]> {
|
||||
const prompt = createTagsPrompt(content, language);
|
||||
return retryWithBackoff(async () => {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
|
|
@ -326,15 +365,8 @@ export class OpenAIService implements AIService {
|
|||
});
|
||||
}
|
||||
|
||||
async generateWeeklyDigest(contents: string[]): Promise<string> {
|
||||
const combinedContent = contents.join('\n---\n');
|
||||
const prompt = `请对以下一周的内容进行总结和分析,生成一份周报。要求:
|
||||
1. 主要工作内容和成果
|
||||
2. 重要事项和进展
|
||||
3. 问题和解决方案
|
||||
4. 下周计划和展望
|
||||
|
||||
内容:\n${combinedContent}`;
|
||||
async generateWeeklyDigest(contents: string[], language: SummaryLanguage = 'en-US'): Promise<string> {
|
||||
const prompt = createWeeklyDigestPrompt(contents, language);
|
||||
|
||||
return retryWithBackoff(async () => {
|
||||
const response = await this.client.chat.completions.create({
|
||||
|
|
@ -360,19 +392,19 @@ class OllamaService implements AIService {
|
|||
}
|
||||
|
||||
initialize(apiKey?: string, modelName?: string): Promise<void> {
|
||||
this.logger.debug('Ollama 服务初始化成功,使用模型:', this.model);
|
||||
this.logger.debug('Ollama service initialized with model:', this.model);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private handleRateLimit(error: Error, retryCount: number): number {
|
||||
const delay = Math.min(1000 * (2 ** retryCount), 30000);
|
||||
this.logger.warn(`配额限制,等待 ${delay}ms 后重试...`);
|
||||
this.logger.warn(`Rate limited, retrying in ${delay}ms...`);
|
||||
return delay;
|
||||
}
|
||||
|
||||
private handleError(error: Error, retryCount: number): number {
|
||||
const delay = Math.min(1000 * (2 ** retryCount), 30000);
|
||||
this.logger.error(`操作失败,等待 ${delay}ms 后重试...`, error);
|
||||
this.logger.error(`Operation failed, retrying in ${delay}ms...`, error);
|
||||
return delay;
|
||||
}
|
||||
|
||||
|
|
@ -408,31 +440,24 @@ class OllamaService implements AIService {
|
|||
}
|
||||
}
|
||||
|
||||
async generateSummary(content: string, language = 'zh'): Promise<string> {
|
||||
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
|
||||
async generateSummary(content: string, language: SummaryLanguage = 'en-US'): Promise<string> {
|
||||
const prompt = createSummaryPrompt(content, language);
|
||||
return retryWithBackoff(async () => {
|
||||
const response = await this.generateCompletion(prompt);
|
||||
return response.trim();
|
||||
});
|
||||
}
|
||||
|
||||
async generateTags(content: string): Promise<string[]> {
|
||||
const prompt = `请为以下内容生成3-5个相关标签(不要带#号):\n\n${content}`;
|
||||
async generateTags(content: string, language: SummaryLanguage = 'en-US'): Promise<string[]> {
|
||||
const prompt = createTagsPrompt(content, language);
|
||||
return retryWithBackoff(async () => {
|
||||
const response = await this.generateCompletion(prompt);
|
||||
return response.split(/[,,\s]+/).filter(Boolean);
|
||||
});
|
||||
}
|
||||
|
||||
async generateWeeklyDigest(contents: string[]): Promise<string> {
|
||||
const combinedContent = contents.join('\n---\n');
|
||||
const prompt = `请对以下一周的内容进行总结和分析,生成一份周报。要求:
|
||||
1. 主要工作内容和成果
|
||||
2. 重要事项和进展
|
||||
3. 问题和解决方案
|
||||
4. 下周计划和展望
|
||||
|
||||
内容:\n${combinedContent}`;
|
||||
async generateWeeklyDigest(contents: string[], language: SummaryLanguage = 'en-US'): Promise<string> {
|
||||
const prompt = createWeeklyDigestPrompt(contents, language);
|
||||
|
||||
return retryWithBackoff(async () => {
|
||||
const response = await this.generateCompletion(prompt);
|
||||
|
|
@ -472,19 +497,19 @@ export function createDummyAIService(): AIService {
|
|||
const logger = new Logger('DummyAIService');
|
||||
return {
|
||||
generateSummary(): Promise<string> {
|
||||
logger.debug('使用空 AI 服务');
|
||||
logger.debug('Using dummy AI service');
|
||||
return Promise.resolve('');
|
||||
},
|
||||
generateTags(): Promise<string[]> {
|
||||
logger.debug('使用空 AI 服务');
|
||||
logger.debug('Using dummy AI service');
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
generateWeeklyDigest(): Promise<string> {
|
||||
logger.debug('使用空 AI 服务');
|
||||
logger.debug('Using dummy AI service');
|
||||
return Promise.resolve('');
|
||||
},
|
||||
initialize(): Promise<void> {
|
||||
logger.debug('初始化空 AI 服务');
|
||||
logger.debug('Initializing dummy AI service');
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { AIService } from './ai-service';
|
||||
import type { MemoItem } from '../models/settings';
|
||||
import type { MemoItem, SummaryLanguage, UiLanguage } from '../models/settings';
|
||||
import type { Vault } from 'obsidian';
|
||||
import { formatLocaleDateTime, formatMonthDay, t } from '../i18n';
|
||||
|
||||
export class ContentService {
|
||||
constructor(
|
||||
|
|
@ -8,7 +9,8 @@ export class ContentService {
|
|||
private aiEnabled: boolean,
|
||||
private enableSummary: boolean,
|
||||
private enableTags: boolean,
|
||||
private summaryLanguage: string,
|
||||
private summaryLanguage: SummaryLanguage,
|
||||
private uiLanguage: UiLanguage,
|
||||
private vault: Vault,
|
||||
private syncDirectory: string
|
||||
) {}
|
||||
|
|
@ -33,14 +35,14 @@ export class ContentService {
|
|||
if (this.enableSummary) {
|
||||
const summary = await this.aiService.generateSummary(content, this.summaryLanguage);
|
||||
if (summary?.trim()) {
|
||||
processedContent += `> [!abstract]+ 内容摘要\n> ${summary.replace(/\n/g, '\n> ')}\n\n`;
|
||||
processedContent += `> [!abstract]+ ${t(this.uiLanguage, 'content.summaryHeading')}\n> ${summary.replace(/\n/g, '\n> ')}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.enableTags) {
|
||||
const tags = await this.aiService.generateTags(content);
|
||||
const tags = await this.aiService.generateTags(content, this.summaryLanguage);
|
||||
if (tags?.length > 0) {
|
||||
processedContent += `> [!info]- 相关标签\n> ${tags.map(tag => `#${tag}`).join(' ')}\n\n`;
|
||||
processedContent += `> [!info]- ${t(this.uiLanguage, 'content.tagsHeading')}\n> ${tags.map(tag => `#${tag}`).join(' ')}\n\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +55,6 @@ export class ContentService {
|
|||
const lines = content.split('\n');
|
||||
const firstLine = lines[0].trim();
|
||||
|
||||
// 如果第一行是标题格式(# 开头),提取标题文本
|
||||
if (firstLine.startsWith('# ')) {
|
||||
return firstLine.slice(2).trim();
|
||||
}
|
||||
|
|
@ -68,7 +69,7 @@ export class ContentService {
|
|||
|
||||
private getWeeklyDigestPath(year: string, week: string): string {
|
||||
const weeklyDigestDir = `${this.syncDirectory}/${year}/weekly`;
|
||||
const fileName = `第${week}周总结.md`;
|
||||
const fileName = t(this.uiLanguage, 'content.weeklyFileName', { week });
|
||||
return `${weeklyDigestDir}/${fileName}`;
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +102,7 @@ export class ContentService {
|
|||
await this.ensureDirectoryExists(weeklyDigestDir);
|
||||
|
||||
const contents = weekMemos.map(memo => memo.content);
|
||||
const digest = await this.aiService.generateWeeklyDigest(contents);
|
||||
const digest = await this.aiService.generateWeeklyDigest(contents, this.summaryLanguage);
|
||||
|
||||
if (digest?.trim()) {
|
||||
const weeklyContent = this.formatWeeklyDigest(digest, year, week, weekMemos.length);
|
||||
|
|
@ -110,7 +111,7 @@ export class ContentService {
|
|||
try {
|
||||
await this.vault.create(weeklyDigestPath, weeklyContent);
|
||||
} catch (error) {
|
||||
console.error(`生成第 ${week} 周总结失败:`, error);
|
||||
console.error(`Failed to generate weekly digest for week ${week}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,24 +119,24 @@ export class ContentService {
|
|||
|
||||
private formatWeeklyDigest(digest: string, year: string, week: string, memoCount: number): string {
|
||||
const weekRange = this.getWeekDateRange(Number.parseInt(year, 10), Number.parseInt(week, 10));
|
||||
return `# 📅 第 ${week} 周回顾 (${weekRange})
|
||||
return `# ${t(this.uiLanguage, 'content.weeklyTitle', { week, range: weekRange })}
|
||||
|
||||
## 🌟 本周亮点
|
||||
## ${t(this.uiLanguage, 'content.highlights')}
|
||||
|
||||
${digest}
|
||||
|
||||
## 📊 统计数据
|
||||
## ${t(this.uiLanguage, 'content.stats')}
|
||||
|
||||
- 📝 记录数量:${memoCount} 条
|
||||
- 📅 时间范围:${weekRange}
|
||||
- ${t(this.uiLanguage, 'content.memoCount', { count: memoCount })}
|
||||
- ${t(this.uiLanguage, 'content.timeRange', { range: weekRange })}
|
||||
|
||||
## 💪 下周展望
|
||||
## ${t(this.uiLanguage, 'content.nextWeekOutlook')}
|
||||
|
||||
> [!quote] 激励语录
|
||||
> 每一个当下都是未来的起点,让我们继续前行,创造更多精彩!
|
||||
> [!quote] ${t(this.uiLanguage, 'content.quoteTitle')}
|
||||
> ${t(this.uiLanguage, 'content.quoteText')}
|
||||
|
||||
---
|
||||
*生成时间:${new Date().toLocaleString('zh-CN', { hour12: false })}*
|
||||
*${t(this.uiLanguage, 'content.generatedAt', { time: formatLocaleDateTime(this.uiLanguage, new Date()) })}*
|
||||
|
||||
`;
|
||||
}
|
||||
|
|
@ -152,7 +153,7 @@ ${digest}
|
|||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
|
||||
const formatDate = (date: Date): string => {
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
return formatMonthDay(this.uiLanguage, date);
|
||||
};
|
||||
|
||||
return `${formatDate(weekStart)} - ${formatDate(weekEnd)}`;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { TFile } from 'obsidian';
|
||||
import type { Vault } from 'obsidian';
|
||||
import type { MemoItem } from '../models/settings';
|
||||
import type { MemoItem, UiLanguage } from '../models/settings';
|
||||
import type { MemosService } from './memos-service';
|
||||
import { Logger } from './logger';
|
||||
import { t } from '../i18n';
|
||||
|
||||
export class FileService {
|
||||
private logger: Logger;
|
||||
|
|
@ -10,7 +11,8 @@ export class FileService {
|
|||
constructor(
|
||||
private vault: Vault,
|
||||
private syncDirectory: string,
|
||||
private memosService: MemosService
|
||||
private memosService: MemosService,
|
||||
private uiLanguage: UiLanguage
|
||||
) {
|
||||
this.logger = new Logger('FileService');
|
||||
}
|
||||
|
|
@ -166,7 +168,7 @@ export class FileService {
|
|||
}
|
||||
|
||||
if (otherFiles.length > 0) {
|
||||
documentContent += '\n\n### Attachments\n';
|
||||
documentContent += `\n\n### ${t(this.uiLanguage, 'file.attachments')}\n`;
|
||||
for (const file of otherFiles) {
|
||||
const resourceData = await this.memosService.downloadResource(file);
|
||||
if (resourceData) {
|
||||
|
|
@ -188,16 +190,16 @@ export class FileService {
|
|||
const cleanTags = tags.map(tag => tag.replace(/^#|#$/g, '').trim());
|
||||
|
||||
documentContent += '\n\n---\n';
|
||||
documentContent += '> [!note]- Memo Properties\n';
|
||||
documentContent += `> - Created: ${this.formatDateTime(memo.createdTs)}\n`;
|
||||
documentContent += `> - Updated: ${this.formatDateTime(memo.updatedTs)}\n`;
|
||||
documentContent += '> - Type: memo\n';
|
||||
documentContent += `> [!note]- ${t(this.uiLanguage, 'file.memoProperties')}\n`;
|
||||
documentContent += `> - ${t(this.uiLanguage, 'file.created')}: ${this.formatDateTime(memo.createdTs)}\n`;
|
||||
documentContent += `> - ${t(this.uiLanguage, 'file.updated')}: ${this.formatDateTime(memo.updatedTs)}\n`;
|
||||
documentContent += `> - ${t(this.uiLanguage, 'file.type')}: memo\n`;
|
||||
if (cleanTags.length > 0) {
|
||||
documentContent += `> - Tags: [${cleanTags.join(', ')}]\n`;
|
||||
documentContent += `> - ${t(this.uiLanguage, 'file.tags')}: [${cleanTags.join(', ')}]\n`;
|
||||
}
|
||||
documentContent += `> - memo_id: ${memo.id}\n`;
|
||||
documentContent += `> - Visibility: ${memo.visibility.toLowerCase()}\n`;
|
||||
if (memo.pinned) documentContent += '> - Pinned: true\n';
|
||||
documentContent += `> - ${t(this.uiLanguage, 'file.visibility')}: ${memo.visibility.toLowerCase()}\n`;
|
||||
if (memo.pinned) documentContent += `> - ${t(this.uiLanguage, 'file.pinned')}: true\n`;
|
||||
|
||||
const exists = await this.vault.adapter.exists(filePath);
|
||||
if (exists) {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import { Notice, setIcon } from 'obsidian';
|
||||
import type { UiLanguage } from '../models/settings';
|
||||
import { t } from '../i18n';
|
||||
|
||||
type SyncStatus = 'idle' | 'syncing' | 'error' | 'success' | 'warning';
|
||||
|
||||
export class StatusService {
|
||||
private statusBarItem: HTMLElement;
|
||||
private uiLanguage: UiLanguage;
|
||||
private currentStatus: SyncStatus = 'idle';
|
||||
private syncStartTime = 0;
|
||||
private progressCount = 0;
|
||||
private totalCount = 0;
|
||||
|
||||
constructor(statusBarItem: HTMLElement) {
|
||||
constructor(statusBarItem: HTMLElement, uiLanguage: UiLanguage) {
|
||||
this.statusBarItem = statusBarItem;
|
||||
this.uiLanguage = uiLanguage;
|
||||
this.updateStatusBar();
|
||||
}
|
||||
|
||||
|
|
@ -23,27 +27,27 @@ export class StatusService {
|
|||
icon = 'sync';
|
||||
const progress = this.totalCount ? ` ${this.progressCount}/${this.totalCount}` : '';
|
||||
const elapsed = this.syncStartTime ? ` (${Math.round((Date.now() - this.syncStartTime) / 1000)}s)` : '';
|
||||
text = `Syncing${progress}${elapsed}`;
|
||||
text = `${t(this.uiLanguage, 'status.syncing')}${progress}${elapsed}`;
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
icon = 'alert-circle';
|
||||
text = 'Sync failed';
|
||||
text = t(this.uiLanguage, 'status.syncFailed');
|
||||
break;
|
||||
}
|
||||
case 'success': {
|
||||
icon = 'check-circle';
|
||||
text = 'Sync complete';
|
||||
text = t(this.uiLanguage, 'status.syncComplete');
|
||||
break;
|
||||
}
|
||||
case 'warning': {
|
||||
icon = 'alert-triangle';
|
||||
text = 'Warning';
|
||||
text = t(this.uiLanguage, 'status.warning');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
icon = 'clock';
|
||||
text = 'Idle';
|
||||
text = t(this.uiLanguage, 'status.idle');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +71,7 @@ export class StatusService {
|
|||
this.progressCount = 0;
|
||||
this.totalCount = totalItems;
|
||||
this.updateStatusBar();
|
||||
new Notice('Syncing memos');
|
||||
new Notice(t(this.uiLanguage, 'notice.syncingMemos'));
|
||||
}
|
||||
|
||||
updateProgress(current: number, message?: string) {
|
||||
|
|
@ -81,7 +85,7 @@ export class StatusService {
|
|||
setError(error: string) {
|
||||
this.currentStatus = 'error';
|
||||
this.updateStatusBar();
|
||||
new Notice(`同步失败: ${error}`, 5000);
|
||||
new Notice(t(this.uiLanguage, 'notice.syncFailed', { error }), 5000);
|
||||
console.error('Sync failed:', error);
|
||||
}
|
||||
|
||||
|
|
@ -114,4 +118,9 @@ export class StatusService {
|
|||
this.updateStatusBar();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
setLanguage(uiLanguage: UiLanguage) {
|
||||
this.uiLanguage = uiLanguage;
|
||||
this.updateStatusBar();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import type { AIModelType } from '../models/settings';
|
||||
import type { AIModelType, SummaryLanguage, UiLanguage } from '../models/settings';
|
||||
import type MemosSyncPlugin from '../../main';
|
||||
import { GEMINI_MODELS, OPENAI_MODELS, OLLAMA_MODELS, MODEL_DESCRIPTIONS } from '../services/ai-service';
|
||||
import { t } from '../i18n';
|
||||
|
||||
export class MemosSyncSettingTab extends PluginSettingTab {
|
||||
plugin: MemosSyncPlugin;
|
||||
|
|
@ -14,10 +15,27 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
const uiLanguage = this.plugin.settings.uiLanguage;
|
||||
const tr = (key: Parameters<typeof t>[1], vars?: Parameters<typeof t>[2]) => t(uiLanguage, key, vars);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Memos URL')
|
||||
.setDesc('Memos server base URL')
|
||||
.setName(tr('settings.uiLanguage.name'))
|
||||
.setDesc(tr('settings.uiLanguage.desc'))
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('en-US', tr('language.en-US'))
|
||||
.addOption('zh-CN', tr('language.zh-CN'))
|
||||
.addOption('tr-TR', tr('language.tr-TR'))
|
||||
.addOption('ja-JP', tr('language.ja-JP'))
|
||||
.setValue(this.plugin.settings.uiLanguage)
|
||||
.onChange(async (value: UiLanguage) => {
|
||||
this.plugin.settings.uiLanguage = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(tr('settings.memosUrl.name'))
|
||||
.setDesc(tr('settings.memosUrl.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('https://demo.usememos.com/')
|
||||
.setValue(this.plugin.settings.memosApiUrl)
|
||||
|
|
@ -27,10 +45,10 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Access token')
|
||||
.setDesc('Your memos API access token')
|
||||
.setName(tr('settings.accessToken.name'))
|
||||
.setDesc(tr('settings.accessToken.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter access token')
|
||||
.setPlaceholder(tr('settings.placeholder.enterAccessToken'))
|
||||
.setValue(this.plugin.settings.memosAccessToken)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.memosAccessToken = value;
|
||||
|
|
@ -38,8 +56,8 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Sync directory')
|
||||
.setDesc('Folder in Obsidian where memos content will be stored')
|
||||
.setName(tr('settings.syncDirectory.name'))
|
||||
.setDesc(tr('settings.syncDirectory.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Memos')
|
||||
.setValue(this.plugin.settings.syncDirectory)
|
||||
|
|
@ -49,23 +67,22 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Sync mode')
|
||||
.setDesc('Choose manual or automatic sync')
|
||||
.setName(tr('settings.syncMode.name'))
|
||||
.setDesc(tr('settings.syncMode.desc'))
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('manual', 'Manual')
|
||||
.addOption('auto', 'Automatic')
|
||||
.addOption('manual', tr('settings.syncMode.manual'))
|
||||
.addOption('auto', tr('settings.syncMode.auto'))
|
||||
.setValue(this.plugin.settings.syncFrequency)
|
||||
.onChange(async (value: 'manual' | 'auto') => {
|
||||
this.plugin.settings.syncFrequency = value;
|
||||
await this.plugin.saveSettings();
|
||||
// 重新渲染以显示/隐藏自动同步间隔设置
|
||||
this.display();
|
||||
}));
|
||||
|
||||
if (this.plugin.settings.syncFrequency === 'auto') {
|
||||
new Setting(containerEl)
|
||||
.setName('Sync interval')
|
||||
.setDesc('Interval between automatic syncs (minutes)')
|
||||
.setName(tr('settings.syncInterval.name'))
|
||||
.setDesc(tr('settings.syncInterval.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('30')
|
||||
.setValue(String(this.plugin.settings.autoSyncInterval))
|
||||
|
|
@ -79,8 +96,8 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Sync limit')
|
||||
.setDesc('Maximum number of memos to fetch per sync.')
|
||||
.setName(tr('settings.syncLimit.name'))
|
||||
.setDesc(tr('settings.syncLimit.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('1000')
|
||||
.setValue(String(this.plugin.settings.syncLimit))
|
||||
|
|
@ -93,8 +110,8 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Sync after date')
|
||||
.setDesc('Only sync memos created on or after this date (yyyy-mm-dd). Leave empty to sync all.')
|
||||
.setName(tr('settings.syncAfter.name'))
|
||||
.setDesc(tr('settings.syncAfter.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('2024-01-01')
|
||||
.setValue(this.plugin.settings.syncAfter)
|
||||
|
|
@ -103,24 +120,21 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// AI features
|
||||
new Setting(containerEl)
|
||||
.setName('Enable AI features')
|
||||
.setDesc('Toggle AI-enhanced processing')
|
||||
.setName(tr('settings.aiEnabled.name'))
|
||||
.setDesc(tr('settings.aiEnabled.desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.ai.enabled)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.enabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
// 重新渲染以显示/隐藏相关设置
|
||||
this.display();
|
||||
}));
|
||||
|
||||
if (this.plugin.settings.ai.enabled) {
|
||||
// AI 模型选择
|
||||
new Setting(containerEl)
|
||||
.setName('AI model')
|
||||
.setDesc('Select the AI model provider')
|
||||
.setName(tr('settings.aiModel.name'))
|
||||
.setDesc(tr('settings.aiModel.desc'))
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('openai', 'OpenAI')
|
||||
.addOption('gemini', 'Google Gemini')
|
||||
|
|
@ -130,17 +144,15 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value: AIModelType) => {
|
||||
this.plugin.settings.ai.modelType = value;
|
||||
await this.plugin.saveSettings();
|
||||
// 重新渲染以显示/隐藏相关设置
|
||||
this.display();
|
||||
}));
|
||||
|
||||
// 只有在选择非 Ollama 模型时显示 API 密钥设置
|
||||
if (this.plugin.settings.ai.modelType !== 'ollama') {
|
||||
new Setting(containerEl)
|
||||
.setName('API key')
|
||||
.setDesc('Your AI service API key')
|
||||
.setName(tr('settings.apiKey.name'))
|
||||
.setDesc(tr('settings.apiKey.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter API key')
|
||||
.setPlaceholder(tr('settings.placeholder.enterApiKey'))
|
||||
.setValue(this.plugin.settings.ai.apiKey)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.apiKey = value;
|
||||
|
|
@ -148,13 +160,11 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
}
|
||||
|
||||
// 显示特定模型的选项
|
||||
this.displayModelOptions(containerEl);
|
||||
this.displayModelOptions(containerEl, tr);
|
||||
|
||||
// AI 功能选项
|
||||
new Setting(containerEl)
|
||||
.setName('Weekly digest')
|
||||
.setDesc('Automatically generate a weekly summary')
|
||||
.setName(tr('settings.weeklyDigest.name'))
|
||||
.setDesc(tr('settings.weeklyDigest.desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.ai.weeklyDigest)
|
||||
.onChange(async (value) => {
|
||||
|
|
@ -163,8 +173,8 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto tags')
|
||||
.setDesc('Automatically generate tags based on content')
|
||||
.setName(tr('settings.autoTags.name'))
|
||||
.setDesc(tr('settings.autoTags.desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.ai.autoTags)
|
||||
.onChange(async (value) => {
|
||||
|
|
@ -173,8 +183,8 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Intelligent summary')
|
||||
.setDesc('Automatically generate content summaries')
|
||||
.setName(tr('settings.intelligentSummary.name'))
|
||||
.setDesc(tr('settings.intelligentSummary.desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.ai.intelligentSummary)
|
||||
.onChange(async (value) => {
|
||||
|
|
@ -183,53 +193,52 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Summary language')
|
||||
.setDesc('Select the language for generated summaries')
|
||||
.setName(tr('settings.summaryLanguage.name'))
|
||||
.setDesc(tr('settings.summaryLanguage.desc'))
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('zh', 'Chinese')
|
||||
.addOption('en', 'English')
|
||||
.addOption('ja', 'Japanese')
|
||||
.addOption('ko', 'Korean')
|
||||
.addOption('en-US', tr('language.en-US'))
|
||||
.addOption('zh-CN', tr('language.zh-CN'))
|
||||
.addOption('tr-TR', tr('language.tr-TR'))
|
||||
.addOption('ja-JP', tr('language.ja-JP'))
|
||||
.setValue(this.plugin.settings.ai.summaryLanguage)
|
||||
.onChange(async (value: 'zh' | 'en' | 'ja' | 'ko') => {
|
||||
.onChange(async (value: SummaryLanguage) => {
|
||||
this.plugin.settings.ai.summaryLanguage = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private displayModelOptions(containerEl: HTMLElement) {
|
||||
private displayModelOptions(
|
||||
containerEl: HTMLElement,
|
||||
tr: (key: Parameters<typeof t>[1], vars?: Parameters<typeof t>[2]) => string
|
||||
) {
|
||||
const modelType = this.plugin.settings.ai.modelType;
|
||||
|
||||
if (modelType === 'gemini') {
|
||||
new Setting(containerEl)
|
||||
.setName('Gemini model')
|
||||
.setDesc('Select the Gemini model to use')
|
||||
.setName(tr('settings.geminiModel.name'))
|
||||
.setDesc(tr('settings.geminiModel.desc'))
|
||||
.addDropdown(dropdown => {
|
||||
// 添加所有模型选项
|
||||
for (const [displayName, modelId] of Object.entries(GEMINI_MODELS)) {
|
||||
dropdown.addOption(modelId, `${displayName} - ${MODEL_DESCRIPTIONS[modelId]}`);
|
||||
}
|
||||
|
||||
// 设置当前值或默认值
|
||||
const currentModel = this.plugin.settings.ai.modelName || GEMINI_MODELS['Gemini 1.5 Flash'];
|
||||
dropdown.setValue(currentModel);
|
||||
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.ai.modelName = value;
|
||||
await this.plugin.saveSettings();
|
||||
// 重新渲染以显示/隐藏自定义模型输入框
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
// If custom model is selected, show input
|
||||
if (this.plugin.settings.ai.modelName === 'custom') {
|
||||
new Setting(containerEl)
|
||||
.setName('Custom model name')
|
||||
.setDesc('Enter the name of the custom model')
|
||||
.setName(tr('settings.customModel.name'))
|
||||
.setDesc(tr('settings.customModel.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('E.g. Gemini-pro-latest')
|
||||
.setPlaceholder('E.g. gemini-pro-latest')
|
||||
.setValue(this.plugin.settings.ai.customModelName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.customModelName = value;
|
||||
|
|
@ -238,59 +247,54 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}
|
||||
} else if (modelType === 'openai') {
|
||||
new Setting(containerEl)
|
||||
.setName('OpenAI model')
|
||||
.setDesc('Select the OpenAI model to use')
|
||||
.setName(tr('settings.openaiModel.name'))
|
||||
.setDesc(tr('settings.openaiModel.desc'))
|
||||
.addDropdown(dropdown => {
|
||||
// 添加所有模型选项
|
||||
for (const [displayName, modelId] of Object.entries(OPENAI_MODELS)) {
|
||||
dropdown.addOption(modelId, `${displayName} - ${MODEL_DESCRIPTIONS[modelId]}`);
|
||||
}
|
||||
|
||||
// 设置当前值或默认值
|
||||
const currentModel = this.plugin.settings.ai.modelName || OPENAI_MODELS['GPT-4o'];
|
||||
dropdown.setValue(currentModel);
|
||||
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.ai.modelName = value;
|
||||
await this.plugin.saveSettings();
|
||||
// 重新渲染以显示/隐藏自定义模型输入框
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
// If custom model is selected, show input
|
||||
if (this.plugin.settings.ai.modelName === 'custom') {
|
||||
new Setting(containerEl)
|
||||
.setName('Custom model name')
|
||||
.setDesc('Enter the name of the custom model')
|
||||
.setName(tr('settings.customModel.name'))
|
||||
.setDesc(tr('settings.customModel.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('E.g. GPT-4-1106-preview')
|
||||
.setPlaceholder('E.g. gpt-4.1-mini')
|
||||
.setValue(this.plugin.settings.ai.customModelName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.customModelName = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('OpenAI API base URL')
|
||||
.setDesc('Base URL for custom API services')
|
||||
.addText(text => text
|
||||
.setPlaceholder('HTTPS://api.OpenAI.com/v1')
|
||||
.setValue(this.plugin.settings.ai.openaiBaseUrl || 'https://api.openai.com/v1')
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.openaiBaseUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName(tr('settings.openaiBaseUrl.name'))
|
||||
.setDesc(tr('settings.openaiBaseUrl.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('https://api.openai.com/v1')
|
||||
.setValue(this.plugin.settings.ai.openaiBaseUrl || 'https://api.openai.com/v1')
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.openaiBaseUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
} else if (modelType === 'claude') {
|
||||
new Setting(containerEl)
|
||||
.setName('Claude model')
|
||||
.setDesc('Select the Claude model to use')
|
||||
.setName(tr('settings.claudeModel.name'))
|
||||
.setDesc(tr('settings.claudeModel.desc'))
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('claude-3-opus-20240229', 'Claude 3 opus')
|
||||
.addOption('claude-3-sonnet-20240229', 'Claude 3 sonnet')
|
||||
.addOption('claude-3-haiku-20240307', 'Claude 3 haiku')
|
||||
dropdown.addOption('claude-3-opus-20240229', 'Claude 3 Opus')
|
||||
.addOption('claude-3-sonnet-20240229', 'Claude 3 Sonnet')
|
||||
.addOption('claude-3-haiku-20240307', 'Claude 3 Haiku')
|
||||
.addOption('custom', 'Custom model');
|
||||
|
||||
const currentModel = this.plugin.settings.ai.modelName || 'claude-3-opus-20240229';
|
||||
|
|
@ -305,10 +309,10 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
|
||||
if (this.plugin.settings.ai.modelName === 'custom') {
|
||||
new Setting(containerEl)
|
||||
.setName('Custom model name')
|
||||
.setDesc('Enter the name of the custom model')
|
||||
.setName(tr('settings.customModel.name'))
|
||||
.setDesc(tr('settings.customModel.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('E.g. Claude-3-opus-next')
|
||||
.setPlaceholder('E.g. claude-3-7-sonnet-latest')
|
||||
.setValue(this.plugin.settings.ai.customModelName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.customModelName = value;
|
||||
|
|
@ -316,31 +320,27 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
}
|
||||
} else if (modelType === 'ollama') {
|
||||
// Ollama base URL
|
||||
new Setting(containerEl)
|
||||
.setName('Ollama base URL')
|
||||
.setDesc('Base URL for the ollama service (default: http://localhost:11434)')
|
||||
.setName(tr('settings.ollamaBaseUrl.name'))
|
||||
.setDesc(tr('settings.ollamaBaseUrl.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('HTTP://localhost:11434')
|
||||
.setPlaceholder('http://localhost:11434')
|
||||
.setValue(this.plugin.settings.ai.ollamaBaseUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.ollamaBaseUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Ollama model selection
|
||||
new Setting(containerEl)
|
||||
.setName('Ollama model')
|
||||
.setDesc('Select the ollama model to use')
|
||||
.setName(tr('settings.ollamaModel.name'))
|
||||
.setDesc(tr('settings.ollamaModel.desc'))
|
||||
.addDropdown(dropdown => {
|
||||
// 添加所有模型选项
|
||||
for (const [displayName, modelId] of Object.entries(OLLAMA_MODELS)) {
|
||||
if (typeof modelId === 'string') {
|
||||
dropdown.addOption(modelId, `${displayName} - ${MODEL_DESCRIPTIONS[modelId] || modelId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置当前值或默认值
|
||||
const defaultModel = OLLAMA_MODELS['Llama 2'] as string;
|
||||
const currentModel = this.plugin.settings.ai.modelName || defaultModel;
|
||||
dropdown.setValue(currentModel);
|
||||
|
|
@ -352,13 +352,12 @@ export class MemosSyncSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
// If custom model is selected, show input
|
||||
if (this.plugin.settings.ai.modelName === 'custom') {
|
||||
new Setting(containerEl)
|
||||
.setName('Custom model name')
|
||||
.setDesc('Enter the name of the custom model')
|
||||
.setName(tr('settings.customModel.name'))
|
||||
.setDesc(tr('settings.customModel.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder('E.g. Llama2:13b')
|
||||
.setPlaceholder('E.g. llama3.1:8b')
|
||||
.setValue(this.plugin.settings.ai.customModelName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ai.customModelName = value;
|
||||
|
|
|
|||
Loading…
Reference in a new issue