mirror of
https://github.com/erbanku/obsidian-memos-ai-sync.git
synced 2026-07-22 06:53:32 +00:00
chore: update plugin configuration and add license
This commit is contained in:
parent
9031a8676f
commit
9db56b629e
21 changed files with 4734 additions and 4734 deletions
76
.github/workflows/release.yml
vendored
76
.github/workflows/release.yml
vendored
|
|
@ -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
144
.gitignore
vendored
|
|
@ -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.*
|
||||
|
|
|
|||
130
CHANGELOG.md
130
CHANGELOG.md
|
|
@ -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
40
LICENSE
|
|
@ -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
434
README.md
|
|
@ -1,218 +1,218 @@
|
|||
# Obsidian Memos Sync Plugin
|
||||
|
||||
[English](README_EN.md) | 简体中文
|
||||
|
||||
将 Memos 内容同步到 Obsidian 的插件,提供无缝集成体验。
|
||||
|
||||
# 界面预览
|
||||
## 设置界面
|
||||

|
||||
## AI 增强
|
||||
可以进行memos的总结和自动打标签,便于管理
|
||||
|
||||
### 原memos内容
|
||||

|
||||
|
||||
### AI增强后内容
|
||||

|
||||
|
||||
## AI 周总结
|
||||
对同步的内容进行统一梳理,形成每周总结,便于review。
|
||||
|
||||
### AI 周总结内容
|
||||

|
||||
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 核心功能
|
||||
- 一键同步 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 的插件,提供无缝集成体验。
|
||||
|
||||
# 界面预览
|
||||
## 设置界面
|
||||

|
||||
## AI 增强
|
||||
可以进行memos的总结和自动打标签,便于管理
|
||||
|
||||
### 原memos内容
|
||||

|
||||
|
||||
### AI增强后内容
|
||||

|
||||
|
||||
## AI 周总结
|
||||
对同步的内容进行统一梳理,形成每周总结,便于review。
|
||||
|
||||
### AI 周总结内容
|
||||

|
||||
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 核心功能
|
||||
- 一键同步 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
|
||||
432
README_EN.md
432
README_EN.md
|
|
@ -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
|
||||

|
||||
## AI Enhancement
|
||||
Summarize memos and auto-tag for better management
|
||||
|
||||
### Original Memos Content
|
||||

|
||||
|
||||
### AI Enhanced Content
|
||||

|
||||
|
||||
## AI Weekly Summary
|
||||
Organize synced content into weekly summaries for easy review.
|
||||
|
||||
### AI Weekly Summary Content
|
||||

|
||||
|
||||
## 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
|
||||

|
||||
## AI Enhancement
|
||||
Summarize memos and auto-tag for better management
|
||||
|
||||
### Original Memos Content
|
||||

|
||||
|
||||
### AI Enhanced Content
|
||||

|
||||
|
||||
## AI Weekly Summary
|
||||
Organize synced content into weekly summaries for easy review.
|
||||
|
||||
### AI Weekly Summary Content
|
||||

|
||||
|
||||
## 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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
4986
package-lock.json
generated
File diff suppressed because it is too large
Load diff
64
package.json
64
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
};
|
||||
|
|
@ -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 () => ''
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 += `\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 += `\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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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('开始获取 memos,API 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('开始获取 memos,API 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue