chore: update plugin configuration and add license

This commit is contained in:
root 2024-12-13 13:43:51 +08:00
parent 9031a8676f
commit 9db56b629e
21 changed files with 4734 additions and 4734 deletions

View file

@ -1,39 +1,39 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
env:
PLUGIN_NAME: memos-ai-sync
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "18.x"
cache: 'npm'
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
# Create release
gh release create "$tag" \
--title="$tag" \
--draft \
name: Release Obsidian plugin
on:
push:
tags:
- "*"
env:
PLUGIN_NAME: memos-ai-sync
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "18.x"
cache: 'npm'
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
# Create release
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css LICENSE

144
.gitignore vendored
View file

@ -1,72 +1,72 @@
# Build output
dist/
build/
main.js
*.js.map
# Development dependencies
node_modules/
# Environment and configuration
.env
.env.*
data.json
*.data.json
# IDE and editor files
.idea/
.vscode/
*.iml
*.swp
*.swo
.vs/
*.sublime-*
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.log
*.tmp
*.temp
.cache/
# Test files
coverage/
.nyc_output/
*.test.js
*.spec.js
# Debug files
*.log
debug.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Yarn integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.test
.env.production
# Local development files
*.local
local.*
# Build output
dist/
build/
main.js
*.js.map
# Development dependencies
node_modules/
# Environment and configuration
.env
.env.*
data.json
*.data.json
# IDE and editor files
.idea/
.vscode/
*.iml
*.swp
*.swo
.vs/
*.sublime-*
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.log
*.tmp
*.temp
.cache/
# Test files
coverage/
.nyc_output/
*.test.js
*.spec.js
# Debug files
*.log
debug.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Yarn integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.test
.env.production
# Local development files
*.local
local.*

View file

@ -1,66 +1,66 @@
# Changelog
## [1.1.4] - 2024-03-12
### Changed
- 优化周总结功能
- 按周维度生成独立的总结文件
- 总结文件保存在年份目录下的 weekly 子目录中
- 智能跳过已存在的周总结
- 改进文件组织结构,更加清晰
- 优化生成逻辑,提高效率
## [1.1.3] - 2024-03-12
### Fixed
- 优化文件名处理逻辑
- 移除文件名开头的特殊字符
- 改进特殊字符处理方式,避免不必要的下划线
- 保持文件名的简洁性和可读性
- 确保文件名合法性
## [1.1.2] - 2024-03-12
### Added
- 新增智能同步控制
- 添加基于 Memo ID 的重复检测机制
- 自动跳过已同步的内容
- 保护已同步文件不被覆盖
- 优化同步性能和效率
## [1.1.1] - 2024-03-12
### Changed
- 优化文件名生成逻辑
- 移除文件名中的 Obsidian callout 标记
- 移除文件名中的 Markdown 格式标记(如斜体、粗体等)
- 优化链接和图片标记的处理
- 保持文件名的简洁和可读性
## [1.1.0] - 2024-03-12
### Changed
- 重构项目代码结构,优化代码组织
- 将代码按功能模块拆分到 src 目录下
- 创建独立的服务类处理不同功能
- 改进类型定义和接口设计
### Added
- 新增模块化的代码结构:
- `src/models`: 类型定义和接口
- `src/services`: 核心服务实现
- `src/ui`: 用户界面组件
### Internal
- 将 Memos API 相关功能封装到 `MemosService`
- 将文件处理逻辑封装到 `FileService`
- 将设置界面代码移至独立文件
- 优化主插件文件结构
- 改进错误处理和日志记录
## [1.0.0] - 2024-03-04
- 初始版本发布
- 支持同步 Memos 内容到 Obsidian
- 支持手动和自动同步
- 支持资源文件同步
# Changelog
## [1.1.4] - 2024-03-12
### Changed
- 优化周总结功能
- 按周维度生成独立的总结文件
- 总结文件保存在年份目录下的 weekly 子目录中
- 智能跳过已存在的周总结
- 改进文件组织结构,更加清晰
- 优化生成逻辑,提高效率
## [1.1.3] - 2024-03-12
### Fixed
- 优化文件名处理逻辑
- 移除文件名开头的特殊字符
- 改进特殊字符处理方式,避免不必要的下划线
- 保持文件名的简洁性和可读性
- 确保文件名合法性
## [1.1.2] - 2024-03-12
### Added
- 新增智能同步控制
- 添加基于 Memo ID 的重复检测机制
- 自动跳过已同步的内容
- 保护已同步文件不被覆盖
- 优化同步性能和效率
## [1.1.1] - 2024-03-12
### Changed
- 优化文件名生成逻辑
- 移除文件名中的 Obsidian callout 标记
- 移除文件名中的 Markdown 格式标记(如斜体、粗体等)
- 优化链接和图片标记的处理
- 保持文件名的简洁和可读性
## [1.1.0] - 2024-03-12
### Changed
- 重构项目代码结构,优化代码组织
- 将代码按功能模块拆分到 src 目录下
- 创建独立的服务类处理不同功能
- 改进类型定义和接口设计
### Added
- 新增模块化的代码结构:
- `src/models`: 类型定义和接口
- `src/services`: 核心服务实现
- `src/ui`: 用户界面组件
### Internal
- 将 Memos API 相关功能封装到 `MemosService`
- 将文件处理逻辑封装到 `FileService`
- 将设置界面代码移至独立文件
- 优化主插件文件结构
- 改进错误处理和日志记录
## [1.0.0] - 2024-03-04
- 初始版本发布
- 支持同步 Memos 内容到 Obsidian
- 支持手动和自动同步
- 支持资源文件同步
- 支持按年/月组织文件结构

40
LICENSE
View file

@ -1,21 +1,21 @@
MIT License
Copyright (c) 2024 leoleelxh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
MIT License
Copyright (c) 2024 leoleelxh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

434
README.md
View file

