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:
Cactus 2026-06-02 13:17:10 +08:00
parent 8848cd1343
commit b8f8ea59c5
No known key found for this signature in database
GPG key ID: E4B0ABC94A6222EC
9 changed files with 798 additions and 256 deletions

View file

@ -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
View file

@ -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
View 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': '仅同步该日期及之后创建的 Memoyyyy-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);
}

View file

@ -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'
}
};

View file

@ -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();
}
};

View file

@ -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)}`;

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -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;