@ -1,218 +1,218 @@
# Obsidian Memos Sync Plugin
[English](README_EN.md) | 简体中文
将 Memos 内容同步到 Obsidian 的插件,提供无缝集成体验。
# 界面预览
## 设置界面
![设置界面](/examples/example01.png)
## AI 增强
可以进行memos的总结和自动打标签便于管理
### 原memos内容
![原memos内容](/examples/example03.png)
### AI增强后内容
![AI 增强](/examples/example02.png)
## AI 周总结
对同步的内容进行统一梳理形成每周总结便于review。
### AI 周总结内容
![AI 周总结](/examples/example04.png)
## 功能特点
### 核心功能
- 一键同步 Memos 内容到 Obsidian
- 支持手动和自动同步模式
- 智能的文件组织(年/月结构)
- 可自定义同步间隔
- 智能同步控制
- 自动跳过已同步的内容
- 基于 Memo ID 的重复检测
- 保护已同步文件不被覆盖
### 内容处理
- 智能文件命名
- 自动提取内容预览作为文件名
- 智能处理特殊字符,保持文件名简洁
- 自动移除文件名开头的特殊字符
- 保留时间戳便于识别:`(YYYY-MM-DD HH-mm)`
- Markdown 内容优化
- 标签转换(从 Memos 格式 #tag# 到 Obsidian 格式 #tag
- 支持图片和文件附件
### AI 增强
```
目前已实现openai、gemini、ollama调用claude还没测试。
```
#### AI 设置说明
1. **选择 AI 提供商**
- OpenAI
- Google Gemini
- Ollama本地部署
- Claude开发中
2. **配置说明**
- OpenAI 设置
- API Key填入您的 OpenAI API 密钥
- 模型选择:支持 gpt-3.5-turbo、gpt-4 等
- Gemini 设置
- API Key填入您的 Google API 密钥
- 模型gemini-pro
- Ollama 设置
- 服务器地址:例如 http://localhost:11434
- 模型:支持 llama2、mistral 等
3. **功能开关**
- 自动总结:对每条 memo 生成摘要
- 智能标签:自动推荐相关标签
- 周报生成:自动生成每周总结
- 提示词配置:可自定义 AI 提示词(开发中)
- 自动生成内容摘要
- 智能标签推荐
- 每周内容汇总
- 按周维度生成独立的总结文件
- 自动跳过已存在的周总结
- 总结文件保存在 `{year}/weekly/` 目录下
- 包含本周亮点、统计数据和展望
- 可配置的 AI 功能
### 资源管理
- 自动下载图片和附件
- 本地资源存储(组织化目录结构)
- 正确的相对路径生成
- 支持多种文件类型
### 文档结构
- 内容优先的格式设计
- 图片内联显示
- 专门的"附件"区域
- 元数据存储在可折叠的 callout 中
- 周总结文件组织
- 目录结构:`sync_directory/YYYY/weekly/第WW周总结.md`
- 每周一个独立的总结文件
- 包含周数、日期范围和统计信息
### 文件组织
- 文件按年月组织: `sync_directory/YYYY/MM/`
- 资源文件存储在专门的目录中
- 文件名包含内容预览和时间戳
- 示例:`Meeting notes for project (2024-01-10 15-30).md`
- 周总结文件结构:
```
sync_directory/
├── 2024/
│ ├── 01/
│ │ ├── memo1.md
│ │ └── resources/
│ │ └── attachments...
│ ├── 02/
│ │ └── memo2.md
│ └── weekly/
│ ├── 第01周总结.md
│ ├── 第02周总结.md
│ └── 第03周总结.md
└── 2023/
└── ...
```
## 安装
1. 打开 Obsidian 设置
2. 进入社区插件并关闭安全模式
3. 点击浏览并搜索 "Memos Sync"
4. 安装插件
5. 启用插件
## 配置
### 必需设置
- **Memos API URL**: 您的 Memos 服务器 API 端点
- **Access Token**: 您的 Memos API 访问令牌
- **同步目录**: Memos 内容在 Obsidian 中的存储位置
### 可选设置
- **同步模式**: 选择手动或自动同步
- **同步间隔**: 设置自动同步的频率(如果启用)
- **同步限制**: 一次同步的最大条目数
## 使用方法
### 手动同步
1. 点击工具栏中的同步图标
2. 等待同步过程完成
3. 您的 memos 将按组织结构保存
### 自动同步
1. 在设置中启用自动同步
2. 设置您偏好的同步间隔
3. 插件将按配置自动同步
## 项目结构
```
obsidian-memos-sync/
├── src/
│ ├── models/ # 类型定义和接口
│ │ ├── settings.ts # 设置和类型定义
│ │ └── plugin.ts # 插件接口定义
│ ├── services/ # 核心服务实现
│ │ ├── memos-service.ts # Memos API 服务
│ │ └── file-service.ts # 文件处理服务
│ └── ui/ # 用户界面组件
│ └── settings-tab.ts # 设置页面
├── main.ts # 主插件文件
├── manifest.json # 插件清单
└── package.json # 项目配置
```
### 代码结构说明
- **models**: 包含所有类型定义和接口
- `settings.ts`: 定义插件设置和数据模型
- `plugin.ts`: 定义插件接口
- **services**: 核心服务实现
- `memos-service.ts`: 处理与 Memos API 的所有交互
- `file-service.ts`: 处理文件系统操作和内容格式化
- **ui**: 用户界面组件
- `settings-tab.ts`: 实现插件设置界面
## 兼容性
- 支持 Memos 版本:最高至 0.22.5
- 推荐使用 Memos v0.22.5 以获得最佳兼容性
## 故障排除
### 常见问题
1. **同步失败**
- 检查 Memos API URL 和访问令牌
- 确保 Obsidian 对同步目录有写入权限
2. **资源文件不加载**
- 验证 Memos 服务器是否可访问
- 检查网络连接
- 确保认证正确
3. **文件组织问题**
- 检查同步目录权限
- 验证路径配置
## 支持
如果遇到问题或有建议:
1. 访问 [GitHub 仓库](https://github.com/leoleelxh/obsidian-memos-sync-plugin)
2. 创建 issue 并详细描述问题
3. 包含相关错误信息和配置
## 许可证
# Obsidian Memos Sync Plugin
[English](README_EN.md) | 简体中文
将 Memos 内容同步到 Obsidian 的插件,提供无缝集成体验。
# 界面预览
## 设置界面
![设置界面](/examples/example01.png)
## AI 增强
可以进行memos的总结和自动打标签便于管理
### 原memos内容
![原memos内容](/examples/example03.png)
### AI增强后内容
![AI 增强](/examples/example02.png)
## AI 周总结
对同步的内容进行统一梳理形成每周总结便于review。
### AI 周总结内容
![AI 周总结](/examples/example04.png)
## 功能特点
### 核心功能
- 一键同步 Memos 内容到 Obsidian
- 支持手动和自动同步模式
- 智能的文件组织(年/月结构)
- 可自定义同步间隔
- 智能同步控制
- 自动跳过已同步的内容
- 基于 Memo ID 的重复检测
- 保护已同步文件不被覆盖
### 内容处理
- 智能文件命名
- 自动提取内容预览作为文件名
- 智能处理特殊字符,保持文件名简洁
- 自动移除文件名开头的特殊字符
- 保留时间戳便于识别:`(YYYY-MM-DD HH-mm)`
- Markdown 内容优化
- 标签转换(从 Memos 格式 #tag# 到 Obsidian 格式 #tag
- 支持图片和文件附件
### AI 增强
```
目前已实现openai、gemini、ollama调用claude还没测试。
```
#### AI 设置说明
1. **选择 AI 提供商**
- OpenAI
- Google Gemini
- Ollama本地部署
- Claude开发中
2. **配置说明**
- OpenAI 设置
- API Key填入您的 OpenAI API 密钥
- 模型选择:支持 gpt-3.5-turbo、gpt-4 等
- Gemini 设置
- API Key填入您的 Google API 密钥
- 模型gemini-pro
- Ollama 设置
- 服务器地址:例如 http://localhost:11434
- 模型:支持 llama2、mistral 等
3. **功能开关**
- 自动总结:对每条 memo 生成摘要
- 智能标签:自动推荐相关标签
- 周报生成:自动生成每周总结
- 提示词配置:可自定义 AI 提示词(开发中)
- 自动生成内容摘要
- 智能标签推荐
- 每周内容汇总
- 按周维度生成独立的总结文件
- 自动跳过已存在的周总结
- 总结文件保存在 `{year}/weekly/` 目录下
- 包含本周亮点、统计数据和展望
- 可配置的 AI 功能
### 资源管理
- 自动下载图片和附件
- 本地资源存储(组织化目录结构)
- 正确的相对路径生成
- 支持多种文件类型
### 文档结构
- 内容优先的格式设计
- 图片内联显示
- 专门的"附件"区域
- 元数据存储在可折叠的 callout 中
- 周总结文件组织
- 目录结构:`sync_directory/YYYY/weekly/第WW周总结.md`
- 每周一个独立的总结文件
- 包含周数、日期范围和统计信息
### 文件组织
- 文件按年月组织: `sync_directory/YYYY/MM/`
- 资源文件存储在专门的目录中
- 文件名包含内容预览和时间戳
- 示例:`Meeting notes for project (2024-01-10 15-30).md`
- 周总结文件结构:
```
sync_directory/
├── 2024/
│ ├── 01/
│ │ ├── memo1.md
│ │ └── resources/
│ │ └── attachments...
│ ├── 02/
│ │ └── memo2.md
│ └── weekly/
│ ├── 第01周总结.md
│ ├── 第02周总结.md
│ └── 第03周总结.md
└── 2023/
└── ...
```
## 安装
1. 打开 Obsidian 设置
2. 进入社区插件并关闭安全模式
3. 点击浏览并搜索 "Memos Sync"
4. 安装插件
5. 启用插件
## 配置
### 必需设置
- **Memos API URL**: 您的 Memos 服务器 API 端点
- **Access Token**: 您的 Memos API 访问令牌
- **同步目录**: Memos 内容在 Obsidian 中的存储位置
### 可选设置
- **同步模式**: 选择手动或自动同步
- **同步间隔**: 设置自动同步的频率(如果启用)
- **同步限制**: 一次同步的最大条目数
## 使用方法
### 手动同步
1. 点击工具栏中的同步图标
2. 等待同步过程完成
3. 您的 memos 将按组织结构保存
### 自动同步
1. 在设置中启用自动同步
2. 设置您偏好的同步间隔
3. 插件将按配置自动同步
## 项目结构
```
obsidian-memos-sync/
├── src/
│ ├── models/ # 类型定义和接口
│ │ ├── settings.ts # 设置和类型定义
│ │ └── plugin.ts # 插件接口定义
│ ├── services/ # 核心服务实现
│ │ ├── memos-service.ts # Memos API 服务
│ │ └── file-service.ts # 文件处理服务
│ └── ui/ # 用户界面组件
│ └── settings-tab.ts # 设置页面
├── main.ts # 主插件文件
├── manifest.json # 插件清单
└── package.json # 项目配置
```
### 代码结构说明
- **models**: 包含所有类型定义和接口
- `settings.ts`: 定义插件设置和数据模型
- `plugin.ts`: 定义插件接口
- **services**: 核心服务实现
- `memos-service.ts`: 处理与 Memos API 的所有交互
- `file-service.ts`: 处理文件系统操作和内容格式化
- **ui**: 用户界面组件
- `settings-tab.ts`: 实现插件设置界面
## 兼容性
- 支持 Memos 版本:最高至 0.22.5
- 推荐使用 Memos v0.22.5 以获得最佳兼容性
## 故障排除
### 常见问题
1. **同步失败**
- 检查 Memos API URL 和访问令牌
- 确保 Obsidian 对同步目录有写入权限
2. **资源文件不加载**
- 验证 Memos 服务器是否可访问
- 检查网络连接
- 确保认证正确
3. **文件组织问题**
- 检查同步目录权限
- 验证路径配置
## 支持
如果遇到问题或有建议:
1. 访问 [GitHub 仓库](https://github.com/leoleelxh/obsidian-memos-sync-plugin)
2. 创建 issue 并详细描述问题
3. 包含相关错误信息和配置
## 许可证
MIT

View file

@ -1,217 +1,217 @@
# Obsidian Memos Sync Plugin
English | [简体中文](README.md)
A plugin that syncs Memos content to Obsidian, providing seamless integration.
# Interface Preview
## Settings Interface
![Settings Interface](/examples/example01.png)
## AI Enhancement
Summarize memos and auto-tag for better management
### Original Memos Content
![Original Memos Content](/examples/example03.png)
### AI Enhanced Content
![AI Enhanced](/examples/example02.png)
## AI Weekly Summary
Organize synced content into weekly summaries for easy review.
### AI Weekly Summary Content
![AI Weekly Summary](/examples/example04.png)
## Features
### Core Features
- One-click sync from Memos to Obsidian
- Manual and automatic sync modes
- Smart file organization (year/month structure)
- Customizable sync interval
- Intelligent sync control
- Auto-skip synced content
- Duplicate detection based on Memo ID
- Protection for synced files
### Content Processing
- Smart file naming
- Auto-extract content preview as filename
- Smart special character handling
- Auto-remove special characters from filename start
- Timestamp for identification: `(YYYY-MM-DD HH-mm)`
- Markdown content optimization
- Tag conversion (from Memos format #tag# to Obsidian format #tag)
- Support for images and file attachments
### AI Enhancement
```
Currently supports OpenAI, Gemini, and Ollama integration. Claude support is under testing.
```
#### AI Settings Guide
1. **Choose AI Provider**
- OpenAI
- Google Gemini
- Ollama (Local Deployment)
- Claude (In Development)
2. **Configuration Details**
- OpenAI Settings
- API Key: Enter your OpenAI API key
- Model Selection: Supports gpt-3.5-turbo, gpt-4, etc.
- Gemini Settings
- API Key: Enter your Google API key
- Model: gemini-pro
- Ollama Settings
- Server Address: e.g., http://localhost:11434
- Models: Supports llama2, mistral, etc.
3. **Feature Controls**
- Auto Summary: Generate summary for each memo
- Smart Tags: Auto-recommend relevant tags
- Weekly Report: Auto-generate weekly summaries
- Prompt Configuration: Customize AI prompts (under development)
- Auto-generate content summaries
- Smart tag recommendations
- Weekly content summary
- Generate independent summary files by week
- Auto-skip existing weekly summaries
- Save summaries in `{year}/weekly/` directory
- Include weekly highlights, statistics, and outlook
- Configurable AI features
### Resource Management
- Auto-download images and attachments
- Local resource storage (organized directory structure)
- Correct relative path generation
- Support for multiple file types
### Document Structure
- Content-first format design
- Inline image display
- Dedicated "Attachments" section
- Metadata stored in collapsible callouts
- Weekly summary file organization
- Directory structure: `sync_directory/YYYY/weekly/Week-WW-Summary.md`
- One summary file per week
- Includes week number, date range, and statistics
### File Organization
- Files organized by year/month: `sync_directory/YYYY/MM/`
- Resource files in dedicated directories
- Filenames include content preview and timestamp
- Example: `Meeting notes for project (2024-01-10 15-30).md`
- Weekly summary file structure:
```
sync_directory/
├── 2024/
│ ├── 01/
│ │ ├── memo1.md
│ │ └── resources/
│ │ └── attachments...
│ ├── 02/
│ │ └── memo2.md
│ └── weekly/
│ ├── Week-01-Summary.md
│ ├── Week-02-Summary.md
│ └── Week-03-Summary.md
└── 2023/
└── ...
```
## Installation
1. Open Obsidian Settings
2. Go to Community Plugins and disable Safe Mode
3. Click Browse and search for "Memos Sync"
4. Install the plugin
5. Enable the plugin
## Configuration
### Required Settings
- **Memos API URL**: Your Memos server API endpoint
- **Access Token**: Your Memos API access token
- **Sync Directory**: Location for Memos content in Obsidian
### Optional Settings
- **Sync Mode**: Choose manual or automatic sync
- **Sync Interval**: Set automatic sync frequency (if enabled)
- **Sync Limit**: Maximum entries to sync at once
## Usage
### Manual Sync
1. Click the sync icon in the toolbar
2. Wait for the sync process to complete
3. Your memos will be saved according to the organization structure
### Automatic Sync
1. Enable automatic sync in settings
2. Set your preferred sync interval
3. Plugin will sync automatically according to configuration
## Project Structure
```
obsidian-memos-sync/
├── src/
│ ├── models/ # Type definitions and interfaces
│ │ ├── settings.ts # Settings and type definitions
│ │ └── plugin.ts # Plugin interface definitions
│ ├── services/ # Core service implementations
│ │ ├── memos-service.ts # Memos API service
│ │ └── file-service.ts # File handling service
│ └── ui/ # User interface components
│ └── settings-tab.ts # Settings page
├── main.ts # Main plugin file
├── manifest.json # Plugin manifest
└── package.json # Project configuration
```
### Code Structure Description
- **models**: Contains all type definitions and interfaces
- `settings.ts`: Defines plugin settings and data models
- `plugin.ts`: Defines plugin interfaces
- **services**: Core service implementations
- `memos-service.ts`: Handles all Memos API interactions
- `file-service.ts`: Handles file system operations and content formatting
- **ui**: User interface components
- `settings-tab.ts`: Implements plugin settings interface
## Compatibility
- Supports Memos version: up to 0.22.5
- Recommended to use Memos v0.22.5 for best compatibility
## Troubleshooting
### Common Issues
1. **Sync Fails**
- Check Memos API URL and access token
- Ensure Obsidian has write permissions for sync directory
2. **Resource Files Not Loading**
- Verify Memos server accessibility
- Check network connection
- Ensure authentication is correct
3. **File Organization Issues**
- Check sync directory permissions
- Verify path configuration
## Support
If you encounter issues or have suggestions:
1. Visit the [GitHub Repository](https://github.com/leoleelxh/obsidian-memos-sync-plugin)
2. Create an issue with detailed description
3. Include relevant error messages and configuration
## License
# Obsidian Memos Sync Plugin
English | [简体中文](README.md)
A plugin that syncs Memos content to Obsidian, providing seamless integration.
# Interface Preview
## Settings Interface
![Settings Interface](/examples/example01.png)
## AI Enhancement
Summarize memos and auto-tag for better management
### Original Memos Content
![Original Memos Content](/examples/example03.png)
### AI Enhanced Content
![AI Enhanced](/examples/example02.png)
## AI Weekly Summary
Organize synced content into weekly summaries for easy review.
### AI Weekly Summary Content
![AI Weekly Summary](/examples/example04.png)
## Features
### Core Features
- One-click sync from Memos to Obsidian
- Manual and automatic sync modes
- Smart file organization (year/month structure)
- Customizable sync interval
- Intelligent sync control
- Auto-skip synced content
- Duplicate detection based on Memo ID
- Protection for synced files
### Content Processing
- Smart file naming
- Auto-extract content preview as filename
- Smart special character handling
- Auto-remove special characters from filename start
- Timestamp for identification: `(YYYY-MM-DD HH-mm)`
- Markdown content optimization
- Tag conversion (from Memos format #tag# to Obsidian format #tag)
- Support for images and file attachments
### AI Enhancement
```
Currently supports OpenAI, Gemini, and Ollama integration. Claude support is under testing.
```
#### AI Settings Guide
1. **Choose AI Provider**
- OpenAI
- Google Gemini
- Ollama (Local Deployment)
- Claude (In Development)
2. **Configuration Details**
- OpenAI Settings
- API Key: Enter your OpenAI API key
- Model Selection: Supports gpt-3.5-turbo, gpt-4, etc.
- Gemini Settings
- API Key: Enter your Google API key
- Model: gemini-pro
- Ollama Settings
- Server Address: e.g., http://localhost:11434
- Models: Supports llama2, mistral, etc.
3. **Feature Controls**
- Auto Summary: Generate summary for each memo
- Smart Tags: Auto-recommend relevant tags
- Weekly Report: Auto-generate weekly summaries
- Prompt Configuration: Customize AI prompts (under development)
- Auto-generate content summaries
- Smart tag recommendations
- Weekly content summary
- Generate independent summary files by week
- Auto-skip existing weekly summaries
- Save summaries in `{year}/weekly/` directory
- Include weekly highlights, statistics, and outlook
- Configurable AI features
### Resource Management
- Auto-download images and attachments
- Local resource storage (organized directory structure)
- Correct relative path generation
- Support for multiple file types
### Document Structure
- Content-first format design
- Inline image display
- Dedicated "Attachments" section
- Metadata stored in collapsible callouts
- Weekly summary file organization
- Directory structure: `sync_directory/YYYY/weekly/Week-WW-Summary.md`
- One summary file per week
- Includes week number, date range, and statistics
### File Organization
- Files organized by year/month: `sync_directory/YYYY/MM/`
- Resource files in dedicated directories
- Filenames include content preview and timestamp
- Example: `Meeting notes for project (2024-01-10 15-30).md`
- Weekly summary file structure:
```
sync_directory/
├── 2024/
│ ├── 01/
│ │ ├── memo1.md
│ │ └── resources/
│ │ └── attachments...
│ ├── 02/
│ │ └── memo2.md
│ └── weekly/
│ ├── Week-01-Summary.md
│ ├── Week-02-Summary.md
│ └── Week-03-Summary.md
└── 2023/
└── ...
```
## Installation
1. Open Obsidian Settings
2. Go to Community Plugins and disable Safe Mode
3. Click Browse and search for "Memos Sync"
4. Install the plugin
5. Enable the plugin
## Configuration
### Required Settings
- **Memos API URL**: Your Memos server API endpoint
- **Access Token**: Your Memos API access token
- **Sync Directory**: Location for Memos content in Obsidian
### Optional Settings
- **Sync Mode**: Choose manual or automatic sync
- **Sync Interval**: Set automatic sync frequency (if enabled)
- **Sync Limit**: Maximum entries to sync at once
## Usage
### Manual Sync
1. Click the sync icon in the toolbar
2. Wait for the sync process to complete
3. Your memos will be saved according to the organization structure
### Automatic Sync
1. Enable automatic sync in settings
2. Set your preferred sync interval
3. Plugin will sync automatically according to configuration
## Project Structure
```
obsidian-memos-sync/
├── src/
│ ├── models/ # Type definitions and interfaces
│ │ ├── settings.ts # Settings and type definitions
│ │ └── plugin.ts # Plugin interface definitions
│ ├── services/ # Core service implementations
│ │ ├── memos-service.ts # Memos API service
│ │ └── file-service.ts # File handling service
│ └── ui/ # User interface components
│ └── settings-tab.ts # Settings page
├── main.ts # Main plugin file
├── manifest.json # Plugin manifest
└── package.json # Project configuration
```
### Code Structure Description
- **models**: Contains all type definitions and interfaces
- `settings.ts`: Defines plugin settings and data models
- `plugin.ts`: Defines plugin interfaces
- **services**: Core service implementations
- `memos-service.ts`: Handles all Memos API interactions
- `file-service.ts`: Handles file system operations and content formatting
- **ui**: User interface components
- `settings-tab.ts`: Implements plugin settings interface
## Compatibility
- Supports Memos version: up to 0.22.5
- Recommended to use Memos v0.22.5 for best compatibility
## Troubleshooting
### Common Issues
1. **Sync Fails**
- Check Memos API URL and access token
- Ensure Obsidian has write permissions for sync directory
2. **Resource Files Not Loading**
- Verify Memos server accessibility
- Check network connection
- Ensure authentication is correct
3. **File Organization Issues**
- Check sync directory permissions
- Verify path configuration
## Support
If you encounter issues or have suggestions:
1. Visit the [GitHub Repository](https://github.com/leoleelxh/obsidian-memos-sync-plugin)
2. Create an issue with detailed description
3. Include relevant error messages and configuration
## License
MIT

View file

@ -1,7 +1,7 @@
{
"id": "memos-ai-sync",
"name": "Memos AI Sync",
"author": "leoleelxh",
"description": "Sync Memos content with AI enhancement",
"repo": "leoleelxh/obsidian-memos-ai-sync"
{
"id": "memos-ai-sync",
"name": "Memos AI Sync",
"author": "leoleelxh",
"description": "Sync Memos content with AI enhancement",
"repo": "leoleelxh/obsidian-memos-ai-sync"
}

View file

@ -1,48 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

View file

@ -1,11 +1,11 @@
{
"id": "memos-ai-sync",
"name": "Memos AI Sync",
"version": "1.0.2",
"minAppVersion": "1.4.0",
"description": "Sync Memos content with AI enhancement",
"author": "leoleelxh",
"authorUrl": "https://github.com/leoleelxh",
"isDesktopOnly": false,
"repo": "leoleelxh/obsidian-memos-ai-sync"
}
{
"id": "memos-ai-sync",
"name": "Memos AI Sync",
"version": "1.0.2",
"minAppVersion": "1.4.0",
"description": "Sync Memos content with AI enhancement",
"author": "leoleelxh",
"authorUrl": "https://github.com/leoleelxh",
"isDesktopOnly": false,
"repo": "leoleelxh/obsidian-memos-ai-sync"
}

4986
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,32 @@
{
"name": "obsidian-memos-sync",
"version": "1.0.0",
"description": "Sync Memos content to Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"memos",
"sync"
],
"author": "leoleelxh",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.8.0",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"builtin-modules": "^3.3.0",
"esbuild": "^0.19.4",
"obsidian": "^1.4.11",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
},
"dependencies": {
"@google/generative-ai": "^0.21.0",
"openai": "^4.28.0"
}
}
{
"name": "obsidian-memos-sync",
"version": "1.0.0",
"description": "Sync Memos content to Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"memos",
"sync"
],
"author": "leoleelxh",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.8.0",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"builtin-modules": "^3.3.0",
"esbuild": "^0.19.4",
"obsidian": "^1.4.11",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
},
"dependencies": {
"@google/generative-ai": "^0.21.0",
"openai": "^4.28.0"
}
}

View file

@ -1,8 +1,8 @@
import { Plugin } from 'obsidian';
import { MemosPluginSettings } from './settings';
export default interface MemosSyncPlugin extends Plugin {
settings: MemosPluginSettings;
loadSettings(): Promise<void>;
saveSettings(): Promise<void>;
import { Plugin } from 'obsidian';
import { MemosPluginSettings } from './settings';
export default interface MemosSyncPlugin extends Plugin {
settings: MemosPluginSettings;
loadSettings(): Promise<void>;
saveSettings(): Promise<void>;
}

View file

@ -1,72 +1,72 @@
// AI 模型类型
export type AIModelType = 'openai' | 'gemini' | 'claude' | 'ollama';
// Memo 项目接口
export interface MemoItem {
name: string;
uid: string;
content: string;
visibility: string;
createTime: string;
updateTime: string;
displayTime: string;
creator: string;
rowStatus: string;
pinned: boolean;
resources: Array<{
name: string;
uid: string;
filename: string;
type: string;
size: string;
createTime: string;
}>;
tags: string[];
}
// AI 功能配置
export interface AISettings {
enabled: boolean;
modelType: AIModelType;
modelName: string;
customModelName: string;
apiKey: string;
ollamaBaseUrl: string;
weeklyDigest: boolean;
autoTags: boolean;
intelligentSummary: boolean;
summaryLanguage: 'zh' | 'en' | 'ja' | 'ko';
}
// 主设置接口
export interface MemosPluginSettings {
memosApiUrl: string;
memosAccessToken: string;
syncDirectory: string;
syncFrequency: 'manual' | 'auto';
autoSyncInterval: number;
syncLimit: number;
ai: AISettings;
}
// 默认设置
export const DEFAULT_SETTINGS: MemosPluginSettings = {
memosApiUrl: '',
memosAccessToken: '',
syncDirectory: 'memos',
syncFrequency: 'manual',
autoSyncInterval: 30,
syncLimit: 1000,
ai: {
enabled: false,
modelType: 'openai',
modelName: 'gpt-4o',
customModelName: '',
apiKey: '',
ollamaBaseUrl: 'http://localhost:11434',
weeklyDigest: true,
autoTags: true,
intelligentSummary: true,
summaryLanguage: 'zh'
}
// AI 模型类型
export type AIModelType = 'openai' | 'gemini' | 'claude' | 'ollama';
// Memo 项目接口
export interface MemoItem {
name: string;
uid: string;
content: string;
visibility: string;
createTime: string;
updateTime: string;
displayTime: string;
creator: string;
rowStatus: string;
pinned: boolean;
resources: Array<{
name: string;
uid: string;
filename: string;
type: string;
size: string;
createTime: string;
}>;
tags: string[];
}
// AI 功能配置
export interface AISettings {
enabled: boolean;
modelType: AIModelType;
modelName: string;
customModelName: string;
apiKey: string;
ollamaBaseUrl: string;
weeklyDigest: boolean;
autoTags: boolean;
intelligentSummary: boolean;
summaryLanguage: 'zh' | 'en' | 'ja' | 'ko';
}
// 主设置接口
export interface MemosPluginSettings {
memosApiUrl: string;
memosAccessToken: string;
syncDirectory: string;
syncFrequency: 'manual' | 'auto';
autoSyncInterval: number;
syncLimit: number;
ai: AISettings;
}
// 默认设置
export const DEFAULT_SETTINGS: MemosPluginSettings = {
memosApiUrl: '',
memosAccessToken: '',
syncDirectory: 'memos',
syncFrequency: 'manual',
autoSyncInterval: 30,
syncLimit: 1000,
ai: {
enabled: false,
modelType: 'openai',
modelName: 'gpt-4o',
customModelName: '',
apiKey: '',
ollamaBaseUrl: 'http://localhost:11434',
weeklyDigest: true,
autoTags: true,
intelligentSummary: true,
summaryLanguage: 'zh'
}
};

View file

@ -1,428 +1,428 @@
import { GoogleGenerativeAI } from '@google/generative-ai';
import OpenAI from 'openai';
import { Notice } from 'obsidian';
export interface AIService {
generateSummary(content: string, language?: string): Promise<string>;
generateTags(content: string): Promise<string[]>;
generateWeeklyDigest(contents: string[]): Promise<string>;
}
export const GEMINI_MODELS = {
'Gemini 1.5 Flash': 'gemini-1.5-flash',
'Gemini 1.5 Flash-8B': 'gemini-1.5-flash-8b',
'Gemini 1.5 Pro': 'gemini-1.5-pro',
'Gemini 1.0 Pro': 'gemini-1.0-pro',
'Text Embedding': 'text-embedding-004',
'AQA': 'aqa',
'自定义模型': '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',
'GPT-4o Mini (2024-07-18)': 'gpt-4o-mini-2024-07-18',
'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' // 已添加
} as const;
export const OLLAMA_MODELS = {
'Llama 2': 'llama2',
'Mistral': 'mistral',
'Mixtral': 'mixtral',
'CodeLlama': 'codellama',
'Phi': 'phi',
'Neural Chat': 'neural-chat',
'自定义模型': '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 - 对话优化模型'
} as const;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1秒
async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = MAX_RETRIES,
initialDelay: number = RETRY_DELAY
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (error.message.includes('429')) {
// 配额限制错误,等待更长时间
const delay = initialDelay * Math.pow(2, i);
console.log(`配额限制,等待 ${delay}ms 后重试...`);
await sleep(delay);
continue;
}
if (i === maxRetries - 1) {
throw error; // 最后一次重试失败,抛出错误
}
// 其他错误,继续重试
const delay = initialDelay * Math.pow(2, i);
console.log(`操作失败,等待 ${delay}ms 后重试...`);
await sleep(delay);
}
}
throw new Error('重试次数已达上限');
}
class GeminiService implements AIService {
private model: any;
constructor(apiKey: string, modelName?: string) {
const genAI = new GoogleGenerativeAI(apiKey);
this.model = genAI.getGenerativeModel({ model: modelName || GEMINI_MODELS['Gemini 1.5 Flash'] });
}
async generateSummary(content: string, language = 'zh'): Promise<string> {
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
return retryWithBackoff(async () => {
const result = await this.model.generateContent(prompt);
const text = result.response.text();
return text.trim();
});
}
async generateTags(content: string): Promise<string[]> {
const prompt = `请为以下内容生成3-5个相关标签不要带#号):\n\n${content}`;
return retryWithBackoff(async () => {
const result = await this.model.generateContent(prompt);
const text = result.response.text();
return text.split(/[,\s]+/).filter(Boolean);
});
}
async generateWeeklyDigest(contents: string[]): Promise<string> {
const combinedContent = contents.join('\n---\n');
const prompt = `请对下一周的内容进行总结和分析,生成一份周报。重点关注:
1.
2.
3.
4.
\n${combinedContent}`;
return retryWithBackoff(async () => {
const result = await this.model.generateContent(prompt);
const text = result.response.text();
return text.trim();
});
}
}
export class OpenAIService implements AIService {
private client: OpenAI;
private model: string;
private encryptionKey: Uint8Array;
// 生成随机 IV
private async generateIV(): Promise<Uint8Array> {
return crypto.getRandomValues(new Uint8Array(12));
}
// 生成加密密钥
private async generateKey(): Promise<CryptoKey> {
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true,
['encrypt', 'decrypt']
);
}
// 加密 API 密钥
private async encryptApiKey(apiKey: string): Promise<string> {
const iv = await this.generateIV();
const key = await this.generateKey();
const encodedText = new TextEncoder().encode(apiKey);
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
encodedText
);
const encryptedArray = new Uint8Array(encryptedData);
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);
const encryptedData = this.base64ToArrayBuffer(encryptedStr);
const keyData = this.base64ToArrayBuffer(keyStr);
const key = await crypto.subtle.importKey(
'raw',
keyData,
'AES-GCM',
true,
['decrypt']
);
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encryptedData
);
return new TextDecoder().decode(decryptedData);
}
private arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
private base64ToArrayBuffer(base64: string): Uint8Array {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
constructor() {
this.encryptionKey = crypto.getRandomValues(new Uint8Array(32));
}
async initialize(apiKey: string, modelName?: string) {
try {
if (!apiKey) {
throw new Error('API 密钥不能为空');
}
// 基本的 API 密钥格式验证
if (!apiKey.startsWith('sk-') || apiKey.length < 20) {
throw new Error('无效的 API 密钥格式');
}
// 加密存储 API 密钥
const encryptedKey = await this.encryptApiKey(apiKey);
this.client = new OpenAI({
apiKey: await this.decryptApiKey(encryptedKey),
baseURL: 'https://api.openai.com/v1',
dangerouslyAllowBrowser: true
});
// 验证 API 密钥
try {
await this.client.models.list();
} catch (error) {
throw new Error('API 密钥验证失败');
}
// 如果是自定义模型,使用 customModelName
this.model = modelName || OPENAI_MODELS['GPT-4o'];
console.log('OpenAI 服务初始化成功,使用模型:', this.model);
new Notice(`AI 服务初始化成功`);
} catch (error) {
console.error('OpenAI 服务初始化失败:', error);
new Notice(`AI 服务初始化失败: ${error.message}`);
throw error;
}
}
async generateSummary(content: string, language = 'zh'): Promise<string> {
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
return retryWithBackoff(async () => {
const response = await this.client.chat.completions.create({
model: this.model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 500
});
return response.choices[0]?.message?.content?.trim() || '';
});
}
async generateTags(content: string): Promise<string[]> {
const prompt = `请为以下内容生成3-5个相关标签不要带#号):\n\n${content}`;
return retryWithBackoff(async () => {
const response = await this.client.chat.completions.create({
model: this.model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 100
});
const text = response.choices[0]?.message?.content || '';
return text.split(/[,\s]+/).filter(Boolean);
});
}
async generateWeeklyDigest(contents: string[]): Promise<string> {
const combinedContent = contents.join('\n---\n');
const prompt = `请对以下一周的内容进行总结和分析,生成一份周报。要求:
1.
2.
3.
4.
\n${combinedContent}`;
return retryWithBackoff(async () => {
const response = await this.client.chat.completions.create({
model: this.model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1000
});
return response.choices[0]?.message?.content?.trim() || '';
});
}
}
class OllamaService implements AIService {
private baseUrl: string;
private model: string;
constructor(baseUrl: string = 'http://localhost:11434', modelName?: string) {
this.baseUrl = baseUrl;
this.model = modelName || OLLAMA_MODELS['Llama 2'];
}
private async generateCompletion(prompt: string): Promise<string> {
try {
const response = await fetch(`${this.baseUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
prompt: prompt,
stream: false,
options: {
temperature: 0.7,
top_p: 0.9,
max_tokens: 1000,
}
})
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
const data = await response.json();
return data.response;
} catch (error) {
console.error('Ollama API error:', error);
throw error;
}
}
async generateSummary(content: string, language = 'zh'): Promise<string> {
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
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}`;
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}`;
return retryWithBackoff(async () => {
const response = await this.generateCompletion(prompt);
return response.trim();
});
}
}
export function createAIService(type: string, apiKey?: string, modelName?: string): AIService {
switch (type.toLowerCase()) {
case 'gemini':
if (!apiKey) {
throw new Error('未配置 Gemini API 密钥');
}
return new GeminiService(apiKey, modelName);
case 'openai':
if (!apiKey) {
throw new Error('未配置 OpenAI API 密钥');
}
const service = new OpenAIService();
service.initialize(apiKey, modelName);
return service;
case 'ollama':
return new OllamaService(apiKey || 'http://localhost:11434', modelName);
default:
console.log('使用默认的空 AI 服务');
return createDummyAIService();
}
}
export function createDummyAIService(): AIService {
return {
generateSummary: async () => '',
generateTags: async () => [],
generateWeeklyDigest: async () => ''
};
import { GoogleGenerativeAI } from '@google/generative-ai';
import OpenAI from 'openai';
import { Notice } from 'obsidian';
export interface AIService {
generateSummary(content: string, language?: string): Promise<string>;
generateTags(content: string): Promise<string[]>;
generateWeeklyDigest(contents: string[]): Promise<string>;
}
export const GEMINI_MODELS = {
'Gemini 1.5 Flash': 'gemini-1.5-flash',
'Gemini 1.5 Flash-8B': 'gemini-1.5-flash-8b',
'Gemini 1.5 Pro': 'gemini-1.5-pro',
'Gemini 1.0 Pro': 'gemini-1.0-pro',
'Text Embedding': 'text-embedding-004',
'AQA': 'aqa',
'自定义模型': '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',
'GPT-4o Mini (2024-07-18)': 'gpt-4o-mini-2024-07-18',
'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' // 已添加
} as const;
export const OLLAMA_MODELS = {
'Llama 2': 'llama2',
'Mistral': 'mistral',
'Mixtral': 'mixtral',
'CodeLlama': 'codellama',
'Phi': 'phi',
'Neural Chat': 'neural-chat',
'自定义模型': '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 - 对话优化模型'
} as const;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1秒
async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = MAX_RETRIES,
initialDelay: number = RETRY_DELAY
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (error.message.includes('429')) {
// 配额限制错误,等待更长时间
const delay = initialDelay * Math.pow(2, i);
console.log(`配额限制,等待 ${delay}ms 后重试...`);
await sleep(delay);
continue;
}
if (i === maxRetries - 1) {
throw error; // 最后一次重试失败,抛出错误
}
// 其他错误,继续重试
const delay = initialDelay * Math.pow(2, i);
console.log(`操作失败,等待 ${delay}ms 后重试...`);
await sleep(delay);
}
}
throw new Error('重试次数已达上限');
}
class GeminiService implements AIService {
private model: any;
constructor(apiKey: string, modelName?: string) {
const genAI = new GoogleGenerativeAI(apiKey);
this.model = genAI.getGenerativeModel({ model: modelName || GEMINI_MODELS['Gemini 1.5 Flash'] });
}
async generateSummary(content: string, language = 'zh'): Promise<string> {
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
return retryWithBackoff(async () => {
const result = await this.model.generateContent(prompt);
const text = result.response.text();
return text.trim();
});
}
async generateTags(content: string): Promise<string[]> {
const prompt = `请为以下内容生成3-5个相关标签不要带#号):\n\n${content}`;
return retryWithBackoff(async () => {
const result = await this.model.generateContent(prompt);
const text = result.response.text();
return text.split(/[,\s]+/).filter(Boolean);
});
}
async generateWeeklyDigest(contents: string[]): Promise<string> {
const combinedContent = contents.join('\n---\n');
const prompt = `请对下一周的内容进行总结和分析,生成一份周报。重点关注:
1.
2.
3.
4.
\n${combinedContent}`;
return retryWithBackoff(async () => {
const result = await this.model.generateContent(prompt);
const text = result.response.text();
return text.trim();
});
}
}
export class OpenAIService implements AIService {
private client: OpenAI;
private model: string;
private encryptionKey: Uint8Array;
// 生成随机 IV
private async generateIV(): Promise<Uint8Array> {
return crypto.getRandomValues(new Uint8Array(12));
}
// 生成加密密钥
private async generateKey(): Promise<CryptoKey> {
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true,
['encrypt', 'decrypt']
);
}
// 加密 API 密钥
private async encryptApiKey(apiKey: string): Promise<string> {
const iv = await this.generateIV();
const key = await this.generateKey();
const encodedText = new TextEncoder().encode(apiKey);
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
encodedText
);
const encryptedArray = new Uint8Array(encryptedData);
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);
const encryptedData = this.base64ToArrayBuffer(encryptedStr);
const keyData = this.base64ToArrayBuffer(keyStr);
const key = await crypto.subtle.importKey(
'raw',
keyData,
'AES-GCM',
true,
['decrypt']
);
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encryptedData
);
return new TextDecoder().decode(decryptedData);
}
private arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
private base64ToArrayBuffer(base64: string): Uint8Array {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
constructor() {
this.encryptionKey = crypto.getRandomValues(new Uint8Array(32));
}
async initialize(apiKey: string, modelName?: string) {
try {
if (!apiKey) {
throw new Error('API 密钥不能为空');
}
// 基本的 API 密钥格式验证
if (!apiKey.startsWith('sk-') || apiKey.length < 20) {
throw new Error('无效的 API 密钥格式');
}
// 加密存储 API 密钥
const encryptedKey = await this.encryptApiKey(apiKey);
this.client = new OpenAI({
apiKey: await this.decryptApiKey(encryptedKey),
baseURL: 'https://api.openai.com/v1',
dangerouslyAllowBrowser: true
});
// 验证 API 密钥
try {
await this.client.models.list();
} catch (error) {
throw new Error('API 密钥验证失败');
}
// 如果是自定义模型,使用 customModelName
this.model = modelName || OPENAI_MODELS['GPT-4o'];
console.log('OpenAI 服务初始化成功,使用模型:', this.model);
new Notice(`AI 服务初始化成功`);
} catch (error) {
console.error('OpenAI 服务初始化失败:', error);
new Notice(`AI 服务初始化失败: ${error.message}`);
throw error;
}
}
async generateSummary(content: string, language = 'zh'): Promise<string> {
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
return retryWithBackoff(async () => {
const response = await this.client.chat.completions.create({
model: this.model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 500
});
return response.choices[0]?.message?.content?.trim() || '';
});
}
async generateTags(content: string): Promise<string[]> {
const prompt = `请为以下内容生成3-5个相关标签不要带#号):\n\n${content}`;
return retryWithBackoff(async () => {
const response = await this.client.chat.completions.create({
model: this.model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 100
});
const text = response.choices[0]?.message?.content || '';
return text.split(/[,\s]+/).filter(Boolean);
});
}
async generateWeeklyDigest(contents: string[]): Promise<string> {
const combinedContent = contents.join('\n---\n');
const prompt = `请对以下一周的内容进行总结和分析,生成一份周报。要求:
1.
2.
3.
4.
\n${combinedContent}`;
return retryWithBackoff(async () => {
const response = await this.client.chat.completions.create({
model: this.model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1000
});
return response.choices[0]?.message?.content?.trim() || '';
});
}
}
class OllamaService implements AIService {
private baseUrl: string;
private model: string;
constructor(baseUrl: string = 'http://localhost:11434', modelName?: string) {
this.baseUrl = baseUrl;
this.model = modelName || OLLAMA_MODELS['Llama 2'];
}
private async generateCompletion(prompt: string): Promise<string> {
try {
const response = await fetch(`${this.baseUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
prompt: prompt,
stream: false,
options: {
temperature: 0.7,
top_p: 0.9,
max_tokens: 1000,
}
})
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
const data = await response.json();
return data.response;
} catch (error) {
console.error('Ollama API error:', error);
throw error;
}
}
async generateSummary(content: string, language = 'zh'): Promise<string> {
const prompt = `请用${language === 'zh' ? '中文' : 'English'}总结以下内容的要点:\n\n${content}`;
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}`;
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}`;
return retryWithBackoff(async () => {
const response = await this.generateCompletion(prompt);
return response.trim();
});
}
}
export function createAIService(type: string, apiKey?: string, modelName?: string): AIService {
switch (type.toLowerCase()) {
case 'gemini':
if (!apiKey) {
throw new Error('未配置 Gemini API 密钥');
}
return new GeminiService(apiKey, modelName);
case 'openai':
if (!apiKey) {
throw new Error('未配置 OpenAI API 密钥');
}
const service = new OpenAIService();
service.initialize(apiKey, modelName);
return service;
case 'ollama':
return new OllamaService(apiKey || 'http://localhost:11434', modelName);
default:
console.log('使用默认的空 AI 服务');
return createDummyAIService();
}
}
export function createDummyAIService(): AIService {
return {
generateSummary: async () => '',
generateTags: async () => [],
generateWeeklyDigest: async () => ''
};
}

View file

@ -1,193 +1,193 @@
import type { AIService } from './ai-service';
import type { MemoItem } from '../models/settings';
import type { Vault } from 'obsidian';
export class ContentService {
constructor(
private aiService: AIService,
private aiEnabled: boolean,
private enableSummary: boolean,
private enableTags: boolean,
private summaryLanguage: string,
private vault: Vault,
private syncDirectory: string
) {}
private isContentSuitableForAI(content: string): boolean {
const cleanContent = content
.replace(/\[([^\]]*)\]\([^)]*\)/g, '')
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '')
.replace(/```[\s\S]*?```/g, '')
.trim();
return cleanContent.length >= 10;
}
async processMemoContent(memo: MemoItem): Promise<string> {
const { content } = memo;
const title = this.extractTitle(content);
const mainContent = title ? content.slice(title.length).trim() : content;
let processedContent = title ? `# ${title}\n\n` : '';
if (this.aiEnabled && this.isContentSuitableForAI(content)) {
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`;
}
}
if (this.enableTags) {
const tags = await this.aiService.generateTags(content);
if (tags?.length > 0) {
processedContent += `> [!info]- 相关标签\n> ${tags.map(tag => `#${tag}`).join(' ')}\n\n`;
}
}
}
processedContent += mainContent;
return processedContent.trim();
}
private extractTitle(content: string): string | null {
const lines = content.split('\n');
const firstLine = lines[0].trim();
// 如果第一行是标题格式(# 开头),提取标题文本
if (firstLine.startsWith('# ')) {
return firstLine.slice(2).trim();
}
return null;
}
private async weeklyDigestExists(year: string, week: string): Promise<boolean> {
const weeklyDigestPath = this.getWeeklyDigestPath(year, week);
return await this.vault.adapter.exists(weeklyDigestPath);
}
private getWeeklyDigestPath(year: string, week: string): string {
const weeklyDigestDir = `${this.syncDirectory}/${year}/weekly`;
const fileName = `${week}周总结.md`;
return `${weeklyDigestDir}/${fileName}`;
}
private async ensureDirectoryExists(dirPath: string): Promise<void> {
if (!(await this.vault.adapter.exists(dirPath))) {
await this.vault.adapter.mkdir(dirPath);
}
}
async generateWeeklyDigest(memos: MemoItem[]): Promise<void> {
if (!this.aiEnabled) {
console.log('AI 功能未启用,跳过周总结生成');
return;
}
const suitableMemos = memos.filter(memo => this.isContentSuitableForAI(memo.content));
if (suitableMemos.length === 0) {
console.log('没有足够的内容生成周总结');
return;
}
const weekGroups = this.groupMemosByWeek(suitableMemos);
for (const [weekKey, weekMemos] of Object.entries(weekGroups)) {
const [year, week] = weekKey.split('-W');
// 检查该周的总结是否已存在
if (await this.weeklyDigestExists(year, week)) {
console.log(`${week} 周的总结已存在,跳过生成`);
continue;
}
// 确保周总结目录存在
const weeklyDigestDir = `${this.syncDirectory}/${year}/weekly`;
await this.ensureDirectoryExists(weeklyDigestDir);
// 生成该周的总结
const contents = weekMemos.map(memo => memo.content);
const digest = await this.aiService.generateWeeklyDigest(contents);
if (digest?.trim()) {
const weeklyContent = this.formatWeeklyDigest(digest, year, week, weekMemos.length);
const weeklyDigestPath = this.getWeeklyDigestPath(year, week);
try {
await this.vault.create(weeklyDigestPath, weeklyContent);
console.log(`成功生成第 ${week} 周总结: ${weeklyDigestPath}`);
} catch (error) {
console.error(`生成第 ${week} 周总结失败:`, error);
}
}
}
}
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})
## 🌟
${digest}
## 📊
- 📝 ${memoCount}
- 📅 ${weekRange}
## 💪
> [!quote]
>
---
*${new Date().toLocaleString('zh-CN', { hour12: false })}*
`;
}
private getWeekDateRange(year: number, week: number): string {
const firstDayOfYear = new Date(year, 0, 1);
const daysToFirstMonday = (8 - firstDayOfYear.getDay()) % 7;
const firstMonday = new Date(year, 0, 1 + daysToFirstMonday);
const weekStart = new Date(firstMonday);
weekStart.setDate(firstMonday.getDate() + (week - 1) * 7);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
const formatDate = (date: Date): string => {
return `${date.getMonth() + 1}${date.getDate()}`;
};
return `${formatDate(weekStart)} - ${formatDate(weekEnd)}`;
}
private groupMemosByWeek(memos: MemoItem[]): { [key: string]: MemoItem[] } {
const groups: { [key: string]: MemoItem[] } = {};
for (const memo of memos) {
const date = new Date(memo.createTime);
const year = date.getFullYear();
const week = this.getWeekNumber(date);
const key = `${year}-W${week.toString().padStart(2, '0')}`;
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(memo);
}
return groups;
}
private getWeekNumber(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
import type { AIService } from './ai-service';
import type { MemoItem } from '../models/settings';
import type { Vault } from 'obsidian';
export class ContentService {
constructor(
private aiService: AIService,
private aiEnabled: boolean,
private enableSummary: boolean,
private enableTags: boolean,
private summaryLanguage: string,
private vault: Vault,
private syncDirectory: string
) {}
private isContentSuitableForAI(content: string): boolean {
const cleanContent = content
.replace(/\[([^\]]*)\]\([^)]*\)/g, '')
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '')
.replace(/```[\s\S]*?```/g, '')
.trim();
return cleanContent.length >= 10;
}
async processMemoContent(memo: MemoItem): Promise<string> {
const { content } = memo;
const title = this.extractTitle(content);
const mainContent = title ? content.slice(title.length).trim() : content;
let processedContent = title ? `# ${title}\n\n` : '';
if (this.aiEnabled && this.isContentSuitableForAI(content)) {
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`;
}
}
if (this.enableTags) {
const tags = await this.aiService.generateTags(content);
if (tags?.length > 0) {
processedContent += `> [!info]- 相关标签\n> ${tags.map(tag => `#${tag}`).join(' ')}\n\n`;
}
}
}
processedContent += mainContent;
return processedContent.trim();
}
private extractTitle(content: string): string | null {
const lines = content.split('\n');
const firstLine = lines[0].trim();
// 如果第一行是标题格式(# 开头),提取标题文本
if (firstLine.startsWith('# ')) {
return firstLine.slice(2).trim();
}
return null;
}
private async weeklyDigestExists(year: string, week: string): Promise<boolean> {
const weeklyDigestPath = this.getWeeklyDigestPath(year, week);
return await this.vault.adapter.exists(weeklyDigestPath);
}
private getWeeklyDigestPath(year: string, week: string): string {
const weeklyDigestDir = `${this.syncDirectory}/${year}/weekly`;
const fileName = `${week}周总结.md`;
return `${weeklyDigestDir}/${fileName}`;
}
private async ensureDirectoryExists(dirPath: string): Promise<void> {
if (!(await this.vault.adapter.exists(dirPath))) {
await this.vault.adapter.mkdir(dirPath);
}
}
async generateWeeklyDigest(memos: MemoItem[]): Promise<void> {
if (!this.aiEnabled) {
console.log('AI 功能未启用,跳过周总结生成');
return;
}
const suitableMemos = memos.filter(memo => this.isContentSuitableForAI(memo.content));
if (suitableMemos.length === 0) {
console.log('没有足够的内容生成周总结');
return;
}
const weekGroups = this.groupMemosByWeek(suitableMemos);
for (const [weekKey, weekMemos] of Object.entries(weekGroups)) {
const [year, week] = weekKey.split('-W');
// 检查该周的总结是否已存在
if (await this.weeklyDigestExists(year, week)) {
console.log(`${week} 周的总结已存在,跳过生成`);
continue;
}
// 确保周总结目录存在
const weeklyDigestDir = `${this.syncDirectory}/${year}/weekly`;
await this.ensureDirectoryExists(weeklyDigestDir);
// 生成该周的总结
const contents = weekMemos.map(memo => memo.content);
const digest = await this.aiService.generateWeeklyDigest(contents);
if (digest?.trim()) {
const weeklyContent = this.formatWeeklyDigest(digest, year, week, weekMemos.length);
const weeklyDigestPath = this.getWeeklyDigestPath(year, week);
try {
await this.vault.create(weeklyDigestPath, weeklyContent);
console.log(`成功生成第 ${week} 周总结: ${weeklyDigestPath}`);
} catch (error) {
console.error(`生成第 ${week} 周总结失败:`, error);
}
}
}
}
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})
## 🌟
${digest}
## 📊
- 📝 ${memoCount}
- 📅 ${weekRange}
## 💪
> [!quote]
>
---
*${new Date().toLocaleString('zh-CN', { hour12: false })}*
`;
}
private getWeekDateRange(year: number, week: number): string {
const firstDayOfYear = new Date(year, 0, 1);
const daysToFirstMonday = (8 - firstDayOfYear.getDay()) % 7;
const firstMonday = new Date(year, 0, 1 + daysToFirstMonday);
const weekStart = new Date(firstMonday);
weekStart.setDate(firstMonday.getDate() + (week - 1) * 7);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
const formatDate = (date: Date): string => {
return `${date.getMonth() + 1}${date.getDate()}`;
};
return `${formatDate(weekStart)} - ${formatDate(weekEnd)}`;
}
private groupMemosByWeek(memos: MemoItem[]): { [key: string]: MemoItem[] } {
const groups: { [key: string]: MemoItem[] } = {};
for (const memo of memos) {
const date = new Date(memo.createTime);
const year = date.getFullYear();
const week = this.getWeekNumber(date);
const key = `${year}-W${week.toString().padStart(2, '0')}`;
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(memo);
}
return groups;
}
private getWeekNumber(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
}

View file

@ -1,229 +1,229 @@
import type { TFile, Vault } from 'obsidian';
import type { MemoItem } from '../models/settings';
import type { MemosService } from './memos-service';
export class FileService {
constructor(
private vault: Vault,
private syncDirectory: string,
private memosService: MemosService
) {}
private formatDateTime(date: Date, format: 'filename' | 'display' = 'display'): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
if (format === 'filename') {
return `${year}-${month}-${day} ${hours}-${minutes}`;
}
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
private sanitizeFileName(fileName: string): string {
let sanitized = fileName.replace(/^[\\/:*?"<>|#\s]+/, '');
sanitized = sanitized
.replace(/\s+/g, ' ')
.replace(/[\\/:*?"<>|#]/g, '')
.trim();
return sanitized || 'untitled';
}
private getRelativePath(fromPath: string, toPath: string): string {
const fromParts = fromPath.split('/');
const toParts = toPath.split('/');
fromParts.pop();
let i = 0;
while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
i++;
}
const goBack = fromParts.length - i;
const relativePath = [
...Array(goBack).fill('..'),
...toParts.slice(i)
].join('/');
console.log(`Relative path from ${fromPath} to ${toPath}: ${relativePath}`);
return relativePath;
}
private isImageFile(filename: string): boolean {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
const ext = filename.toLowerCase().split('.').pop();
return ext ? imageExtensions.includes(`.${ext}`) : false;
}
private async ensureDirectoryExists(dirPath: string): Promise<void> {
if (!(await this.vault.adapter.exists(dirPath))) {
await this.vault.adapter.mkdir(dirPath);
}
}
private getContentPreview(content: string): string {
let preview = content
.replace(/^>\s*\[!.*?\].*$/gm, '')
.replace(/^>\s.*$/gm, '')
.replace(/^\s*#\s+/gm, '')
.replace(/[_*~`]|_{2,}|\*{2,}|~{2,}/g, '')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '')
.replace(/\n+/g, ' ')
.trim();
if (!preview) {
return 'Untitled';
}
if (preview.length > 50) {
preview = `${preview.slice(0, 50)}...`;
}
return preview;
}
private async getMemoFiles(): Promise<string[]> {
const files: string[] = [];
const processDirectory = async (dirPath: string) => {
const items = await this.vault.adapter.list(dirPath);
for (const file of items.files) {
if (file.endsWith('.md')) {
files.push(file);
}
}
for (const dir of items.folders) {
await processDirectory(dir);
}
};
await processDirectory(this.syncDirectory);
return files;
}
async isMemoExists(memoId: string): Promise<boolean> {
try {
const files = await this.getMemoFiles();
for (const file of files) {
const content = await this.vault.adapter.read(file);
if (content.includes(`> - ID: ${memoId}`)) {
return true;
}
}
return false;
} catch (error) {
console.error('检查 memo 是否存在时出错:', error);
return false;
}
}
async saveMemoToFile(memo: MemoItem): Promise<void> {
try {
const exists = await this.isMemoExists(memo.name);
if (exists) {
console.log(`Memo ${memo.name} 已存在,跳过`);
return;
}
const date = new Date(memo.createTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const yearDir = `${this.syncDirectory}/${year}`;
const monthDir = `${yearDir}/${month}`;
await this.ensureDirectoryExists(yearDir);
await this.ensureDirectoryExists(monthDir);
const contentPreview = memo.content
? this.getContentPreview(memo.content)
: this.sanitizeFileName(memo.name.replace('memos/', ''));
const timeStr = this.formatDateTime(date, 'filename');
const fileName = this.sanitizeFileName(`${contentPreview} (${timeStr}).md`);
const filePath = `${monthDir}/${fileName}`;
let content = memo.content || '';
content = content.replace(/\#([^\#\s]+)\#/g, '#$1');
let documentContent = content;
if (memo.resources && memo.resources.length > 0) {
const images = memo.resources.filter(r => this.isImageFile(r.filename));
const otherFiles = memo.resources.filter(r => !this.isImageFile(r.filename));
if (images.length > 0) {
documentContent += '\n\n';
for (const image of images) {
const resourceData = await this.memosService.downloadResource(image);
if (resourceData) {
const resourceDir = `${monthDir}/resources`;
await this.ensureDirectoryExists(resourceDir);
const localFilename = `${image.name.split('/').pop()}_${this.sanitizeFileName(image.filename)}`;
const localPath = `${resourceDir}/${localFilename}`;
await this.vault.adapter.writeBinary(localPath, resourceData);
const relativePath = this.getRelativePath(filePath, localPath);
documentContent += `![${image.filename}](${relativePath})\n`;
}
}
}
if (otherFiles.length > 0) {
documentContent += '\n\n### Attachments\n';
for (const file of otherFiles) {
const resourceData = await this.memosService.downloadResource(file);
if (resourceData) {
const resourceDir = `${monthDir}/resources`;
await this.ensureDirectoryExists(resourceDir);
const localFilename = `${file.name.split('/').pop()}_${this.sanitizeFileName(file.filename)}`;
const localPath = `${resourceDir}/${localFilename}`;
await this.vault.adapter.writeBinary(localPath, resourceData);
const relativePath = this.getRelativePath(filePath, localPath);
documentContent += `- [${file.filename}](${relativePath})\n`;
}
}
}
}
const tags = (memo.content || '').match(/\#([^\#\s]+)(?:\#|\s|$)/g) || [];
const cleanTags = tags.map(tag => tag.replace(/^\#|\#$/g, '').trim());
documentContent += '\n\n---\n';
documentContent += '> [!note]- Memo Properties\n';
documentContent += `> - Created: ${this.formatDateTime(new Date(memo.createTime))}\n`;
documentContent += `> - Updated: ${this.formatDateTime(new Date(memo.updateTime))}\n`;
documentContent += '> - Type: memo\n';
if (cleanTags.length > 0) {
documentContent += `> - Tags: [${cleanTags.join(', ')}]\n`;
}
documentContent += `> - ID: ${memo.name}\n`;
documentContent += `> - Visibility: ${memo.visibility.toLowerCase()}\n`;
try {
const exists = await this.vault.adapter.exists(filePath);
if (exists) {
const file = this.vault.getAbstractFileByPath(filePath) as TFile;
if (file) {
await this.vault.modify(file, documentContent);
}
} else {
await this.vault.create(filePath, documentContent);
}
console.log(`Saved memo to: ${filePath}`);
} catch (error) {
console.error(`Failed to save memo to file: ${filePath}`, error);
throw new Error(`Failed to save memo: ${error.message}`);
}
} catch (error) {
console.error('保存 memo 到文件时出错:', error);
throw new Error(`Failed to save memo: ${error.message}`);
}
}
import type { TFile, Vault } from 'obsidian';
import type { MemoItem } from '../models/settings';
import type { MemosService } from './memos-service';
export class FileService {
constructor(
private vault: Vault,
private syncDirectory: string,
private memosService: MemosService
) {}
private formatDateTime(date: Date, format: 'filename' | 'display' = 'display'): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
if (format === 'filename') {
return `${year}-${month}-${day} ${hours}-${minutes}`;
}
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
private sanitizeFileName(fileName: string): string {
let sanitized = fileName.replace(/^[\\/:*?"<>|#\s]+/, '');
sanitized = sanitized
.replace(/\s+/g, ' ')
.replace(/[\\/:*?"<>|#]/g, '')
.trim();
return sanitized || 'untitled';
}
private getRelativePath(fromPath: string, toPath: string): string {
const fromParts = fromPath.split('/');
const toParts = toPath.split('/');
fromParts.pop();
let i = 0;
while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
i++;
}
const goBack = fromParts.length - i;
const relativePath = [
...Array(goBack).fill('..'),
...toParts.slice(i)
].join('/');
console.log(`Relative path from ${fromPath} to ${toPath}: ${relativePath}`);
return relativePath;
}
private isImageFile(filename: string): boolean {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
const ext = filename.toLowerCase().split('.').pop();
return ext ? imageExtensions.includes(`.${ext}`) : false;
}
private async ensureDirectoryExists(dirPath: string): Promise<void> {
if (!(await this.vault.adapter.exists(dirPath))) {
await this.vault.adapter.mkdir(dirPath);
}
}
private getContentPreview(content: string): string {
let preview = content
.replace(/^>\s*\[!.*?\].*$/gm, '')
.replace(/^>\s.*$/gm, '')
.replace(/^\s*#\s+/gm, '')
.replace(/[_*~`]|_{2,}|\*{2,}|~{2,}/g, '')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '')
.replace(/\n+/g, ' ')
.trim();
if (!preview) {
return 'Untitled';
}
if (preview.length > 50) {
preview = `${preview.slice(0, 50)}...`;
}
return preview;
}
private async getMemoFiles(): Promise<string[]> {
const files: string[] = [];
const processDirectory = async (dirPath: string) => {
const items = await this.vault.adapter.list(dirPath);
for (const file of items.files) {
if (file.endsWith('.md')) {
files.push(file);
}
}
for (const dir of items.folders) {
await processDirectory(dir);
}
};
await processDirectory(this.syncDirectory);
return files;
}
async isMemoExists(memoId: string): Promise<boolean> {
try {
const files = await this.getMemoFiles();
for (const file of files) {
const content = await this.vault.adapter.read(file);
if (content.includes(`> - ID: ${memoId}`)) {
return true;
}
}
return false;
} catch (error) {
console.error('检查 memo 是否存在时出错:', error);
return false;
}
}
async saveMemoToFile(memo: MemoItem): Promise<void> {
try {
const exists = await this.isMemoExists(memo.name);
if (exists) {
console.log(`Memo ${memo.name} 已存在,跳过`);
return;
}
const date = new Date(memo.createTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const yearDir = `${this.syncDirectory}/${year}`;
const monthDir = `${yearDir}/${month}`;
await this.ensureDirectoryExists(yearDir);
await this.ensureDirectoryExists(monthDir);
const contentPreview = memo.content
? this.getContentPreview(memo.content)
: this.sanitizeFileName(memo.name.replace('memos/', ''));
const timeStr = this.formatDateTime(date, 'filename');
const fileName = this.sanitizeFileName(`${contentPreview} (${timeStr}).md`);
const filePath = `${monthDir}/${fileName}`;
let content = memo.content || '';
content = content.replace(/\#([^\#\s]+)\#/g, '#$1');
let documentContent = content;
if (memo.resources && memo.resources.length > 0) {
const images = memo.resources.filter(r => this.isImageFile(r.filename));
const otherFiles = memo.resources.filter(r => !this.isImageFile(r.filename));
if (images.length > 0) {
documentContent += '\n\n';
for (const image of images) {
const resourceData = await this.memosService.downloadResource(image);
if (resourceData) {
const resourceDir = `${monthDir}/resources`;
await this.ensureDirectoryExists(resourceDir);
const localFilename = `${image.name.split('/').pop()}_${this.sanitizeFileName(image.filename)}`;
const localPath = `${resourceDir}/${localFilename}`;
await this.vault.adapter.writeBinary(localPath, resourceData);
const relativePath = this.getRelativePath(filePath, localPath);
documentContent += `![${image.filename}](${relativePath})\n`;
}
}
}
if (otherFiles.length > 0) {
documentContent += '\n\n### Attachments\n';
for (const file of otherFiles) {
const resourceData = await this.memosService.downloadResource(file);
if (resourceData) {
const resourceDir = `${monthDir}/resources`;
await this.ensureDirectoryExists(resourceDir);
const localFilename = `${file.name.split('/').pop()}_${this.sanitizeFileName(file.filename)}`;
const localPath = `${resourceDir}/${localFilename}`;
await this.vault.adapter.writeBinary(localPath, resourceData);
const relativePath = this.getRelativePath(filePath, localPath);
documentContent += `- [${file.filename}](${relativePath})\n`;
}
}
}
}
const tags = (memo.content || '').match(/\#([^\#\s]+)(?:\#|\s|$)/g) || [];
const cleanTags = tags.map(tag => tag.replace(/^\#|\#$/g, '').trim());
documentContent += '\n\n---\n';
documentContent += '> [!note]- Memo Properties\n';
documentContent += `> - Created: ${this.formatDateTime(new Date(memo.createTime))}\n`;
documentContent += `> - Updated: ${this.formatDateTime(new Date(memo.updateTime))}\n`;
documentContent += '> - Type: memo\n';
if (cleanTags.length > 0) {
documentContent += `> - Tags: [${cleanTags.join(', ')}]\n`;
}
documentContent += `> - ID: ${memo.name}\n`;
documentContent += `> - Visibility: ${memo.visibility.toLowerCase()}\n`;
try {
const exists = await this.vault.adapter.exists(filePath);
if (exists) {
const file = this.vault.getAbstractFileByPath(filePath) as TFile;
if (file) {
await this.vault.modify(file, documentContent);
}
} else {
await this.vault.create(filePath, documentContent);
}
console.log(`Saved memo to: ${filePath}`);
} catch (error) {
console.error(`Failed to save memo to file: ${filePath}`, error);
throw new Error(`Failed to save memo: ${error.message}`);
}
} catch (error) {
console.error('保存 memo 到文件时出错:', error);
throw new Error(`Failed to save memo: ${error.message}`);
}
}
}

View file

@ -1,124 +1,124 @@
import type { MemoItem } from '../models/settings';
export interface MemosResponse {
memos: MemoItem[];
nextPageToken?: string;
}
export class MemosService {
constructor(
private apiUrl: string,
private accessToken: string,
private syncLimit: number
) {}
async fetchAllMemos(): Promise<MemoItem[]> {
try {
console.log('开始获取 memosAPI URL:', this.apiUrl);
console.log('Access Token:', this.accessToken ? '已设置' : '未设置');
console.log('同步限制:', this.syncLimit, '条');
const allMemos: MemoItem[] = [];
let pageToken: string | undefined;
const pageSize = Math.min(100, this.syncLimit);
// 验证 API URL 格式
if (!this.apiUrl.includes('/api/v1')) {
throw new Error('API URL 格式不正确,请确保包含 /api/v1');
}
do {
const baseUrl = this.apiUrl;
const url = `${baseUrl}/memos`;
// 构建请求参数
const params = new URLSearchParams({
'rowStatus': 'NORMAL',
'limit': pageSize.toString()
});
if (pageToken) {
params.set('pageToken', pageToken);
}
const finalUrl = `${url}?${params.toString()}`;
console.log('请求 URL:', finalUrl);
const response = await fetch(finalUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Accept': 'application/json'
}
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText}\n响应内容: ${responseText}`);
}
const responseData = await response.json();
console.log('API 响应数据:', responseData);
if (!responseData || !Array.isArray(responseData.memos)) {
throw new Error('响应格式无效: 返回数据不包含 memos 数组');
}
const memos = responseData.memos;
pageToken = responseData.nextPageToken;
if (memos.length === 0) {
break; // 没有更多数据了
}
// 只添加需要的数量
const remainingCount = this.syncLimit - allMemos.length;
const neededCount = Math.min(memos.length, remainingCount);
allMemos.push(...memos.slice(0, neededCount));
console.log(`本次获取 ${neededCount} 条 memos总计: ${allMemos.length}/${this.syncLimit}`);
// 如果已经达到同步限制或没有下一页,就退出
if (allMemos.length >= this.syncLimit || !pageToken) {
break;
}
} while (true);
console.log(`最终返回 ${allMemos.length} 条 memos`);
return allMemos.sort((a, b) =>
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
);
} catch (error) {
console.error('获取 memos 失败:', error);
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new Error(`网络错误: 无法连接到 ${this.apiUrl}。请检查 URL 是否正确且可访问。`);
}
throw error;
}
}
async downloadResource(resource: { name: string; filename: string; type?: string }): Promise<ArrayBuffer | null> {
try {
const resourceId = resource.name.split('/').pop() || resource.name;
const resourceUrl = `${this.apiUrl.replace('/api/v1', '')}/file/resources/${resourceId}/${encodeURIComponent(resource.filename)}`;
console.log(`Downloading resource: ${resourceUrl}`);
const response = await fetch(resourceUrl, {
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});
if (!response.ok) {
console.error(`Failed to download resource: ${response.status} ${response.statusText}`);
return null;
}
return await response.arrayBuffer();
} catch (error) {
console.error('Error downloading resource:', error);
return null;
}
}
import type { MemoItem } from '../models/settings';
export interface MemosResponse {
memos: MemoItem[];
nextPageToken?: string;
}
export class MemosService {
constructor(
private apiUrl: string,
private accessToken: string,
private syncLimit: number
) {}
async fetchAllMemos(): Promise<MemoItem[]> {
try {
console.log('开始获取 memosAPI URL:', this.apiUrl);
console.log('Access Token:', this.accessToken ? '已设置' : '未设置');
console.log('同步限制:', this.syncLimit, '条');
const allMemos: MemoItem[] = [];
let pageToken: string | undefined;
const pageSize = Math.min(100, this.syncLimit);
// 验证 API URL 格式
if (!this.apiUrl.includes('/api/v1')) {
throw new Error('API URL 格式不正确,请确保包含 /api/v1');
}
do {
const baseUrl = this.apiUrl;
const url = `${baseUrl}/memos`;
// 构建请求参数
const params = new URLSearchParams({
'rowStatus': 'NORMAL',
'limit': pageSize.toString()
});
if (pageToken) {
params.set('pageToken', pageToken);
}
const finalUrl = `${url}?${params.toString()}`;
console.log('请求 URL:', finalUrl);
const response = await fetch(finalUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Accept': 'application/json'
}
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText}\n响应内容: ${responseText}`);
}
const responseData = await response.json();
console.log('API 响应数据:', responseData);
if (!responseData || !Array.isArray(responseData.memos)) {
throw new Error('响应格式无效: 返回数据不包含 memos 数组');
}
const memos = responseData.memos;
pageToken = responseData.nextPageToken;
if (memos.length === 0) {
break; // 没有更多数据了
}
// 只添加需要的数量
const remainingCount = this.syncLimit - allMemos.length;
const neededCount = Math.min(memos.length, remainingCount);
allMemos.push(...memos.slice(0, neededCount));
console.log(`本次获取 ${neededCount} 条 memos总计: ${allMemos.length}/${this.syncLimit}`);
// 如果已经达到同步限制或没有下一页,就退出
if (allMemos.length >= this.syncLimit || !pageToken) {
break;
}
} while (true);
console.log(`最终返回 ${allMemos.length} 条 memos`);
return allMemos.sort((a, b) =>
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
);
} catch (error) {
console.error('获取 memos 失败:', error);
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new Error(`网络错误: 无法连接到 ${this.apiUrl}。请检查 URL 是否正确且可访问。`);
}
throw error;
}
}
async downloadResource(resource: { name: string; filename: string; type?: string }): Promise<ArrayBuffer | null> {
try {
const resourceId = resource.name.split('/').pop() || resource.name;
const resourceUrl = `${this.apiUrl.replace('/api/v1', '')}/file/resources/${resourceId}/${encodeURIComponent(resource.filename)}`;
console.log(`Downloading resource: ${resourceUrl}`);
const response = await fetch(resourceUrl, {
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});
if (!response.ok) {
console.error(`Failed to download resource: ${response.status} ${response.statusText}`);
return null;
}
return await response.arrayBuffer();
} catch (error) {
console.error('Error downloading resource:', error);
return null;
}
}
}

View file

@ -1,90 +1,90 @@
import { Notice, setIcon } from 'obsidian';
type SyncStatus = 'idle' | 'syncing' | 'error' | 'success';
export class StatusService {
private statusBarItem: HTMLElement;
private currentStatus: SyncStatus = 'idle';
private syncStartTime = 0;
private progressCount = 0;
private totalCount = 0;
constructor(statusBarItem: HTMLElement) {
this.statusBarItem = statusBarItem;
this.updateStatusBar();
}
private updateStatusBar() {
let icon: string;
let text: string;
switch (this.currentStatus) {
case 'syncing': {
icon = 'sync';
const progress = this.totalCount ? ` ${this.progressCount}/${this.totalCount}` : '';
const elapsed = this.syncStartTime ? ` (${Math.round((Date.now() - this.syncStartTime) / 1000)}s)` : '';
text = `同步中${progress}${elapsed}`;
break;
}
case 'error': {
icon = 'alert-circle';
text = '同步失败';
break;
}
case 'success': {
icon = 'check-circle';
text = '同步完成';
break;
}
default: {
icon = 'clock';
text = '等待同步';
}
}
this.statusBarItem.empty();
setIcon(this.statusBarItem.createSpan(), icon);
this.statusBarItem.createSpan({ text: ` ${text}` });
}
startSync(totalItems: number) {
this.currentStatus = 'syncing';
this.syncStartTime = Date.now();
this.progressCount = 0;
this.totalCount = totalItems;
this.updateStatusBar();
new Notice('开始同步 Memos');
}
updateProgress(current: number, message?: string) {
this.progressCount = current;
this.updateStatusBar();
if (message) {
new Notice(message);
}
}
setError(error: string) {
this.currentStatus = 'error';
this.updateStatusBar();
new Notice(`同步失败: ${error}`, 5000);
console.error('Sync failed:', error);
}
setSuccess(message: string) {
this.currentStatus = 'success';
this.updateStatusBar();
new Notice(message);
// 5秒后重置状态为空闲
setTimeout(() => {
this.currentStatus = 'idle';
this.updateStatusBar();
}, 5000);
}
setIdle() {
this.currentStatus = 'idle';
this.updateStatusBar();
}
import { Notice, setIcon } from 'obsidian';
type SyncStatus = 'idle' | 'syncing' | 'error' | 'success';
export class StatusService {
private statusBarItem: HTMLElement;
private currentStatus: SyncStatus = 'idle';
private syncStartTime = 0;
private progressCount = 0;
private totalCount = 0;
constructor(statusBarItem: HTMLElement) {
this.statusBarItem = statusBarItem;
this.updateStatusBar();
}
private updateStatusBar() {
let icon: string;
let text: string;
switch (this.currentStatus) {
case 'syncing': {
icon = 'sync';
const progress = this.totalCount ? ` ${this.progressCount}/${this.totalCount}` : '';
const elapsed = this.syncStartTime ? ` (${Math.round((Date.now() - this.syncStartTime) / 1000)}s)` : '';
text = `同步中${progress}${elapsed}`;
break;
}
case 'error': {
icon = 'alert-circle';
text = '同步失败';
break;
}
case 'success': {
icon = 'check-circle';
text = '同步完成';
break;
}
default: {
icon = 'clock';
text = '等待同步';
}
}
this.statusBarItem.empty();
setIcon(this.statusBarItem.createSpan(), icon);
this.statusBarItem.createSpan({ text: ` ${text}` });
}
startSync(totalItems: number) {
this.currentStatus = 'syncing';
this.syncStartTime = Date.now();
this.progressCount = 0;
this.totalCount = totalItems;
this.updateStatusBar();
new Notice('开始同步 Memos');
}
updateProgress(current: number, message?: string) {
this.progressCount = current;
this.updateStatusBar();
if (message) {
new Notice(message);
}
}
setError(error: string) {
this.currentStatus = 'error';
this.updateStatusBar();
new Notice(`同步失败: ${error}`, 5000);
console.error('Sync failed:', error);
}
setSuccess(message: string) {
this.currentStatus = 'success';
this.updateStatusBar();
new Notice(message);
// 5秒后重置状态为空闲
setTimeout(() => {
this.currentStatus = 'idle';
this.updateStatusBar();
}, 5000);
}
setIdle() {
this.currentStatus = 'idle';
this.updateStatusBar();
}
}

View file

@ -1,352 +1,352 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import type { AIModelType, MemosPluginSettings } from '../models/settings';
import type MemosSyncPlugin from '../../main';
import { GEMINI_MODELS, OPENAI_MODELS, OLLAMA_MODELS, MODEL_DESCRIPTIONS } from '../services/ai-service';
export class MemosSyncSettingTab extends PluginSettingTab {
plugin: MemosSyncPlugin;
constructor(app: App, plugin: MemosSyncPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// 基本设置
containerEl.createEl('h2', { text: '基本设置' });
new Setting(containerEl)
.setName('Memos API URL')
.setDesc('您的 Memos 服务器 API 地址')
.addText(text => text
.setPlaceholder('例如https://demo.usememos.com/api/v1')
.setValue(this.plugin.settings.memosApiUrl)
.onChange(async (value) => {
this.plugin.settings.memosApiUrl = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('访问令牌')
.setDesc('您的 Memos API 访问令牌')
.addText(text => text
.setPlaceholder('输入访问令牌')
.setValue(this.plugin.settings.memosAccessToken)
.onChange(async (value) => {
this.plugin.settings.memosAccessToken = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('同步目录')
.setDesc('Memos 内容在 Obsidian 中的存储位置')
.addText(text => text
.setPlaceholder('例如memos')
.setValue(this.plugin.settings.syncDirectory)
.onChange(async (value) => {
this.plugin.settings.syncDirectory = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('同步模式')
.setDesc('选择手动同步或自动同步')
.addDropdown(dropdown => dropdown
.addOption('manual', '手动同步')
.addOption('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('同步间隔')
.setDesc('自动同步的时间间隔(分钟)')
.addText(text => text
.setPlaceholder('例如30')
.setValue(String(this.plugin.settings.autoSyncInterval))
.onChange(async (value) => {
const interval = Number.parseInt(value, 10);
if (Number.isFinite(interval) && interval > 0) {
this.plugin.settings.autoSyncInterval = interval;
await this.plugin.saveSettings();
}
}));
}
new Setting(containerEl)
.setName('同步条数')
.setDesc('每次同步的最大条目数')
.addText(text => text
.setPlaceholder('例如100')
.setValue(String(this.plugin.settings.syncLimit))
.onChange(async (value) => {
const limit = Number.parseInt(value, 10);
if (Number.isFinite(limit) && limit > 0) {
this.plugin.settings.syncLimit = limit;
await this.plugin.saveSettings();
}
}));
// AI 功能设置
containerEl.createEl('h2', { text: 'AI 功能设置' });
new Setting(containerEl)
.setName('启用 AI 功能')
.setDesc('开启或关闭 AI 增强功能')
.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 模型')
.setDesc('选择要使用的 AI 模型')
.addDropdown(dropdown => dropdown
.addOption('openai', 'OpenAI')
.addOption('gemini', 'Google Gemini')
.addOption('claude', 'Anthropic Claude')
.addOption('ollama', 'Ollama')
.setValue(this.plugin.settings.ai.modelType)
.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 密钥')
.setDesc('您的 AI 服务 API 密钥')
.addText(text => text
.setPlaceholder('输入 API 密钥')
.setValue(this.plugin.settings.ai.apiKey)
.onChange(async (value) => {
this.plugin.settings.ai.apiKey = value;
await this.plugin.saveSettings();
}));
}
// 显示特定模型的选项
this.displayModelOptions(containerEl);
// AI 功能选项
new Setting(containerEl)
.setName('每周汇总')
.setDesc('自动生成每周内容汇总')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.ai.weeklyDigest)
.onChange(async (value) => {
this.plugin.settings.ai.weeklyDigest = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('自动标签')
.setDesc('根据内容自动生成标签')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.ai.autoTags)
.onChange(async (value) => {
this.plugin.settings.ai.autoTags = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('智能摘要')
.setDesc('自动生成内容摘要')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.ai.intelligentSummary)
.onChange(async (value) => {
this.plugin.settings.ai.intelligentSummary = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('摘要语言')
.setDesc('选择摘要生成的语言')
.addDropdown(dropdown => dropdown
.addOption('zh', '中文')
.addOption('en', '英文')
.addOption('ja', '日文')
.addOption('ko', '韩文')
.setValue(this.plugin.settings.ai.summaryLanguage)
.onChange(async (value: 'zh' | 'en' | 'ja' | 'ko') => {
this.plugin.settings.ai.summaryLanguage = value;
await this.plugin.saveSettings();
}));
}
}
private displayModelOptions(containerEl: HTMLElement) {
const modelType = this.plugin.settings.ai.modelType;
if (modelType === 'gemini') {
new Setting(containerEl)
.setName('Gemini 模型')
.setDesc('选择要使用的 Gemini 模型')
.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 (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如gemini-pro-latest')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
} else if (modelType === 'openai') {
new Setting(containerEl)
.setName('OpenAI 模型')
.setDesc('选择要使用的 OpenAI 模型')
.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 (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如gpt-4-1106-preview')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
} else if (modelType === 'claude') {
new Setting(containerEl)
.setName('Claude 模型')
.setDesc('选择要使用的 Claude 模型')
.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')
.addOption('custom', '自定义模型 - 自定义模型名称');
const currentModel = this.plugin.settings.ai.modelName || 'claude-3-opus-20240229';
dropdown.setValue(currentModel);
dropdown.onChange(async (value) => {
this.plugin.settings.ai.modelName = value;
await this.plugin.saveSettings();
this.display();
});
});
if (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如claude-3-opus-next')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
} else if (modelType === 'ollama') {
// 添加 Ollama 服务地址设置
new Setting(containerEl)
.setName('Ollama 服务地址')
.setDesc('设置 Ollama 服务的地址(默认为 http://localhost:11434')
.addText(text => text
.setPlaceholder('http://localhost:11434')
.setValue(this.plugin.settings.ai.ollamaBaseUrl)
.onChange(async (value) => {
this.plugin.settings.ai.ollamaBaseUrl = value;
await this.plugin.saveSettings();
}));
// Ollama 模型选择
new Setting(containerEl)
.setName('Ollama 模型')
.setDesc('选择要使用的 Ollama 模型')
.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);
dropdown.onChange(async (value) => {
this.plugin.settings.ai.modelName = value;
await this.plugin.saveSettings();
this.display();
});
});
// 如果选择了自定义模型,显示输入框
if (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如llama2:13b')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
}
}
import { App, PluginSettingTab, Setting } from 'obsidian';
import type { AIModelType, MemosPluginSettings } from '../models/settings';
import type MemosSyncPlugin from '../../main';
import { GEMINI_MODELS, OPENAI_MODELS, OLLAMA_MODELS, MODEL_DESCRIPTIONS } from '../services/ai-service';
export class MemosSyncSettingTab extends PluginSettingTab {
plugin: MemosSyncPlugin;
constructor(app: App, plugin: MemosSyncPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// 基本设置
containerEl.createEl('h2', { text: '基本设置' });
new Setting(containerEl)
.setName('Memos API URL')
.setDesc('您的 Memos 服务器 API 地址')
.addText(text => text
.setPlaceholder('例如https://demo.usememos.com/api/v1')
.setValue(this.plugin.settings.memosApiUrl)
.onChange(async (value) => {
this.plugin.settings.memosApiUrl = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('访问令牌')
.setDesc('您的 Memos API 访问令牌')
.addText(text => text
.setPlaceholder('输入访问令牌')
.setValue(this.plugin.settings.memosAccessToken)
.onChange(async (value) => {
this.plugin.settings.memosAccessToken = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('同步目录')
.setDesc('Memos 内容在 Obsidian 中的存储位置')
.addText(text => text
.setPlaceholder('例如memos')
.setValue(this.plugin.settings.syncDirectory)
.onChange(async (value) => {
this.plugin.settings.syncDirectory = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('同步模式')
.setDesc('选择手动同步或自动同步')
.addDropdown(dropdown => dropdown
.addOption('manual', '手动同步')
.addOption('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('同步间隔')
.setDesc('自动同步的时间间隔(分钟)')
.addText(text => text
.setPlaceholder('例如30')
.setValue(String(this.plugin.settings.autoSyncInterval))
.onChange(async (value) => {
const interval = Number.parseInt(value, 10);
if (Number.isFinite(interval) && interval > 0) {
this.plugin.settings.autoSyncInterval = interval;
await this.plugin.saveSettings();
}
}));
}
new Setting(containerEl)
.setName('同步条数')
.setDesc('每次同步的最大条目数')
.addText(text => text
.setPlaceholder('例如100')
.setValue(String(this.plugin.settings.syncLimit))
.onChange(async (value) => {
const limit = Number.parseInt(value, 10);
if (Number.isFinite(limit) && limit > 0) {
this.plugin.settings.syncLimit = limit;
await this.plugin.saveSettings();
}
}));
// AI 功能设置
containerEl.createEl('h2', { text: 'AI 功能设置' });
new Setting(containerEl)
.setName('启用 AI 功能')
.setDesc('开启或关闭 AI 增强功能')
.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 模型')
.setDesc('选择要使用的 AI 模型')
.addDropdown(dropdown => dropdown
.addOption('openai', 'OpenAI')
.addOption('gemini', 'Google Gemini')
.addOption('claude', 'Anthropic Claude')
.addOption('ollama', 'Ollama')
.setValue(this.plugin.settings.ai.modelType)
.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 密钥')
.setDesc('您的 AI 服务 API 密钥')
.addText(text => text
.setPlaceholder('输入 API 密钥')
.setValue(this.plugin.settings.ai.apiKey)
.onChange(async (value) => {
this.plugin.settings.ai.apiKey = value;
await this.plugin.saveSettings();
}));
}
// 显示特定模型的选项
this.displayModelOptions(containerEl);
// AI 功能选项
new Setting(containerEl)
.setName('每周汇总')
.setDesc('自动生成每周内容汇总')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.ai.weeklyDigest)
.onChange(async (value) => {
this.plugin.settings.ai.weeklyDigest = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('自动标签')
.setDesc('根据内容自动生成标签')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.ai.autoTags)
.onChange(async (value) => {
this.plugin.settings.ai.autoTags = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('智能摘要')
.setDesc('自动生成内容摘要')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.ai.intelligentSummary)
.onChange(async (value) => {
this.plugin.settings.ai.intelligentSummary = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('摘要语言')
.setDesc('选择摘要生成的语言')
.addDropdown(dropdown => dropdown
.addOption('zh', '中文')
.addOption('en', '英文')
.addOption('ja', '日文')
.addOption('ko', '韩文')
.setValue(this.plugin.settings.ai.summaryLanguage)
.onChange(async (value: 'zh' | 'en' | 'ja' | 'ko') => {
this.plugin.settings.ai.summaryLanguage = value;
await this.plugin.saveSettings();
}));
}
}
private displayModelOptions(containerEl: HTMLElement) {
const modelType = this.plugin.settings.ai.modelType;
if (modelType === 'gemini') {
new Setting(containerEl)
.setName('Gemini 模型')
.setDesc('选择要使用的 Gemini 模型')
.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 (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如gemini-pro-latest')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
} else if (modelType === 'openai') {
new Setting(containerEl)
.setName('OpenAI 模型')
.setDesc('选择要使用的 OpenAI 模型')
.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 (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如gpt-4-1106-preview')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
} else if (modelType === 'claude') {
new Setting(containerEl)
.setName('Claude 模型')
.setDesc('选择要使用的 Claude 模型')
.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')
.addOption('custom', '自定义模型 - 自定义模型名称');
const currentModel = this.plugin.settings.ai.modelName || 'claude-3-opus-20240229';
dropdown.setValue(currentModel);
dropdown.onChange(async (value) => {
this.plugin.settings.ai.modelName = value;
await this.plugin.saveSettings();
this.display();
});
});
if (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如claude-3-opus-next')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
} else if (modelType === 'ollama') {
// 添加 Ollama 服务地址设置
new Setting(containerEl)
.setName('Ollama 服务地址')
.setDesc('设置 Ollama 服务的地址(默认为 http://localhost:11434')
.addText(text => text
.setPlaceholder('http://localhost:11434')
.setValue(this.plugin.settings.ai.ollamaBaseUrl)
.onChange(async (value) => {
this.plugin.settings.ai.ollamaBaseUrl = value;
await this.plugin.saveSettings();
}));
// Ollama 模型选择
new Setting(containerEl)
.setName('Ollama 模型')
.setDesc('选择要使用的 Ollama 模型')
.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);
dropdown.onChange(async (value) => {
this.plugin.settings.ai.modelName = value;
await this.plugin.saveSettings();
this.display();
});
});
// 如果选择了自定义模型,显示输入框
if (this.plugin.settings.ai.modelName === 'custom') {
new Setting(containerEl)
.setName('自定义模型名称')
.setDesc('输入要使用的自定义模型名称')
.addText(text => text
.setPlaceholder('例如llama2:13b')
.setValue(this.plugin.settings.ai.customModelName)
.onChange(async (value) => {
this.plugin.settings.ai.customModelName = value;
await this.plugin.saveSettings();
}));
}
}
}
}

View file

@ -1,24 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}

View file

@ -1,5 +1,5 @@
{
"1.0.0": "1.4.0",
"1.0.1": "1.4.0",
"1.0.2": "1.4.0"
{
"1.0.0": "1.4.0",
"1.0.1": "1.4.0",
"1.0.2": "1.4.0"
}