Repository: haierkeys/fast-note-sync-service Branch: master Commit: 4adfaaa8432d Files: 547 Total size: 6.2 MB Directory structure: gitextract_kw46j47p/ ├── .github/ │ └── workflows/ │ ├── alpha-release.yml │ ├── mirror-to-cnb.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ ├── bootstrap.go │ ├── gorm_gen/ │ │ └── gen.go │ ├── mfmt/ │ │ └── main.go │ ├── model_gen/ │ │ └── gen.go │ ├── reset_password.go │ ├── root.go │ ├── run.go │ ├── run_server.go │ ├── upgrade.go │ └── version.go ├── config/ │ └── config.yaml ├── docker/ │ ├── Dockerfile │ ├── docker-compose.yaml │ ├── docker_image_clean.sh │ ├── docker_redeploy.sh │ └── entrypoint.sh ├── docs/ │ ├── API-EXTENSIONS.md │ ├── CHANGELOG.en.md │ ├── CHANGELOG.ja.md │ ├── CHANGELOG.ko.md │ ├── CHANGELOG.zh-CN.md │ ├── CHANGELOG.zh-TW.md │ ├── CHANGELOG_GUIDELINE.md │ ├── PR-DESCRIPTION.md │ ├── README.ja.md │ ├── README.ko.md │ ├── README.zh-CN.md │ ├── README.zh-TW.md │ ├── REST_API.md │ ├── Support.csv │ ├── Support.en.json │ ├── Support.en.md │ ├── Support.ja.json │ ├── Support.ja.md │ ├── Support.ko.json │ ├── Support.ko.md │ ├── Support.zh-CN.json │ ├── Support.zh-CN.md │ ├── Support.zh-TW.json │ ├── Support.zh-TW.md │ ├── SyncProtocol.md │ ├── admin_config_api.md │ ├── docs.go │ ├── skills/ │ │ └── fns-mcp/ │ │ ├── SKILL.md │ │ └── configs/ │ │ ├── cherry-studio.md │ │ ├── hermes.yaml │ │ └── openclaw.json │ ├── swagger.json │ ├── swagger.yaml │ ├── test_ws_debug.html │ ├── websocket_integration.md │ ├── ws_api.md │ └── ws_setting_clear_api.md ├── frontend/ │ ├── assets/ │ │ ├── alert-dialog-CfMssux5.js │ │ ├── alert-dialog-CfMssux5.js.br │ │ ├── auth-form-BjZ9qVzL.js │ │ ├── auth-form-BjZ9qVzL.js.br │ │ ├── badge-C63ATniC.js │ │ ├── badge-C63ATniC.js.br │ │ ├── canvas-viewer-Bt8OKmt9.css │ │ ├── canvas-viewer-Bt8OKmt9.css.br │ │ ├── canvas-viewer-Cxwbo1vR.js │ │ ├── canvas-viewer-Cxwbo1vR.js.br │ │ ├── checkbox-DhTHgmeh.js │ │ ├── checkbox-DhTHgmeh.js.br │ │ ├── circle-alert-EFzISefA.js │ │ ├── circle-alert-EFzISefA.js.br │ │ ├── clock-C9LPHszx.js │ │ ├── clock-C9LPHszx.js.br │ │ ├── copy-CEhXannp.js │ │ ├── copy-CEhXannp.js.br │ │ ├── database-eyf5nvY6.js │ │ ├── database-eyf5nvY6.js.br │ │ ├── download-CKtDCbjj.js │ │ ├── download-CKtDCbjj.js.br │ │ ├── en-vU35wTjd.js │ │ ├── en-vU35wTjd.js.br │ │ ├── eye-DrvrOb4o.js │ │ ├── eye-DrvrOb4o.js.br │ │ ├── file-manager-Bz0QGSbU.js │ │ ├── file-manager-Bz0QGSbU.js.br │ │ ├── file-type-DbD_pFnN.js │ │ ├── file-type-DbD_pFnN.js.br │ │ ├── font-loader-B-ynJ_1p.css │ │ ├── font-loader-B-ynJ_1p.css.br │ │ ├── font-loader-CIrh3KnA.js │ │ ├── font-loader-CIrh3KnA.js.br │ │ ├── format-CdHm7RWL.js │ │ ├── format-CdHm7RWL.js.br │ │ ├── git-automation-tBJ0Wppw.js │ │ ├── git-automation-tBJ0Wppw.js.br │ │ ├── git-branch-B1vNHBXG.js │ │ ├── git-branch-B1vNHBXG.js.br │ │ ├── github-Bzk-4SPC.js │ │ ├── github-Bzk-4SPC.js.br │ │ ├── hard-drive-Dw58lXyp.js │ │ ├── hard-drive-Dw58lXyp.js.br │ │ ├── history-BseqF3eb.js │ │ ├── history-BseqF3eb.js.br │ │ ├── image-BFJJNQpe.js │ │ ├── image-BFJJNQpe.js.br │ │ ├── index-JfsWWBj_.js │ │ ├── index-JfsWWBj_.js.br │ │ ├── ja-Q5acyAjl.js │ │ ├── ja-Q5acyAjl.js.br │ │ ├── ko-CMKMFQrR.js │ │ ├── ko-CMKMFQrR.js.br │ │ ├── main-BIi-kGYY.js │ │ ├── main-BIi-kGYY.js.br │ │ ├── markdown-editor-CX5kQlgI.js │ │ ├── markdown-editor-CX5kQlgI.js.br │ │ ├── markdown-editor-DMUawZD_.css │ │ ├── markdown-editor-DMUawZD_.css.br │ │ ├── monitor-BGNS5Y9j.js │ │ ├── monitor-BGNS5Y9j.js.br │ │ ├── note-handle-IK8dQjtF.js │ │ ├── note-handle-IK8dQjtF.js.br │ │ ├── note-manager-DjJcxkCE.js │ │ ├── note-manager-DjJcxkCE.js.br │ │ ├── pencil-DqQhr35g.js │ │ ├── pencil-DqQhr35g.js.br │ │ ├── plus-BBfuNxDX.js │ │ ├── plus-BBfuNxDX.js.br │ │ ├── refresh-cw-BxIJAPy3.js │ │ ├── refresh-cw-BxIJAPy3.js.br │ │ ├── search-DdihTHF8.js │ │ ├── search-DdihTHF8.js.br │ │ ├── select-CJF_alSt.js │ │ ├── select-CJF_alSt.js.br │ │ ├── server-DzJVVqse.js │ │ ├── server-DzJVVqse.js.br │ │ ├── setting-manager-DaP9o-yD.js │ │ ├── setting-manager-DaP9o-yD.js.br │ │ ├── share-2-BVJjAadJ.js │ │ ├── share-2-BVJjAadJ.js.br │ │ ├── share-CN7oeKGv.js │ │ ├── share-CN7oeKGv.js.br │ │ ├── shield-check-CH_gKEpx.js │ │ ├── shield-check-CH_gKEpx.js.br │ │ ├── sync-backup-Bp7n2yHp.js │ │ ├── sync-backup-Bp7n2yHp.js.br │ │ ├── sync-log-manager-Zjq-lA99.js │ │ ├── sync-log-manager-Zjq-lA99.js.br │ │ ├── system-settings-DSUsRYMo.js │ │ ├── system-settings-DSUsRYMo.js.br │ │ ├── table-D9wbHMTA.js │ │ ├── table-D9wbHMTA.js.br │ │ ├── text-cursor-input-Bphfsfyn.js │ │ ├── text-cursor-input-Bphfsfyn.js.br │ │ ├── tooltip-Dr-qRlmI.js │ │ ├── tooltip-Dr-qRlmI.js.br │ │ ├── trash-2-ad7PiUnC.js │ │ ├── trash-2-ad7PiUnC.js.br │ │ ├── vault-list-BzYzvdPK.js │ │ ├── vault-list-BzYzvdPK.js.br │ │ ├── zap-CLLhzk_y.js │ │ ├── zap-CLLhzk_y.js.br │ │ ├── zh-CN-BZhLE8JW.js │ │ ├── zh-CN-BZhLE8JW.js.br │ │ ├── zh-TW-DGtNjFz9.js │ │ ├── zh-TW-DGtNjFz9.js.br │ │ ├── zod-B54Zg8Xp.js │ │ └── zod-B54Zg8Xp.js.br │ ├── index.html │ ├── index.html.br │ ├── share.html │ ├── share.html.br │ └── static/ │ ├── fonts/ │ │ ├── local.css │ │ ├── local.css.br │ │ ├── remote.css │ │ └── remote.css.br │ └── images/ │ ├── icon-black.svg.br │ ├── icon.svg.br │ ├── site.svg.br │ └── site.webmanifest ├── go.mod ├── go.sum ├── internal/ │ ├── app/ │ │ ├── app.go │ │ ├── config.go │ │ ├── infra.go │ │ ├── repos.go │ │ ├── restart_linux.go │ │ ├── restart_unix.go │ │ ├── restart_windows.go │ │ ├── services.go │ │ ├── testing.go │ │ └── version.go │ ├── config/ │ │ ├── app.go │ │ ├── database.go │ │ ├── git.go │ │ ├── log.go │ │ ├── security.go │ │ ├── server.go │ │ ├── short_link.go │ │ ├── storage.go │ │ ├── tracer.go │ │ ├── tunnel.go │ │ ├── user.go │ │ └── webgui.go │ ├── dao/ │ │ ├── backup_repository.go │ │ ├── dao.go │ │ ├── dao_helper.go │ │ ├── file_repository.go │ │ ├── folder_repository.go │ │ ├── git_sync_repository.go │ │ ├── note_fts_repository.go │ │ ├── note_history_repository.go │ │ ├── note_link_repository.go │ │ ├── note_repository.go │ │ ├── setting_repository.go │ │ ├── storage_repository.go │ │ ├── sync_log_repository.go │ │ ├── user_repository.go │ │ ├── user_share_repository.go │ │ └── vault_repository.go │ ├── domain/ │ │ ├── domain_backup.go │ │ ├── domain_file.go │ │ ├── domain_folder.go │ │ ├── domain_git_sync.go │ │ ├── domain_note.go │ │ ├── domain_note_fts.go │ │ ├── domain_note_history.go │ │ ├── domain_note_link.go │ │ ├── domain_setting.go │ │ ├── domain_storage.go │ │ ├── domain_sync_log.go │ │ ├── domain_user.go │ │ ├── domain_user_share.go │ │ ├── domain_vault.go │ │ └── mocks/ │ │ ├── mock_backup_repository.go │ │ ├── mock_file_repository.go │ │ ├── mock_folder_repository.go │ │ ├── mock_git_sync_repository.go │ │ ├── mock_note_fts_repository.go │ │ ├── mock_note_history_repository.go │ │ ├── mock_note_link_repository.go │ │ ├── mock_note_repository.go │ │ ├── mock_setting_repository.go │ │ ├── mock_storage_repository.go │ │ ├── mock_sync_log_repository.go │ │ ├── mock_user_repository.go │ │ ├── mock_user_share_repository.go │ │ └── mock_vault_repository.go │ ├── dto/ │ │ ├── admin_dto.go │ │ ├── app_dto.go │ │ ├── backup.go │ │ ├── conflict_dto.go │ │ ├── file_dto.go │ │ ├── file_dto_ws.go │ │ ├── folder_dto.go │ │ ├── folder_dto_ws.go │ │ ├── git_sync_dto.go │ │ ├── note_dto.go │ │ ├── note_dto_ws.go │ │ ├── setting_dto.go │ │ ├── setting_dto_ws.go │ │ ├── share_dto.go │ │ ├── storage_dto.go │ │ ├── sync_log_dto.go │ │ ├── user_dto.go │ │ ├── vault_dto.go │ │ └── ws_dto.go │ ├── middleware/ │ │ ├── 404nofound.go │ │ ├── access_log.go │ │ ├── app_info.go │ │ ├── context_timeout.go │ │ ├── cors.go │ │ ├── lang.go │ │ ├── limiter.go │ │ ├── proxy.go │ │ ├── recovery.go │ │ ├── share_auth_token.go │ │ ├── simple_auth_token.go │ │ ├── static_compress.go │ │ ├── tracer.go │ │ └── user_auth_token.go │ ├── model/ │ │ ├── backup_config.gen.go │ │ ├── backup_history.gen.go │ │ ├── file.gen.go │ │ ├── folder.gen.go │ │ ├── git_sync_config.gen.go │ │ ├── git_sync_history.gen.go │ │ ├── model.go │ │ ├── note.gen.go │ │ ├── note_fts.go │ │ ├── note_history.gen.go │ │ ├── note_link.gen.go │ │ ├── schema_version.gen.go │ │ ├── setting.gen.go │ │ ├── sqlite_sequence.gen.go │ │ ├── storage.gen.go │ │ ├── sync_log.go │ │ ├── user.gen.go │ │ ├── user_share.gen.go │ │ └── vault.gen.go │ ├── query/ │ │ ├── backup_config.gen.go │ │ ├── backup_history.gen.go │ │ ├── file.gen.go │ │ ├── folder.gen.go │ │ ├── gen.go │ │ ├── git_sync_config.gen.go │ │ ├── git_sync_history.gen.go │ │ ├── note.gen.go │ │ ├── note_history.gen.go │ │ ├── note_link.gen.go │ │ ├── setting.gen.go │ │ ├── storage.gen.go │ │ ├── user.gen.go │ │ ├── user_share.gen.go │ │ └── vault.gen.go │ ├── routers/ │ │ ├── api_router/ │ │ │ ├── handler.go │ │ │ ├── handler_admin_control.go │ │ │ ├── handler_admin_control_test.go │ │ │ ├── handler_backup.go │ │ │ ├── handler_backup_test.go │ │ │ ├── handler_file.go │ │ │ ├── handler_file_test.go │ │ │ ├── handler_folder.go │ │ │ ├── handler_folder_test.go │ │ │ ├── handler_git_sync.go │ │ │ ├── handler_git_sync_test.go │ │ │ ├── handler_health.go │ │ │ ├── handler_health_test.go │ │ │ ├── handler_note.go │ │ │ ├── handler_note_history.go │ │ │ ├── handler_note_history_test.go │ │ │ ├── handler_note_test.go │ │ │ ├── handler_setting.go │ │ │ ├── handler_setting_test.go │ │ │ ├── handler_share.go │ │ │ ├── handler_share_test.go │ │ │ ├── handler_storage.go │ │ │ ├── handler_storage_test.go │ │ │ ├── handler_sync_log.go │ │ │ ├── handler_user.go │ │ │ ├── handler_user_test.go │ │ │ ├── handler_vault.go │ │ │ ├── handler_vault_test.go │ │ │ ├── handler_version.go │ │ │ ├── handler_version_test.go │ │ │ ├── metrics.go │ │ │ └── metrics_test.go │ │ ├── mcp_router/ │ │ │ ├── file_tools.go │ │ │ ├── mcp.go │ │ │ ├── mcp_test.go │ │ │ ├── note_tools.go │ │ │ ├── server.go │ │ │ └── vault_tools.go │ │ ├── pprof.go │ │ ├── router.go │ │ └── websocket_router/ │ │ ├── handler.go │ │ ├── ws_file.go │ │ ├── ws_folder.go │ │ ├── ws_note.go │ │ └── ws_setting.go │ ├── service/ │ │ ├── backup_service.go │ │ ├── backup_service_test.go │ │ ├── cloudflare_service.go │ │ ├── config.go │ │ ├── conflict_service.go │ │ ├── conflict_service_test.go │ │ ├── db_utils.go │ │ ├── file_service.go │ │ ├── folder_service.go │ │ ├── folder_service_test.go │ │ ├── git_sync_service.go │ │ ├── mocks/ │ │ │ ├── mock_backup_service.go │ │ │ ├── mock_cloudflare_service.go │ │ │ ├── mock_conflict_service.go │ │ │ ├── mock_file_service.go │ │ │ ├── mock_folder_service.go │ │ │ ├── mock_git_sync_service.go │ │ │ ├── mock_ngrok_service.go │ │ │ ├── mock_note_history_service.go │ │ │ ├── mock_note_link_service.go │ │ │ ├── mock_note_service.go │ │ │ ├── mock_setting_service.go │ │ │ ├── mock_share_service.go │ │ │ ├── mock_storage_service.go │ │ │ ├── mock_user_service.go │ │ │ └── mock_vault_service.go │ │ ├── ngrok_service.go │ │ ├── note_history_service.go │ │ ├── note_link_service.go │ │ ├── note_service.go │ │ ├── service.go │ │ ├── setting_service.go │ │ ├── share_service.go │ │ ├── share_service_test.go │ │ ├── storage_service.go │ │ ├── sync_log_service.go │ │ ├── user_service.go │ │ ├── user_service_test.go │ │ ├── vault_service.go │ │ └── vault_service_test.go │ ├── task/ │ │ ├── manager.go │ │ ├── registry.go │ │ ├── scheduler.go │ │ ├── task_backup.go │ │ ├── task_check_version.go │ │ ├── task_db_clean.go │ │ ├── task_file_session_temp_clean.go │ │ ├── task_note_history.go │ │ ├── task_sync_fid.go │ │ └── task_update_support.go │ └── upgrade/ │ ├── upgrade.go │ ├── upgrade_note_history_rename.go │ └── upgrade_note_history_rename_test.go ├── main.go ├── pkg/ │ ├── app/ │ │ ├── app.go │ │ ├── dateime.go │ │ ├── form.go │ │ ├── pagination.go │ │ ├── token.go │ │ ├── token_test.go │ │ ├── websocket.go │ │ └── websocket_client_test.go │ ├── code/ │ │ ├── code.go │ │ ├── code_test.go │ │ ├── common.go │ │ ├── lang.go │ │ ├── msg_en.go │ │ └── msg_zh_cn.go │ ├── convert/ │ │ ├── bool_int.go │ │ ├── convert.go │ │ ├── convert_test.go │ │ ├── copy_struct.go │ │ ├── json.go │ │ └── map.go │ ├── diff/ │ │ ├── diff.go │ │ ├── diff_test.go │ │ └── merge_scenarios_test.go │ ├── email/ │ │ ├── email.go │ │ └── email_test.go │ ├── errors/ │ │ ├── err.go │ │ ├── err_test.go │ │ └── errors.go │ ├── fileurl/ │ │ ├── file.go │ │ ├── fileurl_test.go │ │ ├── source_selector.go │ │ └── url.go │ ├── gin_tools/ │ │ ├── form.go │ │ ├── gin_tools_test.go │ │ ├── json.go │ │ └── param.go │ ├── httpclient/ │ │ ├── client.go │ │ └── client_test.go │ ├── json/ │ │ ├── json.go │ │ ├── json_sonic.go │ │ ├── json_std.go │ │ └── json_test.go │ ├── limiter/ │ │ ├── limiter.go │ │ ├── limiter_test.go │ │ └── method_limiter.go │ ├── logger/ │ │ ├── fields.go │ │ ├── logger.go │ │ └── logger_test.go │ ├── order/ │ │ ├── order_sn.go │ │ └── order_sn_test.go │ ├── rand/ │ │ ├── slice.go │ │ └── slice_test.go │ ├── safe_close/ │ │ ├── safe_close.go │ │ └── safe_close_test.go │ ├── shortlink/ │ │ ├── sink_cool.go │ │ └── sink_cool_test.go │ ├── storage/ │ │ ├── aliyun_oss/ │ │ │ ├── delete.go │ │ │ ├── operation.go │ │ │ ├── oss.go │ │ │ └── oss_test.go │ │ ├── aws_s3/ │ │ │ ├── delete.go │ │ │ ├── operation.go │ │ │ ├── s3.go │ │ │ └── s3_test.go │ │ ├── cloudflare_r2/ │ │ │ ├── delete.go │ │ │ ├── operation.go │ │ │ ├── r2.go │ │ │ └── r2_test.go │ │ ├── local_fs/ │ │ │ ├── delete.go │ │ │ ├── local.go │ │ │ ├── operation.go │ │ │ └── operation_test.go │ │ ├── minio/ │ │ │ ├── delete.go │ │ │ ├── minio.go │ │ │ ├── minio_test.go │ │ │ └── operation.go │ │ ├── storage.go │ │ ├── storage_test.go │ │ └── webdav/ │ │ ├── delete.go │ │ ├── operation.go │ │ ├── webdav.go │ │ └── webdav_test.go │ ├── timex/ │ │ ├── time.go │ │ └── time_test.go │ ├── tracer/ │ │ ├── tracer.go │ │ └── tracer_test.go │ ├── util/ │ │ ├── archive.go │ │ ├── array.go │ │ ├── converter.go │ │ ├── crypto.go │ │ ├── frontmatter.go │ │ ├── frontmatter_test.go │ │ ├── hash.go │ │ ├── hash_test.go │ │ ├── link_parser.go │ │ ├── link_parser_test.go │ │ ├── machine.go │ │ ├── math.go │ │ ├── password.go │ │ ├── path.go │ │ ├── random.go │ │ ├── runtime.go │ │ ├── sys_info.go │ │ ├── time.go │ │ ├── tokenizer.go │ │ └── validator.go │ ├── validator/ │ │ ├── custom_validator.go │ │ └── custom_validator_test.go │ ├── workerpool/ │ │ ├── pool.go │ │ └── pool_test.go │ └── writequeue/ │ ├── manager.go │ └── manager_test.go └── scripts/ ├── .air.toml ├── .env ├── db.sql ├── docker-composer/ │ ├── docker-compose.yaml │ ├── docker-re.sh │ └── nginx/ │ └── site.conf ├── gen_support_md.js ├── go_install.sh ├── gormgen.sh ├── https-nginx-example.conf ├── process_support.py ├── process_support_csv.js ├── quest_install.sh ├── test-api.sh ├── test-edge-cases.sh ├── test-folder-api.sh ├── translate_commit.py ├── translate_support.py └── update-version.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/alpha-release.yml ================================================ name: Go-Alpha-Release # 触发条件配置 on: push: branches-ignore: - "master" paths: - "internal/app/version.go" # 权限配置:允许脚本写入仓库内容(用于发布 Release) permissions: contents: write packages: write env: NAME: ${{ github.event.repository.name }} jobs: check-version: if: github.actor == 'haierkeys' runs-on: ubuntu-latest outputs: should_release: ${{ steps.check.outputs.should_release }} release_tag: ${{ steps.check.outputs.release_tag }} commit_msg: ${{ steps.check.outputs.commit_msg }} steps: - uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Check Version id: check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # 从 internal/app/version.go (HEAD) 读取当前版本 # 预期格式: var Version string = "0.0.1" CURRENT_VERSION=$(grep -E 'Version\s+string' internal/app/version.go | awk -F '"' '{print $2}') echo "当前版本 (HEAD): $CURRENT_VERSION" # 从 internal/app/version.go (HEAD~1) 获取上一个版本 # 使用 git show 获取上一次提交的文件内容 # 注意: 我们将其包裹在一个块中以处理 HEAD~1 可能不存在的情况 (例如: 初始提交) if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then PREV_FILE_CONTENT=$(git show HEAD~1:internal/app/version.go) PREV_VERSION=$(echo "$PREV_FILE_CONTENT" | grep -E 'Version\s+string' | awk -F '"' '{print $2}') else PREV_VERSION="0.0.0" fi # 如果 PREV_VERSION 为空 (例如: grep 失败), 默认为 0.0.0 if [ -z "$PREV_VERSION" ]; then PREV_VERSION="0.0.0" fi echo "上一个版本 (HEAD~1): $PREV_VERSION" # 规范化版本以进行比较 (如果存在 'v' 前缀则移除) V_LAST=${PREV_VERSION#v} V_CURR=${CURRENT_VERSION#v} # 比较版本 if [ "$V_CURR" != "$V_LAST" ]; then # 使用 sort -V 确定当前版本是否严格大于旧版本 NEWER_VERSION=$(echo -e "$V_LAST\n$V_CURR" | sort -V | tail -n 1) if [ "$NEWER_VERSION" == "$V_CURR" ] && [ "$NEWER_VERSION" != "$V_LAST" ]; then echo "检测到新版本: $V_CURR > $V_LAST" echo "should_release=true" >> $GITHUB_OUTPUT echo "release_tag=$V_CURR" >> $GITHUB_OUTPUT else echo "版本 $V_CURR 不大于 $V_LAST。跳过发布..." echo "should_release=false" >> $GITHUB_OUTPUT fi else echo "版本匹配 ($V_CURR == $V_LAST)。跳过发布..." echo "should_release=false" >> $GITHUB_OUTPUT fi prepare-message: needs: check-version if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' runs-on: ubuntu-latest outputs: commit_msg: ${{ steps.trans.outputs.commit_msg }} steps: - uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Prepare Commit Message id: trans run: | # 获取提交文本信息 msg=$(git log -1 --pretty=%B) # 设置 Python 进行翻译 pip install deep-translator > /dev/null 2>&1 || true # 运行翻译脚本 export COMMIT_MSG="$msg" echo "commit_msg<> $GITHUB_OUTPUT if [ -f "scripts/translate_commit.py" ]; then python3 scripts/translate_commit.py >> $GITHUB_OUTPUT else echo "未找到翻译脚本,使用原始消息" echo "$msg" >> $GITHUB_OUTPUT fi echo "EOF" >> $GITHUB_OUTPUT create-release: needs: [check-version, prepare-message] if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' name: Create Release runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Create Release id: create_release uses: softprops/action-gh-release@v2 env: token: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ needs.check-version.outputs.release_tag }}-alpha name: ${{ needs.check-version.outputs.release_tag }}-alpha draft: false prerelease: true generate_release_notes: true body: ${{ needs.prepare-message.outputs.commit_msg }} target_commitish: ${{ github.sha }} # 明确指定在当前分支的 commit 上创建 tag overwrite_files: true build-binaries: needs: [check-version, prepare-message, create-release] if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - name: Check Go Version run: go version - name: Go Build Prepare run: go install github.com/mitchellh/gox@latest - name: Go Build Multi-platform env: COMMIT_MSG: ${{ needs.prepare-message.outputs.commit_msg }} run: make gox-all - name: Create Changelog env: COMMIT_MSG: ${{ needs.prepare-message.outputs.commit_msg }} run: | echo "${COMMIT_MSG}" > ./build/changelog.txt - name: Create GZip Archives for All Platforms env: RELEASE_BASENAME: ${{ env.NAME }}-${{ needs.check-version.outputs.release_tag }} run: | # 为所有平台创建 tar.gz 包 # Create tar.gz packages for all platforms declare -a PLATFORMS=("darwin_amd64" "darwin_arm64" "linux_amd64" "linux_arm64" "linux_arm" "windows_amd64") for PLATFORM in "${PLATFORMS[@]}"; do GOOS=$(echo "$PLATFORM" | cut -d'_' -f1) GOARCH=$(echo "$PLATFORM" | cut -d'_' -f2) # 这里的重命名逻辑:如果是 linux_arm,则将后缀改为 armv7l # Rename linux_arm to linux-armv7l for easier identification TARGET_ARCH=$GOARCH if [ "$PLATFORM" == "linux_arm" ]; then TARGET_ARCH="armv7l" fi ARCHIVE_NAME="${RELEASE_BASENAME}-${GOOS}-${TARGET_ARCH}.tar.gz" echo "Creating archive: ${ARCHIVE_NAME}" tar -czvf "./build/${ARCHIVE_NAME}" ./config -C "./build/${PLATFORM}/" . done - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: build_file path: ./build/ - name: Upload Config Artifacts uses: actions/upload-artifact@v4 with: name: config path: ./config - name: Upload Release Archives uses: actions/upload-artifact@v4 with: name: release_archives path: | ./build/${{ env.NAME }}-${{ needs.check-version.outputs.release_tag }}-*.tar.gz ./build/changelog.txt push-docker: needs: [check-version, build-binaries, create-release] if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: build_file path: ./build/ - name: Download Config Artifacts uses: actions/download-artifact@v4 with: name: config path: ./config - name: Set Environment Variables run: | # NAME is already set globally # Use the tag from check-version TAG_VERSION=${{ needs.check-version.outputs.release_tag }} echo "TAG_VERSION=${TAG_VERSION}" >> ${GITHUB_ENV} echo "IMAGE_TAG=${TAG_VERSION}" >> ${GITHUB_ENV} echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> ${GITHUB_ENV} echo "GIT_COMMIT=$(git rev-parse --short HEAD)" >> ${GITHUB_ENV} - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - name: Docker Build & Publish to GitHub Container Registry uses: elgohr/Publish-Docker-Github-Action@v5 with: dockerfile: docker/Dockerfile name: ${{ github.actor }}/${{ env.NAME }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} platforms: linux/amd64,linux/arm64,linux/arm/v7 registry: ghcr.io snapshot: false tags: "${{ env.IMAGE_TAG }}" buildargs: | VERSION=${{ env.TAG_VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} GIT_COMMIT=${{ env.GIT_COMMIT }} - name: Docker Build & Publish to DockerHub uses: elgohr/Publish-Docker-Github-Action@v5 with: dockerfile: docker/Dockerfile name: ${{ github.actor }}/${{ env.NAME }} username: ${{ github.actor }} password: ${{ secrets.DOCKERHUB_TOKEN }} platforms: linux/amd64,linux/arm64,linux/arm/v7 snapshot: false tags: "${{ env.IMAGE_TAG }}" buildargs: | VERSION=${{ env.TAG_VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} GIT_COMMIT=${{ env.GIT_COMMIT }} - name: Docker Build & Publish to CNB uses: elgohr/Publish-Docker-Github-Action@v5 with: dockerfile: docker/Dockerfile name: haierkeys/${{ env.NAME }} username: ${{ secrets.CNB_USERNAME }} password: ${{ secrets.CNB_TOKEN }} platforms: linux/amd64,linux/arm64,linux/arm/v7 registry: docker.cnb.cool snapshot: false tags: "${{ env.IMAGE_TAG }}" buildargs: | VERSION=${{ env.TAG_VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} GIT_COMMIT=${{ env.GIT_COMMIT }} push-release-files: needs: [create-release, build-binaries, check-version] if: needs.check-version.outputs.should_release == 'true' runs-on: ubuntu-latest steps: - name: Download Release Archives uses: actions/download-artifact@v4 with: name: release_archives path: ./archives/ - name: Upload Archives to GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.check-version.outputs.release_tag }}-alpha files: ./archives/* push-cnb-release: needs: [check-version, prepare-message, build-binaries, create-release] if: needs.check-version.outputs.should_release == 'true' name: Push CNB Release runs-on: ubuntu-latest env: CNB_REPO: haierkeys/fast-note-sync-service steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Push Tag to CNB env: CNB_TOKEN: ${{ secrets.CNB_TOKEN }} RELEASE_TAG: ${{ needs.check-version.outputs.release_tag }}-alpha run: | # 确保拉取了最新的 tag 信息 git fetch --tags git remote add cnb "https://cnb:${CNB_TOKEN}@cnb.cool/${CNB_REPO}.git" # 直接推送已存在的 tag git push cnb "${RELEASE_TAG}" --force - name: Download Release Archives uses: actions/download-artifact@v4 with: name: release_archives path: ./archives/ - name: Create CNB Release and Upload Assets env: CNB_TOKEN: ${{ secrets.CNB_TOKEN }} RELEASE_TAG: ${{ needs.check-version.outputs.release_tag }}-alpha RELEASE_BODY: ${{ needs.prepare-message.outputs.commit_msg }} run: | set -e # ============================================================ # 第一步: 创建 CNB Release # Step 1: Create CNB Release # ============================================================ echo "::group::Creating CNB Release: ${RELEASE_TAG}" RELEASE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n \ --arg tag "${RELEASE_TAG}" \ --arg name "${RELEASE_TAG}" \ --arg body "${RELEASE_BODY}" \ '{tag_name: $tag, name: $name, body: $body, prerelease: true, draft: false}')" \ "https://api.cnb.cool/${CNB_REPO}/-/releases") HTTP_CODE=$(echo "$RELEASE_RESPONSE" | tail -1) RESPONSE_BODY=$(echo "$RELEASE_RESPONSE" | sed '$d') if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then RELEASE_ID=$(echo "$RESPONSE_BODY" | jq -r '.id') echo "CNB Release created successfully, ID: ${RELEASE_ID}" else echo "::warning::Failed to create CNB Release (HTTP ${HTTP_CODE}), attempting to fetch existing release..." # 尝试获取已存在的 Release / Try to get existing release LIST_RESPONSE=$(curl -s \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ "https://api.cnb.cool/${CNB_REPO}/-/releases") RELEASE_ID=$(echo "$LIST_RESPONSE" | jq -r ".[] | select(.tag_name==\"${RELEASE_TAG}\") | .id") if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then echo "::error::Cannot create or find CNB Release for tag ${RELEASE_TAG}" exit 1 fi echo "Found existing CNB Release, ID: ${RELEASE_ID}" fi echo "::endgroup::" # ============================================================ # 第二步: 并行上传所有已打包的构建产物 # Step 2: Parallel upload all pre-built release archives # ============================================================ for FILE_PATH in ./archives/*; do ( FILE_NAME=$(basename "$FILE_PATH") echo "Uploading ${FILE_NAME}..." FILE_SIZE=$(stat -c%s "$FILE_PATH") # 2a. 获取上传 URL / Get upload URL UPLOAD_RESPONSE=$(curl -s -X POST \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n \ --arg name "${FILE_NAME}" \ --argjson size ${FILE_SIZE} \ '{asset_name: $name, size: $size, overwrite: true}')" \ "https://api.cnb.cool/${CNB_REPO}/-/releases/${RELEASE_ID}/asset-upload-url") UPLOAD_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_url') VERIFY_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.verify_url') if [ -z "$UPLOAD_URL" ] || [ "$UPLOAD_URL" = "null" ]; then echo "::error::Failed to get upload URL for ${FILE_NAME}" echo "Response: ${UPLOAD_RESPONSE}" exit 1 fi # 2b. 上传文件内容 / Upload file content echo "Uploading ${FILE_NAME} to: ${UPLOAD_URL}" CONTENT_TYPE="application/gzip" if [[ "${FILE_NAME}" == *.txt ]]; then CONTENT_TYPE="text/plain" fi UPLOAD_RESULT=$(curl -s -w "\n%{http_code}" -X PUT \ -H "Content-Type: ${CONTENT_TYPE}" \ --data-binary @"$FILE_PATH" \ "$UPLOAD_URL") UPLOAD_HTTP_CODE=$(echo "$UPLOAD_RESULT" | tail -1) if [ "$UPLOAD_HTTP_CODE" -ne 200 ] && [ "$UPLOAD_HTTP_CODE" -ne 201 ]; then echo "::error::Failed to upload ${FILE_NAME} (HTTP ${UPLOAD_HTTP_CODE})" exit 1 fi # 2c. 确认上传完成 / Confirm upload echo "Confirming upload for ${FILE_NAME}..." CONFIRM_RESULT=$(curl -s -w "\n%{http_code}" -X POST \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ "${VERIFY_URL}") CONFIRM_HTTP_CODE=$(echo "$CONFIRM_RESULT" | tail -1) if [ "$CONFIRM_HTTP_CODE" -ne 200 ] && [ "$CONFIRM_HTTP_CODE" -ne 201 ]; then echo "::error::Failed to confirm upload for ${FILE_NAME} (HTTP ${CONFIRM_HTTP_CODE})" exit 1 fi echo "✅ ${FILE_NAME} uploaded successfully" ) & done # 等待所有后台任务完成 wait echo "🎉 All assets uploaded to CNB Release!" ================================================ FILE: .github/workflows/mirror-to-cnb.yml ================================================ # Mirror repository to CNB.cool # 将当前仓库的所有分支和标签同步到 cnb.cool 远程仓库 name: Mirror to CNB on: push: branches: - '**' # 所有分支的推送都会触发 tags: - '**' # 所有标签的推送都会触发 delete: # 分支或标签删除时也同步 jobs: mirror: name: Mirror to CNB.cool if: github.actor == 'haierkeys' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 # 获取完整的 git 历史记录,包括所有分支和标签 - name: Push to CNB mirror env: CNB_USERNAME: ${{ secrets.CNB_USERNAME }} CNB_TOKEN: ${{ secrets.CNB_TOKEN }} run: | # 设置 CNB 远程仓库地址(使用认证信息) # Set CNB remote URL with authentication git remote add cnb "https://${CNB_USERNAME}:${CNB_TOKEN}@cnb.cool/haierkeys/fast-note-sync-service.git" # 推送所有分支和标签到 CNB 远程仓库 # Push all branches and tags to CNB mirror git push cnb --all --force git push cnb --tags --force ================================================ FILE: .github/workflows/release.yml ================================================ name: Go-Release # 触发条件配置 on: push: # 针对 master 分支的普通提交也触发 branches: - "master" paths: - "internal/app/version.go" # 权限配置:允许脚本写入仓库内容(用于发布 Release) permissions: contents: write packages: write env: NAME: ${{ github.event.repository.name }} jobs: check-version: if: github.actor == 'haierkeys' runs-on: ubuntu-latest outputs: should_release: ${{ steps.check.outputs.should_release }} release_tag: ${{ steps.check.outputs.release_tag }} steps: - uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Check Version id: check run: | # 从 internal/app/version.go (HEAD) 读取当前版本 # 预期格式: var Version string = "0.0.1" CURRENT_VERSION=$(grep -E 'Version\s+string' internal/app/version.go | awk -F '"' '{print $2}') echo "当前版本 (HEAD): $CURRENT_VERSION" # 从 internal/app/version.go (HEAD~1) 获取上一个版本 # 使用 git show 获取上一次提交的文件内容 # 注意: 我们将其包裹在一个块中以处理 HEAD~1 可能不存在的情况 (例如: 初始提交) if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then PREV_FILE_CONTENT=$(git show HEAD~1:internal/app/version.go) PREV_VERSION=$(echo "$PREV_FILE_CONTENT" | grep -E 'Version\s+string' | awk -F '"' '{print $2}') else PREV_VERSION="0.0.0" fi # 如果 PREV_VERSION 为空 (例如: grep 失败), 默认为 0.0.0 if [ -z "$PREV_VERSION" ]; then PREV_VERSION="0.0.0" fi echo "上一个版本 (HEAD~1): $PREV_VERSION" # 规范化版本以进行比较 (如果存在 'v' 前缀则移除) V_LAST=${PREV_VERSION#v} V_CURR=${CURRENT_VERSION#v} # 比较版本 if [ "$V_CURR" != "$V_LAST" ]; then # 使用 sort -V 确定当前版本是否严格大于旧版本 NEWER_VERSION=$(echo -e "$V_LAST\n$V_CURR" | sort -V | tail -n 1) if [ "$NEWER_VERSION" == "$V_CURR" ] && [ "$NEWER_VERSION" != "$V_LAST" ]; then echo "检测到新版本: $V_CURR > $V_LAST" echo "should_release=true" >> $GITHUB_OUTPUT echo "release_tag=$V_CURR" >> $GITHUB_OUTPUT else echo "版本 $V_CURR 不大于 $V_LAST。跳过发布..." echo "should_release=false" >> $GITHUB_OUTPUT fi else echo "版本匹配 ($V_CURR == $V_LAST)。跳过发布..." echo "should_release=false" >> $GITHUB_OUTPUT fi prepare-message: needs: check-version if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' runs-on: ubuntu-latest outputs: commit_msg: ${{ steps.trans.outputs.commit_msg }} steps: - uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Prepare Commit Message id: trans run: | # 获取提交文本信息 msg=$(git log -1 --pretty=%B) # 设置 Python 进行翻译 pip install deep-translator > /dev/null 2>&1 || true # 运行翻译脚本 export COMMIT_MSG="$msg" echo "commit_msg<> $GITHUB_OUTPUT if [ -f "scripts/translate_commit.py" ]; then python3 scripts/translate_commit.py >> $GITHUB_OUTPUT else echo "未找到翻译脚本,使用原始消息" echo "$msg" >> $GITHUB_OUTPUT fi echo "EOF" >> $GITHUB_OUTPUT create-release: needs: [check-version, prepare-message] if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' name: Create Release runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Create Release id: create_release uses: softprops/action-gh-release@v2 env: token: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ needs.check-version.outputs.release_tag }} name: ${{ needs.check-version.outputs.release_tag }} draft: false prerelease: false generate_release_notes: true body: ${{ needs.prepare-message.outputs.commit_msg }} target_commitish: ${{ github.sha }} # 明确指定在当前分支的 commit 上创建 tag overwrite_files: true build-binaries: needs: [check-version, prepare-message, create-release] if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - name: Check Go Version run: go version - name: Go Build Prepare run: go install github.com/mitchellh/gox@latest - name: Go Build Multi-platform env: COMMIT_MSG: ${{ needs.prepare-message.outputs.commit_msg }} run: make gox-all - name: Create Changelog env: COMMIT_MSG: ${{ needs.prepare-message.outputs.commit_msg }} run: | echo "${COMMIT_MSG}" > ./build/changelog.txt - name: Create GZip Archives for All Platforms env: RELEASE_BASENAME: ${{ env.NAME }}-${{ needs.check-version.outputs.release_tag }} run: | # 为所有平台创建 tar.gz 包 # Create tar.gz packages for all platforms declare -a PLATFORMS=("darwin_amd64" "darwin_arm64" "linux_amd64" "linux_arm64" "linux_arm" "windows_amd64") for PLATFORM in "${PLATFORMS[@]}"; do GOOS=$(echo "$PLATFORM" | cut -d'_' -f1) GOARCH=$(echo "$PLATFORM" | cut -d'_' -f2) # 这里的重命名逻辑:如果是 linux_arm,则将后缀改为 armv7l # Rename linux_arm to linux-armv7l for easier identification TARGET_ARCH=$GOARCH if [ "$PLATFORM" == "linux_arm" ]; then TARGET_ARCH="armv7l" fi ARCHIVE_NAME="${RELEASE_BASENAME}-${GOOS}-${TARGET_ARCH}.tar.gz" echo "Creating archive: ${ARCHIVE_NAME}" tar -czvf "./build/${ARCHIVE_NAME}" ./config -C "./build/${PLATFORM}/" . done - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: build_file path: ./build/ - name: Upload Config Artifacts uses: actions/upload-artifact@v4 with: name: config path: ./config - name: Upload Release Archives uses: actions/upload-artifact@v4 with: name: release_archives path: | ./build/${{ env.NAME }}-${{ needs.check-version.outputs.release_tag }}-*.tar.gz ./build/changelog.txt push-docker: needs: [check-version, build-binaries,create-release] if: needs.check-version.outputs.should_release == 'true' && github.actor == 'haierkeys' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: build_file path: ./build/ - name: Download Config Artifacts uses: actions/download-artifact@v4 with: name: config path: ./config - name: Set Environment Variables run: | # NAME is already set globally # Use the tag from check-version TAG_VERSION=${{ needs.check-version.outputs.release_tag }} echo "TAG_VERSION=${TAG_VERSION}" >> ${GITHUB_ENV} echo "IMAGE_TAG=${TAG_VERSION}" >> ${GITHUB_ENV} echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> ${GITHUB_ENV} echo "GIT_COMMIT=$(git rev-parse --short HEAD)" >> ${GITHUB_ENV} - name: Append 'latest' tag if on main branch if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' run: echo "IMAGE_TAG=${{ env.IMAGE_TAG }},latest" >> ${GITHUB_ENV} - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - name: Docker Build & Publish to GitHub Container Registry uses: elgohr/Publish-Docker-Github-Action@v5 with: dockerfile: docker/Dockerfile name: ${{ github.actor }}/${{ env.NAME }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} platforms: linux/amd64,linux/arm64,linux/arm/v7 registry: ghcr.io snapshot: false tags: "${{ env.IMAGE_TAG }}" buildargs: | VERSION=${{ env.TAG_VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} GIT_COMMIT=${{ env.GIT_COMMIT }} - name: Docker Build & Publish to DockerHub uses: elgohr/Publish-Docker-Github-Action@v5 with: dockerfile: docker/Dockerfile name: ${{ github.actor }}/${{ env.NAME }} username: ${{ github.actor }} password: ${{ secrets.DOCKERHUB_TOKEN }} platforms: linux/amd64,linux/arm64,linux/arm/v7 snapshot: false tags: "${{ env.IMAGE_TAG }}" buildargs: | VERSION=${{ env.TAG_VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} GIT_COMMIT=${{ env.GIT_COMMIT }} - name: Docker Build & Publish to CNB uses: elgohr/Publish-Docker-Github-Action@v5 with: dockerfile: docker/Dockerfile name: haierkeys/${{ env.NAME }} username: ${{ secrets.CNB_USERNAME }} password: ${{ secrets.CNB_TOKEN }} platforms: linux/amd64,linux/arm64,linux/arm/v7 registry: docker.cnb.cool snapshot: false tags: "${{ env.IMAGE_TAG }}" buildargs: | VERSION=${{ env.TAG_VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} GIT_COMMIT=${{ env.GIT_COMMIT }} push-release-files: needs: [create-release, build-binaries, check-version] if: needs.check-version.outputs.should_release == 'true' runs-on: ubuntu-latest steps: - name: Download Release Archives uses: actions/download-artifact@v4 with: name: release_archives path: ./archives/ - name: Upload Archives to GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.check-version.outputs.release_tag }} files: ./archives/* push-cnb-release: needs: [check-version, prepare-message, build-binaries, create-release] if: needs.check-version.outputs.should_release == 'true' name: Push CNB Release runs-on: ubuntu-latest env: CNB_REPO: haierkeys/fast-note-sync-service steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Push Tag to CNB env: CNB_TOKEN: ${{ secrets.CNB_TOKEN }} RELEASE_TAG: ${{ needs.check-version.outputs.release_tag }} run: | # 确保拉取了最新的 tag 信息 git fetch --tags git remote add cnb "https://cnb:${CNB_TOKEN}@cnb.cool/${CNB_REPO}.git" # 直接推送已存在的 tag git push cnb "${RELEASE_TAG}" --force - name: Download Release Archives uses: actions/download-artifact@v4 with: name: release_archives path: ./archives/ - name: Create CNB Release and Upload Assets env: CNB_TOKEN: ${{ secrets.CNB_TOKEN }} RELEASE_TAG: ${{ needs.check-version.outputs.release_tag }} RELEASE_BODY: ${{ needs.prepare-message.outputs.commit_msg }} run: | set -e # ============================================================ # 第一步: 创建 CNB Release # Step 1: Create CNB Release # ============================================================ echo "::group::Creating CNB Release: ${RELEASE_TAG}" RELEASE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n \ --arg tag "${RELEASE_TAG}" \ --arg name "${RELEASE_TAG}" \ --arg body "${RELEASE_BODY}" \ '{tag_name: $tag, name: $name, body: $body, prerelease: false, draft: false}')" \ "https://api.cnb.cool/${CNB_REPO}/-/releases") HTTP_CODE=$(echo "$RELEASE_RESPONSE" | tail -1) RESPONSE_BODY=$(echo "$RELEASE_RESPONSE" | sed '$d') if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then RELEASE_ID=$(echo "$RESPONSE_BODY" | jq -r '.id') echo "CNB Release created successfully, ID: ${RELEASE_ID}" else echo "::warning::Failed to create CNB Release (HTTP ${HTTP_CODE}), attempting to fetch existing release..." # 尝试获取已存在的 Release / Try to get existing release LIST_RESPONSE=$(curl -s \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ "https://api.cnb.cool/${CNB_REPO}/-/releases") RELEASE_ID=$(echo "$LIST_RESPONSE" | jq -r ".[] | select(.tag_name==\"${RELEASE_TAG}\") | .id") if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then echo "::error::Cannot create or find CNB Release for tag ${RELEASE_TAG}" exit 1 fi echo "Found existing CNB Release, ID: ${RELEASE_ID}" fi echo "::endgroup::" # ============================================================ # 第二步: 并行上传所有已打包的构建产物 # Step 2: Parallel upload all pre-built release archives # ============================================================ for FILE_PATH in ./archives/*; do ( FILE_NAME=$(basename "$FILE_PATH") echo "Uploading ${FILE_NAME}..." FILE_SIZE=$(stat -c%s "$FILE_PATH") # 2a. 获取上传 URL / Get upload URL UPLOAD_RESPONSE=$(curl -s -X POST \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n \ --arg name "${FILE_NAME}" \ --argjson size ${FILE_SIZE} \ '{asset_name: $name, size: $size, overwrite: true}')" \ "https://api.cnb.cool/${CNB_REPO}/-/releases/${RELEASE_ID}/asset-upload-url") UPLOAD_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.upload_url') VERIFY_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.verify_url') if [ -z "$UPLOAD_URL" ] || [ "$UPLOAD_URL" = "null" ]; then echo "::error::Failed to get upload URL for ${FILE_NAME}" echo "Response: ${UPLOAD_RESPONSE}" exit 1 fi # 2b. 上传文件内容 / Upload file content echo "Uploading ${FILE_NAME} to: ${UPLOAD_URL}" CONTENT_TYPE="application/gzip" if [[ "${FILE_NAME}" == *.txt ]]; then CONTENT_TYPE="text/plain" fi UPLOAD_RESULT=$(curl -s -w "\n%{http_code}" -X PUT \ -H "Content-Type: ${CONTENT_TYPE}" \ --data-binary @"$FILE_PATH" \ "$UPLOAD_URL") UPLOAD_HTTP_CODE=$(echo "$UPLOAD_RESULT" | tail -1) if [ "$UPLOAD_HTTP_CODE" -ne 200 ] && [ "$UPLOAD_HTTP_CODE" -ne 201 ]; then echo "::error::Failed to upload ${FILE_NAME} (HTTP ${UPLOAD_HTTP_CODE})" exit 1 fi # 2c. 确认上传完成 / Confirm upload echo "Confirming upload for ${FILE_NAME}..." CONFIRM_RESULT=$(curl -s -w "\n%{http_code}" -X POST \ -H "Accept: application/vnd.cnb.api+json" \ -H "Authorization: Bearer ${CNB_TOKEN}" \ "${VERIFY_URL}") CONFIRM_HTTP_CODE=$(echo "$CONFIRM_RESULT" | tail -1) if [ "$CONFIRM_HTTP_CODE" -ne 200 ] && [ "$CONFIRM_HTTP_CODE" -ne 201 ]; then echo "::error::Failed to confirm upload for ${FILE_NAME} (HTTP ${CONFIRM_HTTP_CODE})" exit 1 fi echo "✅ ${FILE_NAME} uploaded successfully" ) & done # 等待所有后台任务完成 wait echo "🎉 All assets uploaded to CNB Release!" ================================================ FILE: .gitignore ================================================ # -- Composer ----------------------------------------- /composer.phar /composer.lock # -- Editores ----------------------------------------- # vim .*.sw[a-z] *.un~ Session.vim .netrwhist # vscode /.vscode .vscode # eclipse *.pydevproject .project .metadata tmp/** tmp/**/* *.tmp *.bak *.swp *~.nib local.properties .classpath .settings/ .loadpath .externalToolBuilders/ *.launch .buildpath # phpstorm .idea # textmate *.tmproj *.tmproject tmtags # sublimetext /*.sublime-project *.sublime-workspace # netbeans nbproject/private/ build/ nbbuild/ dist/ nbdist/ nbactions.xml nb-configuration.xml # -- Sistemas Operativos ------------------------------ # Windows Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ # Linux !.gitignore !.htaccess !.env *~ # Mac OS X .DS_Store .AppleDouble .LSOverride Icon ._* .Spotlight-V100 .Trashes # -- Project ----------------------------------------- /build /storage/logs /storage/uploads /storage/ /config/config-dev.yaml /config/lastVersion docs/*_打赏 链接小票单记录*.csv # Claude Code CLAUDE.md .claude/ # Python virtual environment .venv/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2024-2026 HaierKeys Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # Docker 登录示例 # docker login --username=xxxxxx registry.cn-shanghai.aliyuncs.com # 环境变量(可选) # include .env # export $(shell sed 's/=.*//' .env) # ------------------------- # 项目 / 镜像 配置 # ------------------------- REPO = $(eval REPO := $$(shell go list -f '{{.ImportPath}}' .))$(value REPO) DockerHubUser = haierkeys DockerHubName = fast-note-sync-service ReleaseTagPre = release-v DevelopTagPre = develop-v P_NAME = fast-note-sync P_BIN = fast-note-sync-service # ------------------------- # Git / 构建信息 # ------------------------- GitTag = $(shell git describe --tags --abbrev=0) GitVersion = $(shell git log -1 --format=%h) GitVersionDesc = $(shell git log -1 --format=%s) BuildTime = $(shell date +%FT%T%z) # LDFLAGS: 注入版本信息到二进制 # 获取提交信息,并处理掉换行符(用 @@@ 占位)以便安全地注入到 ldflags COMMIT_MSG_CLEAN = $(shell echo "$(COMMIT_MSG)" | tr '\n' '^' | sed 's/\^/@@@/g' | sed 's/"/\\"/g') Changelog ?= $(COMMIT_MSG_CLEAN) LDFLAGS = -ldflags '-X ${REPO}/internal/app.Version=$(GitTag) -X "${REPO}/internal/app.GitTag=$(GitVersion)" -X ${REPO}/internal/app.BuildTime=$(BuildTime) -X "${REPO}/internal/app.Changelog=$(Changelog)"' # go 命令封装 gob = go build ${LDFLAGS} gor = go run ${LDFLAGS} # 编译相关 CGO = CGO_ENABLED=0 rootDir = $(shell pwd) buildDir = $(rootDir)/build # ------------------------- # PHONY 目标 # ------------------------- .PHONY: all build-all run test clean \ push-online push-dev \ build-macos-amd64 build-macos-arm64 build-linux-amd64 \ build-linux-arm64 build-linux-arm build-windows-amd64 gox-linux gox-all \ docs fmt update air dev ver gen sup # 默认目标 all: test build-all # ------------------------- # 简单目标 # ------------------------- sup: node scripts/process_support_csv.js @if [ ! -d ".venv" ]; then python3 -m venv .venv; fi .venv/bin/pip install -q deep-translator .venv/bin/python scripts/process_support.py node scripts/gen_support_md.js sup-md: node scripts/gen_support_md.js test: go test $$(go list ./... | grep -v -E 'internal/service/mocks|internal/domain/mocks|internal/dto|internal/model|internal/query|internal/config|internal/app|/docs|internal/middleware|cmd') dev: air -c ./scripts/.air.toml air: air -c ./scripts/.air.toml fmt: go fmt ./... update: go get -u ./... # 更新版本脚本调用 ver: @node ./scripts/update-version.js $(filter-out $@,$(MAKECMDGOALS)) # 捕获 ver 后面的参数,防止 make 将其视为目标 %: @: gen: go run -v ./cmd/gorm_gen/gen.go -type sqlite -dsn storage/database/db.sqlite3 go run -v ./cmd/model_gen/gen.go docs: go run github.com/swaggo/swag/cmd/swag@latest init -g main.go -o ./docs --parseDependency --parseInternal # 运行 run: # $(call checkStatic) $(call init) $(gor) -v $(rootDir) clean: rm -rf $(buildDir) # ------------------------- # 构建集合 # ------------------------- build-all: # $(call checkStatic) $(MAKE) build-macos-amd64 $(MAKE) build-macos-arm64 $(MAKE) build-linux-amd64 $(MAKE) build-linux-arm64 $(MAKE) build-linux-arm $(MAKE) build-windows-amd64 # macOS build-macos-amd64: $(CGO) GOOS=darwin GOARCH=amd64 $(gob) -o $(buildDir)/darwin_amd64/${P_BIN} $(bin) -v $(rootDir) build-macos-arm64: $(CGO) GOOS=darwin GOARCH=arm64 $(gob) -o $(buildDir)/darwin_arm64/${P_BIN} -v $(rootDir) # Linux build-linux-amd64: # CGO_ENABLED=1 CC=musl-gcc GOOS=linux GOARCH=amd64 $(gob) -o $(buildDir)/linux_amd64/${P_BIN} -v $(rootDir) $(CGO) GOOS=linux GOARCH=amd64 $(gob) -o $(buildDir)/linux_amd64/${P_BIN} -v $(rootDir) build-linux-arm64: $(CGO) GOOS=linux GOARCH=arm64 $(gob) -o $(buildDir)/linux_arm64/${P_BIN} -v $(rootDir) build-linux-arm: $(CGO) GOOS=linux GOARCH=arm GOARM=7 $(gob) -o $(buildDir)/linux_arm/${P_BIN} -v $(rootDir) # Windows build-windows-amd64: # CGO_ENABLED=0 CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC="x86_64-w64-mingw32-gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -lssp" $(gob) -o $(bin).exe -v $(rootDir) $(CGO) GOOS=windows GOARCH=amd64 $(gob) -o $(buildDir)/windows_amd64/${P_BIN}.exe -v $(rootDir) # gox 辅助 gox-linux: $(CGO) GOARM=7 gox ${LDFLAGS} -osarch="linux/amd64 linux/arm64 linux/arm" -output="$(buildDir)/{{.OS}}_{{.Arch}}/${P_BIN}" gox-all: $(CGO) GOARM=7 gox ${LDFLAGS} -osarch="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 linux/arm windows/amd64" -output="$(buildDir)/{{.OS}}_{{.Arch}}/${P_BIN}" # ------------------------- # Docker 发布 # ------------------------- push-online: build-linux $(call dockerImageClean) docker build --platform linux/amd64 -t $(DockerHubUser)/$(DockerHubName):latest -f docker/Dockerfile . docker tag $(DockerHubUser)/$(DockerHubName):latest $(DockerHubUser)/$(DockerHubName):$(ReleaseTagPre)$(GitTag) docker push $(DockerHubUser)/$(DockerHubName):$(ReleaseTagPre)$(GitTag) docker push $(DockerHubUser)/$(DockerHubName):latest push-dev: build-linux $(call dockerImageClean) docker build --platform linux/amd64 -t $(DockerHubUser)/$(DockerHubName):dev-latest -f docker/Dockerfile . docker tag $(DockerHubUser)/$(DockerHubName):dev-latest $(DockerHubUser)/$(DockerHubName):$(DevelopTagPre)$(GitTag) docker push $(DockerHubUser)/$(DockerHubName):$(DevelopTagPre)$(GitTag) docker push $(DockerHubUser)/$(DockerHubName):dev-latest # ------------------------- # 代码片段(定义) # ------------------------- define dockerImageClean @echo "docker Image Clean" bash docker_image_clean.sh endef define init @echo "Build Init" endef ================================================ FILE: README.md ================================================ [简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github.com/haierkeys/fast-note-sync-service/blob/master/README.md) / [日本語](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ja.md) / [한국어](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ko.md) / [繁體中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-TW.md) For any questions, please create a new [issue](https://github.com/haierkeys/fast-note-sync-service/issues/new), or join the Telegram chat group for help: [https://t.me/obsidian_users](https://t.me/obsidian_users) For mainland China, the Tencent `cnb.cool` mirror repository is recommended: [https://cnb.cool/haierkeys/fast-note-sync-service](https://cnb.cool/haierkeys/fast-note-sync-service)

Fast Note Sync Service

release alpha-release license Go

High-performance, low-latency note syncing, online management, and remote REST API service platform
Built with Golang + Websocket + React

Data provision requires the use of the client plugin: Obsidian Fast Note Sync Plugin

fast-note-sync-service-preview fast-note-sync-service-preview
fast-note-sync-service-preview fast-note-sync-service-preview
--- ## 🎯 Core Features * **🧰 Native MCP (Model Context Protocol) Support**: * `FNS` can act as an MCP server connecting to `Cherry Studio`, `Cursor`, and other compatible AI clients. This grants AI the ability to read and write your private notes and attachments, with all changes syncing to all clients in real time. * **🚀 REST API Support**: * Provides standard REST API interfaces, supporting automated program access (e.g., automation scripts, AI assistant integration) for CRUD operations on Obsidian notes. * For more details, please refer to the [RESTful API Documentation](/docs/REST_API.md) or [OpenAPI Documentation](/docs/swagger.yaml). * **💻 Web Management Panel**: * Built-in modern management interface to easily create users, generate plugin configurations, and manage vaults and note contents. * **🔄 Multi-device Note Syncing**: * Supports automatic **Vault** creation. * Supports note management (Add, Delete, Modify, Read) with millisecond-level real-time distribution of changes to all online devices. * **🖼️ Attachment Syncing Support**: * Perfect support for syncing non-note files like images. * Supports chunked upload and download of large attachments, with configurable chunk sizes, improving syncing efficiency. * **⚙️ Configuration Syncing**: * Supports syncing `.obsidian` configuration files. * Supports syncing `PDF` progress states. * **📝 Note History**: * Ability to view historical modification versions of each note via the Web page and plugin client. * (Requires Server v1.2+) * **🗑️ Recycle Bin**: * Supports automatic transfer of notes to the recycle bin upon deletion. * Supports recovering notes from the recycle bin. (Attachment recovery features will be added progressively). * **🚫 Offline Sync Strategy**: * Supports automatic merging of offline note edits. (Requires setup on the plugin client). * Offline deletion automatically synchronizes with server padding or deletions after reconnection. (Requires setup on the plugin client). * **🔗 Share Feature**: * Ability to Create/Cancel note sharing. * Automatically parses attachments such as images, audio, and video referenced in shared notes. * Provides sharing access statistics. * Ability to set a password for shared notes. * Ability to generate short links for shared notes. * **📂 Directory Syncing**: * Supports Create/Rename/Move/Delete syncing for folders. * **🌳 Git Automation**: * Automatically updates and pushes to the remote Git repository when attachments or notes undergo changes. * Automatically releases system memory after the task strictly finishes. * **☁️ Multi-Storage Backup & One-way Mirror Syncing**: * Adapts to S3/OSS/R2/WebDAV/Local and other storage protocols. * Supports full/incremental ZIP scheduled archive backups. * Supports one-way mirror syncing of Vault resources to remote storage. * Automatically cleans up expired backups, with support for custom retention days. * **🗄️ Multi-Database Support**: * Natively supports mainstream databases such as SQLite, MySQL, PostgreSQL, meeting deployment needs ranging from individuals to teams. ## ☕ Sponsorship & Support - If you find this plugin useful and want its development to continue, please support me via the following channels: | Ko-fi *Non-China Region* | | WeChat QR Donation *China Region* | |--------------------------------------------------------------------------------------------------|----|------------------------------------------------| | [BuyMeACoffee](https://ko-fi.com/haierkeys) | OR | | - Supported List: - Support.en.md - Support.en.md (cnb.cool Mirror) ## ⏱️ Changelog - ♨️ [View Changelog](/docs/CHANGELOG.en.md) ## 🗺️ Roadmap - [ ] Add **Mock** testing covering all levels. - [ ] Add WebSocket `Protobuf` transmission format support, enhancing synchronization efficiency. - [ ] The backend to include queries for various operational logs such as sync logs and operation logs. - [ ] Isolate and optimize the current authorization mechanism to elevate overall security. - [ ] Add WebGui note real-time update capability. - [ ] Add client Peer-to-Peer message transmission (non-note & attachments, similar to localsend; not saved closely on the client, saves to the server). - [ ] Enhance various help documents. - [ ] Support more Intranet Penetration (Relay gateway). - [ ] Quick deployment plan: * Deploy FNS Server securely with just the server's public IP address and account credentials. - [ ] Optimize the current offline note merging scheme and introduce conflict-handling mechanisms. We are continually improving. Here are our future development plans: > **If you have improvement suggestions or new ideas, please submit an issue to share them with us. We will sincerely evaluate and adopt appropriate suggestions.** ## 🚀 Quick Deployment We offer multiple installation methods. We recommend utilizing the **One-click Script** or **Docker**. ### Method 1: One-click Script (Recommended) Automatically detects the system environment, completes the installation, and registers the service. ```bash bash <(curl -fsSL https://raw.githubusercontent.com/haierkeys/fast-note-sync-service/master/scripts/quest_install.sh) ``` Users in China can utilize the Tencent `cnb.cool` mirror source: ```bash bash <(curl -fsSL https://cnb.cool/haierkeys/fast-note-sync-service/-/git/raw/master/scripts/quest_install.sh) --cnb ``` **Main Script Actions:** * Automatically downloads the optimal Release binary file for your system. * Default installation path is `/opt/fast-note`, creating a global quick command abstractly named `fns` in `/usr/local/bin/fns`. * Configures and launches Systemd (Linux) or Launchd (macOS) services to realize auto-start on boot. * **Management Commands**: `fns [install|uninstall|start|stop|status|update|menu]` * **Interactive Menu**: Run `fns` directly to enter an interactive menu enabling installation/upgrade, service control, auto-start configuration, and switching between GitHub / CNB mirrors. ----- ### Method 2: Docker Deployment #### Docker Run ```bash # 1. Pull the image docker pull haierkeys/fast-note-sync-service:latest # 2. Start the container docker run -tid --name fast-note-sync-service \ -p 9000:9000 \ -v /data/fast-note-sync/storage/:/fast-note-sync/storage/ \ -v /data/fast-note-sync/config/:/fast-note-sync/config/ \ haierkeys/fast-note-sync-service:latest ``` #### Docker Compose Create a `docker-compose.yaml` file: ```yaml version: '3' services: fast-note-sync-service: image: haierkeys/fast-note-sync-service:latest container_name: fast-note-sync-service restart: always ports: - "9000:9000" # RESTful API & WebSocket ports where /api/user/sync is the WebSocket interface address volumes: - ./storage:/fast-note-sync/storage # Data storage - ./config:/fast-note-sync/config # Configuration files ``` Start the service: ```bash docker compose up -d ``` ----- ### Method 3: Manual Binary Installation Download the latest version corresponding to your system from [Releases](https://github.com/haierkeys/fast-note-sync-service/releases), unzip, and run: ```bash ./fast-note-sync-service run -c config/config.yaml ``` ## 📖 Usage Guide 1. **Access the Management Panel**: Open `http://{Server IP}:9000` via your browser. 2. **Initial Setup**: Register an account on your first visit. *(To disable registration, configure `user.register-is-enable: false` in the settings configuration file)* 3. **Configure Client**: Log into the Management Panel, and click **"Copy API Configuration"**. 4. **Connect to Obsidian**: Navigate to the Obsidian plugin configuration page, and paste the previously copied configuration details. ## ⚙️ Configuration Instructions The default configuration file is `config.yaml`. The application will search for it automatically in the **Root Directory** or the **config/** directory. View a complete configuration example: [config/config.yaml](https://github.com/haierkeys/fast-note-sync-service/blob/master/config/config.yaml) ## 🌐 Nginx Reverse Proxy Configuration Example View a complete configuration example: [https-nginx-example.conf](https://github.com/haierkeys/fast-note-sync-service/blob/master/scripts/https-nginx-example.conf) ## 🧰 MCP (Model Context Protocol) Support FNS natively supports **MCP (Model Context Protocol)**. You can directly incorporate FNS as an MCP server with Cherry Studio, Cursor, and similar compatible AI clients. Once configured, AI attains the capacity to interpret and write within your own private notes and attachments. Furthermore, WebSocket synchronizes all MCP-directed alterations in real time across the entirety of your devices. ### Access Configuration (SSE Mode) FNS furnishes an MCP interface primarily through the **SSE protocol**. General parameter requirements are as follows: - **Interface Address**: `http://:/api/mcp/sse` - **Authentication Header**: `Authorization: Bearer ` (Obtained from “Copy API Configuration” in the WebGUI). - **Optional Header**: `X-Default-Vault-Name: ` (Identifies the default vault for MCP operations; utilized if the `vault` parameter is unspecified during a tool call) - **Optional Header**: `X-Client: ` (Relates to the client type connecting the MCP, e.g., Cherry Studio / OpenClaw) - **Optional Header**: `X-Client-Version: ` (Pertains to the client's actual version interfacing with the MCP, e.g., 1.1) - **Optional Header**: `X-Client-Name: ` (Designates the client's name linking with the MCP, e.g., Mac) #### Example: Cherry Studio / Cursor / Cline, etc. Kindly incorporate the below JSON inside your MCP client configuration parameters: *(Note: Please swap ``, ``, ``, and `` with your proper contextual details)* ```json { "mcpServers": { "fns": { "url": "http://:/api/mcp/sse", "type": "sse", "headers": { "Content-Type": "application/json", "Authorization": "Bearer ", "X-Default-Vault-Name": "", "X-Client": "", "X-Client-Version": "", "X-Client-Name": "" } } } } ``` ## 🔗 Client & Client Plugins * Obsidian Fast Note Sync Plugin * [Obsidian Fast Note Sync Plugin](https://github.com/haierkeys/obsidian-fast-note-sync) / [cnb.cool Mirror](https://cnb.cool/haierkeys/obsidian-fast-note-sync) * Third-Party Clients * [FastNodeSync-CLI ](https://github.com/Go1c/FastNodeSync-CLI) Python and FNS WS API grounded bidirectional real-time synchronization CLI client customized for headless Linux server infrastructures (like OpenClaw), providing analogous synchronization faculties equivalent to Obsidian's desktop/mobile clients. ================================================ FILE: cmd/bootstrap.go ================================================ package cmd import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // bootstrapLogger bootstrap stage logger // bootstrapLogger 启动阶段日志器 // Used to record logs during the startup process before the main logger is initialized // 用于在主日志器初始化之前记录启动过程中的日志 var bootstrapLogger *zap.Logger func init() { // Create encoder configuration for console output // 创建控制台输出的 encoder 配置 encoderConfig := zap.NewDevelopmentEncoderConfig() encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // Create console output // 创建控制台输出 consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) consoleWriter := zapcore.Lock(os.Stderr) // Set log level based on DEBUG environment variable // 根据 DEBUG 环境变量设置日志级别 level := zapcore.InfoLevel if os.Getenv("DEBUG") != "" { level = zapcore.DebugLevel } core := zapcore.NewCore(consoleEncoder, consoleWriter, level) bootstrapLogger = zap.New(core, zap.AddCaller()) } // BootstrapLogger gets the bootstrap stage logger // BootstrapLogger 获取启动阶段日志器 func BootstrapLogger() *zap.Logger { return bootstrapLogger } ================================================ FILE: cmd/gorm_gen/gen.go ================================================ package main // gorm gen configure import ( "flag" "fmt" "os" "strings" "github.com/haierkeys/fast-note-sync-service/pkg/fileurl" "gorm.io/driver/mysql" "gorm.io/driver/sqlite" "gorm.io/gen" "gorm.io/gen/field" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/schema" ) var ( dbType string dbDsn string step int ) func init() { dType := flag.String("type", "", "输入类型") dsn := flag.String("dsn", "", "输入DB dsn地址") dStep := flag.Int("step", 0, "输入执行步骤") flag.Parse() dbType = *dType dbDsn = *dsn step = *dStep } // SQLColumnToHumpStyle sql转换成驼峰模式 func SQLColumnToHumpStyle(in string) (ret string) { for i := 0; i < len(in); i++ { if i > 0 && in[i-1] == '_' && in[i] != '_' { s := strings.ToUpper(string(in[i])) ret += s } else if in[i] == '_' { continue } else { ret += string(in[i]) } } return } func Db(dsn string, dbType string) *gorm.DB { db, err := gorm.Open(useDia(dsn, dbType), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), NamingStrategy: schema.NamingStrategy{ SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` }, }) if err != nil { panic(fmt.Errorf("connect db fail: %w", err)) } return db } func useDia(dsn string, dbType string) gorm.Dialector { if dbType == "mysql" { return mysql.Open(dsn) } else if dbType == "sqlite" { if !fileurl.IsExist(dsn) { fileurl.CreatePath(dsn, os.ModePerm) } return sqlite.Open(dsn) } return nil } // getTableDefaultValueTags 获取指定表的 GORM tag 配置(自动注入默认值以解决 SQLite 迁移限制) func getTableDefaultValueTags(db *gorm.DB, table string) []gen.ModelOpt { var opts []gen.ModelOpt if table == "sqlite_sequence" || table == "schema_version" || strings.HasPrefix(table, "sqlite_") { return opts } // 获取表的所有列信息 columnTypes, err := db.Migrator().ColumnTypes(table) if err != nil { return opts } for _, col := range columnTypes { // 跳过主键字段 if isPrimaryKey(col) { continue } fieldName := col.Name() dbType := strings.ToLower(col.DatabaseTypeName()) defaultValue, ok := col.DefaultValue() // 获取 GORM tag 配置,并在这里注入额外逻辑 opts = append(opts, gen.FieldGORMTag(fieldName, func(tag field.GormTag) field.GormTag { // 1. 处理默认值逻辑 (保留原逻辑) if ok && defaultValue != "" { tag.Set("default", defaultValue) } else { if dbType == "integer" || dbType == "int" || dbType == "bigint" { tag.Set("default", "0") } else if dbType == "text" || strings.Contains(dbType, "char") { tag.Set("default", "''") } } // 2. 处理时间类型兼容性 (移除 type 让 GORM 自动决定) if strings.Contains(dbType, "datetime") || strings.Contains(dbType, "timestamp") { tag.Remove("type") } // 3. 处理整数类型兼容性 (移除 type 让 GORM 根据 Go 类型自动决定) // 特别是处理 SQLite 中大写 INTEGER 导致的冗余 tag,防止 MySQL/PG 识别为 32位 INT if dbType == "integer" || dbType == "int" || dbType == "bigint" { tag.Remove("type") } // 3. 处理 MySQL 索引长度限制 (Error 1071) // 注意:GormTag 可能为 map[string][]string 或 map[string]string。 // 这里通过 key 检查判断索引。 isIndexed := false indexKeys := []string{"index", "uniqueIndex", "unique_index"} for _, k := range indexKeys { if v, exists := tag[k]; exists { if len(v) > 0 { isIndexed = true break } } } if isIndexed && (strings.ToUpper(dbType) == "TEXT" || strings.ToUpper(dbType) == "LONGTEXT" || dbType == "text") { tag.Set("type", "varchar(255)") } return tag })) } return opts } // isPrimaryKey 检查列是否是主键 func isPrimaryKey(col gorm.ColumnType) bool { if pk, ok := col.PrimaryKey(); ok && pk { return true } return false } func main() { g := gen.NewGenerator(gen.Config{ // 默认会在 OutPath 目录生成CRUD代码,并且同目录下生成 model 包 // 所以OutPath最终package不能设置为model,在有数据库表同步的情况下会产生冲突 // 若一定要使用可以通过ModelPkgPath单独指定model package的名称 OutPath: "./internal/query", /* ModelPkgPath: "dal/model"*/ // gen.WithoutContext:禁用WithContext模式 // gen.WithDefaultQuery:生成一个全局Query对象Q // gen.WithQueryInterface:生成Query接口 Mode: gen.WithQueryInterface, WithUnitTest: false, FieldWithTypeTag: false, FieldWithIndexTag: true, }) db := Db(dbDsn, dbType) g.UseDB(db) var dataMap = map[string]func(gorm.ColumnType) (dataType string){ // int mapping "integer": func(columnType gorm.ColumnType) (dataType string) { return "int64" }, "INTEGER": func(columnType gorm.ColumnType) (dataType string) { return "int64" }, "int": func(columnType gorm.ColumnType) (dataType string) { return "int64" }, "INT": func(columnType gorm.ColumnType) (dataType string) { return "int64" }, } g.WithDataTypeMap(dataMap) // 获取表列表 tableList, _ := db.Migrator().GetTables() // 基础配置 opts := []gen.ModelOpt{ gen.FieldRename("fid", "FID"), //gen.FieldType("uid", "int64"), gen.FieldType("created_at", "timex.Time"), gen.FieldType("updated_at", "timex.Time"), gen.FieldType("deleted_at", "timex.Time"), //gen.FieldType("mtime", "timex.Time"), gen.FieldGORMTag("created_at", func(tag field.GormTag) field.GormTag { tag.Set("autoCreateTime", "false") tag.Set("default", "NULL") return tag }), gen.FieldGORMTag("updated_at", func(tag field.GormTag) field.GormTag { tag.Set("autoUpdateTime", "false") tag.Set("default", "NULL") return tag }), gen.FieldGORMTag("deleted_at", func(tag field.GormTag) field.GormTag { tag.Set("default", "NULL") return tag }), gen.FieldGORMTag("mtime", func(tag field.GormTag) field.GormTag { //tag.Set("type", "datetime") tag.Set("default", "0") return tag }), gen.FieldJSONTagWithNS(func(columnName string) string { return SQLColumnToHumpStyle(columnName) }), gen.FieldNewTagWithNS("form", func(columnName string) string { return SQLColumnToHumpStyle(columnName) }), } for _, table := range tableList { if table == "sqlite_sequence" || table == "schema_version" || strings.HasPrefix(table, "sqlite_") { continue } // 组合基础选项和表特有选项 tableOpts := append([]gen.ModelOpt{}, opts...) tableOpts = append(tableOpts, getTableDefaultValueTags(db, table)...) g.ApplyBasic(g.GenerateModel(table, tableOpts...)) } g.Execute() } ================================================ FILE: cmd/mfmt/main.go ================================================ package main import ( "bufio" "bytes" "crypto/sha256" "fmt" "go/ast" "go/format" "go/parser" "go/token" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/pkg/errors" "go.uber.org/zap" "golang.org/x/tools/go/packages" ) var stdlib = make(map[string]bool) func init() { pkgs, err := packages.Load(nil, "std") if err != nil { log.Fatal("get go stdlib err", zap.Error(err)) } for _, pkg := range pkgs { if !strings.HasPrefix(pkg.ID, "vendor") { stdlib[pkg.ID] = true } } } var module string func init() { file, err := os.Open("./go.mod") if err != nil { log.Fatal("no go.mod file found", zap.Error(err)) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "module ") { module = strings.TrimSpace(line[7:]) break } } if module == "" { log.Fatal("go.mod illegal") } } func main() { err := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() || strings.HasPrefix(path, ".") || strings.HasPrefix(path, "vendor") || strings.HasSuffix(path, ".pb.go") || filepath.Ext(path) != ".go" { return nil } raw, err := ioutil.ReadFile(path) if err != nil { return errors.Wrapf(err, "read file %s err", path) } digest0 := sha256.Sum256(raw) if raw, err = format.Source(raw); err != nil { return errors.Wrapf(err, "format file %s err", path) } file, err := parser.ParseFile(token.NewFileSet(), "", raw, 0) if err != nil { return errors.Wrapf(err, "parse file %s err", path) } var first, last int var imports []*ast.ImportSpec comments := make(map[string]string) ast.Inspect(file, func(n ast.Node) bool { switch spec := n.(type) { case *ast.ImportSpec: if first == 0 { first = int(spec.Pos()) } last = int(spec.End()) imports = append(imports, spec) k := last - 1 for ; k < len(raw); k++ { if raw[k] == '\r' || raw[k] == '\n' { break } } comment := string(raw[last-1 : k]) if index := strings.Index(comment, "//"); index != -1 { comments[spec.Path.Value] = strings.TrimSpace(comment[index+2:]) } } return true }) if imports != nil { buf := bytes.NewBuffer(nil) buf.Write(raw[:first-2]) buf.WriteString(sort(imports, comments)) buf.Write(raw[last-1:]) if raw, err = format.Source(buf.Bytes()); err != nil { return errors.Wrapf(err, "double format file %s err", path) } } digest1 := sha256.Sum256(raw) if !bytes.Equal(digest0[:], digest1[:]) { fmt.Println(path) } if err = ioutil.WriteFile(path, raw, info.Mode()); err != nil { return errors.Wrapf(err, "write file %s err", path) } return nil }) if err != nil { log.Fatal("scan project err", zap.Error(err)) } } func sort(imports []*ast.ImportSpec, comments map[string]string) string { system := bytes.NewBuffer(nil) group := bytes.NewBuffer(nil) others := bytes.NewBuffer(nil) for _, pkg := range imports { value := strings.Trim(pkg.Path.Value, `"`) switch { case stdlib[value]: if pkg.Name != nil { system.WriteString(pkg.Name.String()) system.WriteString(" ") } system.WriteString(pkg.Path.Value) if comment, ok := comments[pkg.Path.Value]; ok { system.WriteString(" ") system.WriteString("// ") system.WriteString(comment) } system.WriteString("\n") case strings.HasPrefix(value, module): if pkg.Name != nil { group.WriteString(pkg.Name.String()) group.WriteString(" ") } group.WriteString(pkg.Path.Value) if comment, ok := comments[pkg.Path.Value]; ok { group.WriteString(" ") group.WriteString("// ") group.WriteString(comment) } group.WriteString("\n") default: if pkg.Name != nil { others.WriteString(pkg.Name.String()) others.WriteString(" ") } others.WriteString(pkg.Path.Value) if comment, ok := comments[pkg.Path.Value]; ok { others.WriteString(" ") others.WriteString("// ") others.WriteString(comment) } others.WriteString("\n") } } return fmt.Sprintf("%s\n%s\n%s", system.String(), group.String(), others.String()) } ================================================ FILE: cmd/model_gen/gen.go ================================================ package main // gorm gen configure import ( "os" "reflect" "strings" "github.com/haierkeys/fast-note-sync-service/internal/query" "gorm.io/gen" ) func main() { g := gen.NewGenerator(gen.Config{ // 默认会在 OutPath 目录生成CRUD代码,并且同目录下生成 model 包 // 所以OutPath最终package不能设置为model,在有数据库表同步的情况下会产生冲突 // 若一定要使用可以通过ModelPkgPath单独指定model package的名称 OutPath: "./internal/query", /* ModelPkgPath: "dal/model"*/ // gen.WithoutContext:禁用WithContext模式 // gen.WithDefaultQuery:生成一个全局Query对象Q // gen.WithQueryInterface:生成Query接口 Mode: gen.WithQueryInterface, WithUnitTest: false, FieldWithTypeTag: false, }) v := reflect.ValueOf(query.Query{}) goContent := ` package model import ( "gorm.io/gorm" ) func AutoMigrate(db *gorm.DB, key string) error { if db == nil { return nil } switch key { ` goContentFunc := ` case "{NAME}": return db.AutoMigrate({NAME}{}) ` if v.Kind() == reflect.Struct { t := v.Type() fields := []string{} for i := 0; i < v.NumField(); i++ { field := t.Field(i) if field.Name == "db" { continue } fields = append(fields, field.Name+"{}") goContent += strings.ReplaceAll(goContentFunc, "{NAME}", field.Name) //goContentHeader += fmt.Sprintf("type %s = %s\n", field.Name, field.Type.Name()) } //goContent += "\tcase \"\":\n\t\treturn db.AutoMigrate(" + strings.Join(fields, ", ") + ")" goContent += "\t}\n\treturn nil\n}" _ = os.WriteFile(g.OutPath[0:len(g.OutPath)-6]+"/model/model.go", []byte(goContent), os.ModePerm) } } ================================================ FILE: cmd/reset_password.go ================================================ package cmd import ( "context" "fmt" "os" internalApp "github.com/haierkeys/fast-note-sync-service/internal/app" "github.com/haierkeys/fast-note-sync-service/internal/dao" "github.com/haierkeys/fast-note-sync-service/pkg/fileurl" "github.com/haierkeys/fast-note-sync-service/pkg/logger" "github.com/haierkeys/fast-note-sync-service/pkg/util" "github.com/spf13/cobra" "go.uber.org/zap" "gorm.io/gorm" ) func init() { var configPath string var username string var password string var resetPasswordCmd = &cobra.Command{ Use: "reset-password -u -p [-c config_file]", Short: "Reset a user's password by username", // 通过用户名重置用户密码,无需旧密码 Run: func(cmd *cobra.Command, args []string) { if username == "" { bootstrapLogger.Error("username is required, use -u flag") os.Exit(1) } if password == "" { bootstrapLogger.Error("password is required, use -p flag") os.Exit(1) } // Load configuration // 加载配置 if configPath == "" { // We can rely on default logic in further layers or set a default here // Use the same logic as run.go for consistency if fileurl.IsExist("config/config-dev.yaml") { configPath = "config/config-dev.yaml" } else if fileurl.IsExist("config.yaml") { configPath = "config.yaml" } else { configPath = "config/config.yaml" } } appConfig, configRealpath, err := internalApp.LoadConfig(configPath) if err != nil { bootstrapLogger.Error("failed to load config", zap.Error(err)) os.Exit(1) } bootstrapLogger.Info("loading config", zap.String("path", configRealpath)) // Initialize logger // 初始化日志 lg, err := logger.NewLogger(logger.Config{ Level: appConfig.Log.Level, File: appConfig.Log.File, Production: appConfig.Log.Production, }) if err != nil { bootstrapLogger.Error("failed to init logger", zap.Error(err)) os.Exit(1) } // Initialize database // 初始化数据库 dbConfig := appConfig.Database dbConfig.RunMode = appConfig.Server.RunMode db, err := dao.NewEngine(dbConfig, lg) if err != nil { bootstrapLogger.Error("failed to init database", zap.Error(err)) os.Exit(1) } // Initialize Dao and UserRepository // 初始化 Dao 和 UserRepository ctx := context.Background() daoObj := dao.New(db, ctx, dao.WithConfig(&dbConfig), dao.WithLogger(lg)) userRepo := dao.NewUserRepository(daoObj) // Look up target user by username // 根据用户名查找目标用户 user, err := userRepo.GetByUsername(ctx, username) if err != nil { if err == gorm.ErrRecordNotFound { fmt.Fprintf(os.Stderr, "Error: user '%s' not found\n", username) } else { fmt.Fprintf(os.Stderr, "Error: failed to query user: %v\n", err) } os.Exit(1) } // Generate password hash // 生成密码哈希 hashedPassword, err := util.GeneratePasswordHash(password) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to generate password hash: %v\n", err) os.Exit(1) } // Update password // 更新密码 if err := userRepo.UpdatePassword(ctx, hashedPassword, user.UID); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to update password: %v\n", err) os.Exit(1) } fmt.Printf("Password for user '%s' (uid=%d) has been reset successfully.\n", username, user.UID) }, } rootCmd.AddCommand(resetPasswordCmd) fs := resetPasswordCmd.Flags() fs.StringVarP(&configPath, "config", "c", "", "config file path (default: config/config.yaml)") fs.StringVarP(&username, "username", "u", "", "target username (required)") fs.StringVarP(&password, "password", "p", "", "new password (required)") } ================================================ FILE: cmd/root.go ================================================ package cmd import ( "embed" "os" "github.com/spf13/cobra" "go.uber.org/zap" ) var frontendFiles embed.FS var configDefault string var rootCmd = &cobra.Command{ Use: "fast-note-sync-service", Short: "Fast Note Sync Service", Run: func(cmd *cobra.Command, args []string) { cmd.HelpTemplate() cmd.Help() }, } // Execute executes the root command // Execute 执行根命令 func Execute(efs embed.FS, c string) { frontendFiles = efs configDefault = c if err := rootCmd.Execute(); err != nil { BootstrapLogger().Error("command execution failed", zap.Error(err)) os.Exit(1) } } ================================================ FILE: cmd/run.go ================================================ package cmd import ( "os" "os/signal" "path/filepath" "strings" "syscall" "time" internalApp "github.com/haierkeys/fast-note-sync-service/internal/app" "github.com/haierkeys/fast-note-sync-service/pkg/fileurl" "github.com/haierkeys/fast-note-sync-service/pkg/util" "github.com/radovskyb/watcher" "github.com/spf13/cobra" "go.uber.org/zap" ) type runFlags struct { dir string // Project root directory // 项目根目录 port string // Startup port // 启动端口 runMode string // Startup mode // 启动模式 config string // Specified configuration file path // 指定要使用的配置文件路径 } func init() { runEnv := new(runFlags) var runCommand = &cobra.Command{ Use: "run [-c config_file] [-d working_dir] [-p port]", Short: "Run service", Run: func(cmd *cobra.Command, args []string) { if len(runEnv.dir) > 0 { err := os.Chdir(runEnv.dir) if err != nil { bootstrapLogger.Error("failed to change the current working directory", zap.Error(err)) } bootstrapLogger.Info("working directory changed", zap.String("dir", runEnv.dir)) } if len(runEnv.config) <= 0 { if fileurl.IsExist("config/config-dev.yaml") { runEnv.config = "config/config-dev.yaml" } else if fileurl.IsExist("config.yaml") { runEnv.config = "config.yaml" } else if fileurl.IsExist("config/config.yaml") { runEnv.config = "config/config.yaml" } else { bootstrapLogger.Warn("config file not found, creating default config") runEnv.config = "config/config.yaml" configDefault = strings.Replace(configDefault, "fast-note-sync-Auth-Token", util.GetRandomString(32), 1) if err := fileurl.CreatePath(runEnv.config, os.ModePerm); err != nil { bootstrapLogger.Error("config file auto create error", zap.Error(err)) return } file, err := os.OpenFile(runEnv.config, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) if err != nil { bootstrapLogger.Error("config file auto create error", zap.Error(err)) return } defer file.Close() _, err = file.WriteString(configDefault) if err != nil { bootstrapLogger.Error("config file auto create writing error", zap.Error(err)) return } bootstrapLogger.Info("config file auto create successfully", zap.String("path", runEnv.config)) } } s, err := NewServer(runEnv) if err != nil { bootstrapLogger.Error("api service start err", zap.Error(err)) return } configChanged := make(chan struct{}, 1) go func() { w := watcher.New() // Set MaxEvents to 1 to receive at most 1 event in each listening cycle // 将 SetMaxEvents 设置为 1,以便在每个监听周期中至多接收 1 个事件 // If SetMaxEvents is not set, all events are sent by default. // 如果没有设置 SetMaxEvents,默认情况下会发送所有事件。 w.SetMaxEvents(1) // Only notify write events. // 只通知写入事件。 w.FilterOps(watcher.Write) go func() { for { select { case event := <-w.Event: s.logger.Info("config watcher change detected", zap.String("event", event.Op.String()), zap.String("file", event.Path)) select { case configChanged <- struct{}{}: default: } case err := <-w.Error: s.logger.Error("config watcher error", zap.Error(err)) case <-w.Closed: bootstrapLogger.Info("config watcher closed") return } } }() // Watch config.yaml file // 监听 config.yaml 文件 if err := w.Add(runEnv.config); err != nil { s.logger.Error("config watcher file error", zap.Error(err)) } // Start watching // 启动监听 if err := w.Start(time.Second * 5); err != nil { s.logger.Error("config watcher start error", zap.Error(err)) } }() quit1 := make(chan os.Signal, 1) signal.Notify(quit1, syscall.SIGINT, syscall.SIGTERM) for { select { case <-quit1: s.logger.Info("Received shutdown signal, initiating graceful shutdown...") s.sc.SendCloseSignal(nil) // 等待并在处理后退出循环 goto wait_and_exit case <-configChanged: s.logger.Info("Reloading server due to config change...") s.sc.SendCloseSignal(nil) if err := s.sc.WaitClosed(); err != nil { s.logger.Error("Failed to close old server during reload", zap.Error(err)) } // 重新初始化 server newS, err := NewServer(runEnv) if err != nil { bootstrapLogger.Error("failed to re-initialize service after config change", zap.Error(err)) continue // 尝试继续运行旧实例(如果可能)或再次等待配置修复 } s = newS s.logger.Info("Server re-initialized successfully") continue // 重新进入 select 监听新 s 的 UpgradeSignal case newBinaryPath := <-s.GetApp().UpgradeSignal: s.logger.Info("Received upgrade/restart signal, starting smooth restart...", zap.String("newBinary", newBinaryPath)) currentBinary, _ := os.Executable() isRestartOnly := newBinaryPath == currentBinary if !isRestartOnly { // 1. Perform file replacement (for upgrade) oldBinary := currentBinary + ".old" _ = os.Remove(oldBinary) if err := util.MoveFile(currentBinary, oldBinary); err != nil { s.logger.Error("Failed to backup current binary", zap.Error(err)) return } if err := util.MoveFile(newBinaryPath, currentBinary); err != nil { s.logger.Error("Failed to replace binary", zap.Error(err)) // Try to restore? _ = util.MoveFile(oldBinary, currentBinary) return } if err := os.Chmod(currentBinary, 0755); err != nil { s.logger.Error("Failed to set executable permission", zap.Error(err)) } // 1.1 Cleanup temp directory (where the tar.gz and temporary binary were) tempDir := filepath.Dir(newBinaryPath) if err := os.RemoveAll(tempDir); err != nil { s.logger.Warn("Failed to cleanup upgrade temp directory", zap.String("path", tempDir), zap.Error(err)) } } // 2. Graceful shutdown (close servers, release ports) s.sc.SendCloseSignal(nil) if err := s.sc.WaitClosed(); err != nil { s.logger.Error("Shutdown failed before restart", zap.Error(err)) } // 3. Restart env := os.Environ() args := os.Args if err := internalApp.RestartProcess(currentBinary, args, env); err != nil { s.logger.Error("Failed to restart process", zap.Error(err)) } return // 退出主循环 } } wait_and_exit: // Wait for all shutdown handlers to complete (including App Container graceful shutdown) // 等待所有关闭处理器完成(包括 App Container 的优雅关闭) if err := s.sc.WaitClosed(); err != nil { s.logger.Error("Shutdown completed with error", zap.Error(err)) } else { s.logger.Info("Service has been shut down gracefully.") } }, } rootCmd.AddCommand(runCommand) fs := runCommand.Flags() fs.StringVarP(&runEnv.dir, "dir", "d", "", "run dir") fs.StringVarP(&runEnv.port, "port", "p", "", "run port") fs.StringVarP(&runEnv.runMode, "mode", "m", "", "run mode") fs.StringVarP(&runEnv.config, "config", "c", "", "config file") } ================================================ FILE: cmd/run_server.go ================================================ package cmd import ( "context" "fmt" "net/http" "os" "path/filepath" "reflect" "strings" "time" internalApp "github.com/haierkeys/fast-note-sync-service/internal/app" "github.com/haierkeys/fast-note-sync-service/internal/dao" "github.com/haierkeys/fast-note-sync-service/internal/routers" "github.com/haierkeys/fast-note-sync-service/internal/task" "github.com/haierkeys/fast-note-sync-service/internal/upgrade" "github.com/haierkeys/fast-note-sync-service/pkg/logger" "github.com/haierkeys/fast-note-sync-service/pkg/safe_close" "github.com/haierkeys/fast-note-sync-service/pkg/validator" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/en" "github.com/go-playground/locales/zh" ut "github.com/go-playground/universal-translator" validatorV10 "github.com/go-playground/validator/v10" en_translations "github.com/go-playground/validator/v10/translations/en" zh_translations "github.com/go-playground/validator/v10/translations/zh" "go.uber.org/zap" "gorm.io/gorm" ) // defaultSecretKeys defines the list of default secret keys to be detected // defaultSecretKeys 定义需要检测的默认密钥列表 var defaultSecretKeys = []string{ "6666", "fast-note-sync-Auth-Token", "", } // DefaultShutdownTimeout default shutdown timeout duration // DefaultShutdownTimeout 默认关闭超时时间 const DefaultShutdownTimeout = 30 * time.Second type Server struct { logger *zap.Logger // Logger // 日志对象 config *internalApp.AppConfig // App configuration (injected dependency) // 应用配置(注入的依赖) db *gorm.DB // Database connection // 数据库连接 ut *ut.UniversalTranslator // Translator // 翻译器 httpServer *http.Server privateHttpServer *http.Server sc *safe_close.SafeClose app *internalApp.App // App Container } // checkSecurityConfigWithConfig checks security configuration, outputs warning if using default keys // checkSecurityConfig 检查安全配置,如果使用默认密钥则输出警告 func checkSecurityConfigWithConfig(cfg *internalApp.AppConfig, lg *zap.Logger) { isDefault := false for _, key := range defaultSecretKeys { if cfg.Security.AuthTokenKey == key { isDefault = true break } } if isDefault { // Output to console // 输出到控制台 fmt.Println() fmt.Println(strings.Repeat("=", 60)) fmt.Println("⚠️ SECURITY WARNING: Using default secret key!") fmt.Println() fmt.Println("Please modify 'security.auth-token-key' in config.yaml") fmt.Println("Generate a secure key with:") fmt.Println(" openssl rand -base64 32") fmt.Println(strings.Repeat("=", 60)) fmt.Println() // Record to log // 记录到日志 if lg != nil { lg.Warn("Using default secret key - please change security.auth-token-key in config.yaml") } } } func NewServer(runEnv *runFlags) (*Server, error) { // Use LoadConfig to directly load config into AppConfig // 使用 LoadConfig 直接加载配置到 AppConfig appConfig, configRealpath, err := internalApp.LoadConfig(runEnv.config) if err != nil { return nil, fmt.Errorf("failed to load config: %w", err) } // Determine run mode // 确定运行模式 runMode := runEnv.runMode if len(runMode) <= 0 { runMode = appConfig.Server.RunMode } if len(runMode) > 0 { gin.SetMode(runMode) } else { gin.SetMode(gin.ReleaseMode) } s := &Server{ config: appConfig, sc: safe_close.NewSafeClose(), } // Initialize logger (using injected config) // 初始化日志器(使用注入的配置) if err := initLoggerWithConfig(s, appConfig); err != nil { return nil, fmt.Errorf("initLogger: %w", err) } // Check security configuration (using injected config) // 检查安全配置(使用注入的配置) checkSecurityConfigWithConfig(appConfig, s.logger) // Initialize storage directory (using injected config) // 初始化存储目录(使用注入的配置) if err := initStorageWithConfig(appConfig); err != nil { return nil, fmt.Errorf("initStorage: %w", err) } // Initialize database (using injected config) // 初始化数据库(使用注入的配置) db, err := initDatabaseWithConfig(appConfig, s.logger) if err != nil { return nil, fmt.Errorf("initDatabase: %w", err) } s.db = db // Initialize App Container (using AppConfig directly) // 初始化 App Container(直接使用 AppConfig) app, err := internalApp.NewApp(appConfig, s.logger, db, frontendFiles) if err != nil { return nil, fmt.Errorf("failed to create app container: %w", err) } s.app = app // Auto-execute migration tasks (using injected config) // 自动执行迁移任务(使用注入的配置) if err := upgrade.Execute( db, s.logger, internalApp.Version, &appConfig.Database, &appConfig.UserDatabase, ); err != nil { return nil, fmt.Errorf("upgrade.Execute: %w", err) } // Initialize validator // 初始化验证器 uni, err := initValidatorWithLogger(s.logger) if err != nil { return nil, fmt.Errorf("initValidator: %w", err) } s.ut = uni validator.RegisterCustom() // Start scheduler // 启动调度器 initScheduler(s) banner := ` ______ __ _ __ __ _____ / ____/___ ______/ /_ / | / /___ / /____ / ___/__ ______ _____ / /_ / __ / ___/ __/ / |/ / __ \/ __/ _ \ \__ \/ / / / __ \/ ___/ / __/ / /_/ (__ ) /_ / /| / /_/ / /_/ __/ ___/ / /_/ / / / / /__ /_/ \__,_/____/\__/ /_/ |_/\____/\__/\___/ /____/\__, /_/ /_/\___/ /____/ ` s.logger.Warn(fmt.Sprintf("%s\n\n%s v%s\nGit: %s\nBuildTime: %s\n", banner, internalApp.Name, internalApp.Version, internalApp.GitTag, internalApp.BuildTime)) s.logger.Warn("config loaded", zap.String("path", configRealpath)) // Start HTTP API server // 启动 HTTP API 服务器 if httpAddr := appConfig.Server.HttpPort; len(httpAddr) > 0 { s.logger.Warn("api_router", zap.String("config.server.HttpPort", appConfig.Server.HttpPort)) s.httpServer = &http.Server{ Addr: appConfig.Server.HttpPort, Handler: routers.NewRouter(frontendFiles, s.app, s.ut), ReadTimeout: time.Duration(appConfig.Server.ReadTimeout) * time.Second, WriteTimeout: time.Duration(appConfig.Server.WriteTimeout) * time.Second, MaxHeaderBytes: 1 << 20, } s.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() errChan := make(chan error, 1) go func() { errChan <- s.httpServer.ListenAndServe() }() select { case err := <-errChan: s.logger.Error("api service err", zap.Error(err)) s.sc.SendCloseSignal(err) case <-closeSignal: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 停止HTTP服务器 if err := s.httpServer.Shutdown(ctx); err != nil { s.logger.Error("api service shutdown error", zap.Error(err)) } // _ = s.httpServer.Close() } }) } if httpAddr := appConfig.Server.PrivateHttpListen; len(httpAddr) > 0 { s.logger.Info("api_router", zap.String("config.server.PrivateHttpListen", appConfig.Server.PrivateHttpListen)) s.privateHttpServer = &http.Server{ Addr: appConfig.Server.PrivateHttpListen, Handler: routers.NewPrivateRouterWithLogger(appConfig.Server.RunMode, s.logger), ReadTimeout: time.Duration(appConfig.Server.ReadTimeout) * time.Second, WriteTimeout: time.Duration(appConfig.Server.WriteTimeout) * time.Second, MaxHeaderBytes: 1 << 20, } s.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() errChan := make(chan error, 1) go func() { errChan <- s.privateHttpServer.ListenAndServe() }() select { case err := <-errChan: s.logger.Error("private api service err", zap.Error(err)) s.sc.SendCloseSignal(err) case <-closeSignal: // _ = s.httpServer.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Stop HTTP server // 停止 HTTP 服务器 if err := s.privateHttpServer.Shutdown(ctx); err != nil { s.logger.Error("private api service shutdown error", zap.Error(err)) } } }) } // Register App Container graceful shutdown (using Shutdown method) // 注册 App Container 的优雅关闭(使用 Shutdown 方法) s.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() <-closeSignal if s.app != nil { // Use graceful shutdown with timeout // 使用带超时的优雅关闭 ctx, cancel := context.WithTimeout(context.Background(), DefaultShutdownTimeout) defer cancel() if err := s.app.Shutdown(ctx); err != nil { s.logger.Error("failed to shutdown app container", zap.Error(err)) } else { s.logger.Info("App container shutdown gracefully") } } }) // Start ngrok tunnel if enabled if appConfig.Ngrok.Enabled && appConfig.Ngrok.AuthToken != "" { s.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() s.logger.Info("Starting ngrok tunnel...") err := s.app.NgrokService.Start(context.Background(), appConfig.Server.HttpPort) if err != nil { s.logger.Error("failed to start ngrok tunnel", zap.Error(err)) return } s.logger.Info("Ngrok tunnel started", zap.String("url", s.app.NgrokService.TunnelURL())) // Stay attached until close signal <-closeSignal }) } // Start Cloudflare tunnel if enabled if appConfig.Cloudflare.Enabled && appConfig.Cloudflare.Token != "" { s.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() s.logger.Info("Starting Cloudflare tunnel...") if err := s.app.CloudflareService.Start(context.Background(), appConfig.Cloudflare.Token, appConfig.Cloudflare.LogEnabled); err != nil { s.logger.Error("failed to start cloudflare tunnel", zap.Error(err)) return } s.logger.Info("Cloudflare tunnel started", zap.String("url", s.app.CloudflareService.TunnelURL())) // Stay attached until close signal <-closeSignal }) } return s, nil } func initScheduler(s *Server) { // Create task manager // 创建任务管理器 manager := task.NewManager(s.logger, s.sc, s.app) // Register all tasks (business layer control) // 注册所有任务(业务层控制) if err := manager.RegisterTasks(); err != nil { s.logger.Error("failed to register tasks", zap.Error(err)) return } // Start task scheduler // 启动任务调度器 manager.Start() } // initLoggerWithConfig initializes logger (using injected config) // initLoggerWithConfig 初始化日志器(使用注入的配置) func initLoggerWithConfig(s *Server, cfg *internalApp.AppConfig) error { lg, err := logger.NewLogger(logger.Config{ Level: cfg.Log.Level, File: cfg.Log.File, Production: cfg.Log.Production, }) if err != nil { return fmt.Errorf("failed to init logger: %w", err) } s.logger = lg return nil } // initValidatorWithLogger initializes validator, returns UniversalTranslator // initValidatorWithLogger 初始化验证器,返回 UniversalTranslator func initValidatorWithLogger(lg *zap.Logger) (*ut.UniversalTranslator, error) { customValidator := validator.NewCustomValidator() customValidator.Engine() binding.Validator = customValidator var uni *ut.UniversalTranslator validate, ok := binding.Validator.Engine().(*validatorV10.Validate) if ok { validate.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) uni = ut.New(en.New(), en.New(), zh.New()) zhTran, _ := uni.GetTranslator("zh") enTran, _ := uni.GetTranslator("en") err := zh_translations.RegisterDefaultTranslations(validate, zhTran) if err != nil { return nil, err } err = en_translations.RegisterDefaultTranslations(validate, enTran) if err != nil { return nil, err } } return uni, nil } func initDatabaseWithConfig(cfg *internalApp.AppConfig, lg *zap.Logger) (*gorm.DB, error) { // Convert AppConfig.DatabaseConfig to config.DatabaseConfig // 转换 AppConfig.DatabaseConfig 为 config.DatabaseConfig dbConfig := cfg.Database dbConfig.RunMode = cfg.Server.RunMode db, err := dao.NewEngine(dbConfig, lg) if err != nil { return nil, err } return db, nil } // initStorageWithConfig initializes storage directory (using injected config) // initStorageWithConfig 初始化存储目录(使用注入的配置) func initStorageWithConfig(cfg *internalApp.AppConfig) error { dirs := []string{ filepath.Dir(cfg.Log.File), cfg.App.TempPath, cfg.Storage.LocalFS.SavePath, filepath.Dir(cfg.Database.Path), } // 如果 UserDatabase 配置了独立的路径且为 sqlite,也需要初始化目录 if cfg.UserDatabase.Type == "sqlite" && cfg.UserDatabase.Path != "" { dirs = append(dirs, filepath.Dir(cfg.UserDatabase.Path)) } for _, dir := range dirs { if dir == "" { continue } if err := os.MkdirAll(dir, 0754); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } } return nil } // GetApp gets App Container // GetApp 获取 App Container func (s *Server) GetApp() *internalApp.App { return s.app } // GetConfig gets app configuration // GetConfig 获取应用配置 func (s *Server) GetConfig() *internalApp.AppConfig { return s.config } ================================================ FILE: cmd/upgrade.go ================================================ package cmd import ( "os" internalApp "github.com/haierkeys/fast-note-sync-service/internal/app" "github.com/haierkeys/fast-note-sync-service/internal/dao" "github.com/haierkeys/fast-note-sync-service/internal/upgrade" "github.com/haierkeys/fast-note-sync-service/pkg/logger" "github.com/spf13/cobra" "go.uber.org/zap" ) var upgradeCmd = &cobra.Command{ Use: "upgrade", Short: "Upgrade legacy database schema and other data to the latest version", Long: `Upgrade legacy database schema and other data to the latest version. This command will check the current database version and apply all pending migrations. It is safe to run this command multiple times - already applied migrations will be skipped.`, Run: func(cmd *cobra.Command, args []string) { // Load configuration // 加载配置 configPath, _ := cmd.Flags().GetString("config") if len(configPath) <= 0 { configPath = "config/config.yaml" } // Use LoadConfig to directly load config into AppConfig // 使用 LoadConfig 直接加载配置到 AppConfig appConfig, configRealpath, err := internalApp.LoadConfig(configPath) if err != nil { bootstrapLogger.Error("Failed to load config", zap.Error(err)) os.Exit(1) } bootstrapLogger.Info("Loading config", zap.String("path", configRealpath)) // Initialize log // 初始化日志 lg, err := logger.NewLogger(logger.Config{ Level: appConfig.Log.Level, File: appConfig.Log.File, Production: appConfig.Log.Production, }) if err != nil { bootstrapLogger.Error("Failed to init logger", zap.Error(err)) os.Exit(1) } // Initialize database (using injected config) // 初始化数据库(使用注入的配置) dbConfig := appConfig.Database dbConfig.RunMode = appConfig.Server.RunMode db, err := dao.NewEngine(dbConfig, lg) if err != nil { bootstrapLogger.Error("Failed to init database", zap.Error(err)) os.Exit(1) } bootstrapLogger.Info("Starting database upgrade...") // Execute upgrade // 执行升级 if err := upgrade.Execute( db, lg, internalApp.Version, &appConfig.Database, &appConfig.UserDatabase, ); err != nil { bootstrapLogger.Error("Upgrade failed", zap.Error(err)) os.Exit(1) } bootstrapLogger.Info("Database upgrade completed successfully!") }, } func init() { rootCmd.AddCommand(upgradeCmd) upgradeCmd.Flags().StringP("config", "c", "", "config file path") } ================================================ FILE: cmd/version.go ================================================ package cmd import ( "fmt" "github.com/haierkeys/fast-note-sync-service/internal/app" "github.com/spf13/cobra" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Print out version info and exit. // 打印版本信息并退出。", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("v%s ( Git:%s ) BuidTime:%s\n", app.Version, app.GitTag, app.BuildTime) }, } func init() { rootCmd.AddCommand(versionCmd) } ================================================ FILE: config/config.yaml ================================================ server: # 运行模式: release | debug # Running mode: release | debug run-mode: release # HTTP 端口 (默认 :9000)。格式为 :port 或 IP:port # HTTP Port (default :9000). Format: :port or IP:port http-port: :9000 # 读取超时时间(秒) # Read timeout duration (seconds) read-timeout: 60 # 写入超时时间(秒) # Write timeout duration (seconds) write-timeout: 60 # 私有 HTTP 监听地址,主要用于监控/度量。留空则不开启。格式: :port # Private HTTP listen address, used for monitoring/metrics. Leave empty to disable. Format: :port private-http-listen: "" # SSE 保活心跳间隔(秒) # MCP SSE ping interval (seconds) mcp-sse-ping-interval: 30 app: # 默认每页显示的项目数 # Default items per page default-page-size: 10 # 每页显示的最大项目数限制 # Maximum items limit per page max-page-size: 100 # 默认请求上下文超时时间(秒) # Default request context timeout (seconds) default-context-timeout: 60 # 临时文件存储路径 # Temporary file storage path temp-path: storage/temp # 是否在响应中返回成功详情消息 # Whether to return success detail message in response is-return-sussess: false # 软删除笔记保留时长。例如: 7d, 24h。0 表示永久保留。 # Retention duration for soft deleted notes. e.g., 7d, 24h. 0 means keep forever. soft-delete-retention-time: "7d" # 同步日志保留时长。例如: 30d, 7d。 # Retention duration for sync logs. e.g., 30d, 7d. sync-log-retention-time: "30d" # 历史记录保留的最大版本数 # Maximum number of history versions to keep history-keep-versions: 100 # 历史记录保存延迟。支持格式: 10s, 1m。 # delay for saving history records. Supports: 10s, 1m. history-save-delay: "10s" # 文件上传会话超时时长 # Timeout duration for file upload sessions upload-session-timeout: "1d" # 文件上传/下载的分块大小。例如: 512KB, 1MB # Chunk size for file upload/download. e.g., 512KB, 1MB file-chunk-size: "512KB" # 文件分片下载超时时长 # Timeout duration for file chunk downloading download-session-timeout: "1h" # Worker Pool 最大工作协程数 # Worker Pool maximum number of worker goroutines worker-pool-max-workers: 100 # Worker Pool 任务队列大小 # Worker Pool task queue capacity worker-pool-queue-size: 1000 # 写入队列容量 (每个用户) # Write queue capacity (per user) write-queue-capacity: 1000 # 写入队列操作超时时长 # Timeout duration for write queue operations write-queue-timeout: "30s" # 写入队列空闲清理时长 # Idle cleanup duration for write queue write-queue-idle-time: "10m" # WebSocket 读取最大负载大小。例如: 128MB # WebSocket maximum read payload size. e.g., 128MB ws-read-max-payload-size: "128MB" # WebSocket 写入最大负载大小。例如: 128MB # WebSocket maximum write payload size. e.g., 128MB ws-write-max-payload-size: "128MB" # 是否开启 WebSocket 并行处理 # Whether to enable WebSocket parallel processing ws-parallel-enabled: true # WebSocket 并行处理的最大协程限制 # Maximum goroutine limit for WebSocket parallel processing ws-parallel-golimit: 8 # 是否对 WebSocket 消息进行 UTF-8 校验 # Whether to perform UTF-8 validation on WebSocket messages ws-check-utf8-enabled: true # 是否开启 WebSocket 消息压缩 # Whether to enable WebSocket message compression ws-compression-enabled: true # WebSocket 压缩级别 (1-9) # WebSocket compression level (1-9) ws-compression-level: 1 # 触发 WebSocket 压缩的最小载荷大小(字节) # Minimum payload size (bytes) to trigger WebSocket compression ws-compression-threshold: 512 # 日志保存路径 # Directory path for log output log-save-fileurl: storage/logs/ # 日志文件名 # Log filename log-file: log.log # 数据拉取源设置: auto(自动检测) | github | cnb # Data pull source setting: auto(detect) | github | cnb pull-source: auto security: # 认证令牌加密混淆 Key # Internal key for auth token encryption and obfuscation auth-token-key: fast-note-sync-Auth-Token # 认证令牌过期时间。例如: 365d, 7d, 24h # Expiry duration for authentication tokens. e.g., 365d, 7d, 24h token-expiry: "365d" # 分享令牌加密混淆 Key # Internal key for share token encryption and obfuscation share-token-key: fns # 分享令牌过期时间。例如: 30d, 7d # Expiry duration for share tokens. e.g., 30d, 7d share-token-expiry: "30d" # 主数据库配置 # Main database configuration database: # 数据库类型: sqlite | mysql | postgres # Database type: sqlite | mysql | postgres type: sqlite # 数据库文件路径 (针对 sqlite) # Database file path (for sqlite) path: storage/database/db.sqlite3 # 数据库连接地址 (针对 mysql/postgres) # Database host (for mysql/postgres) host: # 数据库端口 (针对 mysql/postgres) # Database port (for mysql/postgres) port: # 数据库登录用户名 # Database login username username: # 数据库登录密码 # Database login password password: "" # 数据库名称 # Database name name: # SSL 模式 (仅限 postgres) # SSL mode (postgres only) ssl-mode: # 数据库表前缀 # Prefix for all database tables table-prefix: "" # 数据库 Schema (仅限 postgres) # Database schema (postgres only) schema: # 是否开启自动数据库迁移 # Whether to enable automatic database migration auto-migrate: true # 数据库字符集 (默认 utf8mb4) # Database charset (default utf8mb4) charset: # 是否解析时间字段 (针对 mysql) # Whether to parse time fields (for mysql) parse-time: true # 数据库连接池最大打开连接数 # Maximum number of open connections in the pool max-open-conns: 100 # 数据库连接池最大空闲连接数 # Maximum number of idle connections in the pool max-idle-conns: 10 # 连接最大可重用时长 # Maximum duration a connection can be reused conn-max-lifetime: "30m" # 连接处于空闲状态的最大时长 # Maximum duration a connection can remain idle conn-max-idle-time: "10m" # 是否启用数据库异步写入队列 # Whether to enable asynchronous database write queue enable-write-queue: true # 最大并发写入数限制 (当 enable-write-queue 为 false 时) # Maximum concurrent write operations limit (when enable-write-queue is false) max-write-concurrency: 0 # 用户隔离数据库设置, 如果不设置(Type设置为空), 则使用主数据库 # User isolation database settings, if not set(Type is empty), use the main database user-database: # 数据库类型: sqlite | mysql | postgres # Database type: sqlite | mysql | postgres type: # 数据库文件路径 (针对 sqlite) # Database file path (for sqlite) path: # 数据库连接地址 (针对 mysql/postgres) # Database host (for mysql/postgres) host: # 数据库端口 (针对 mysql/postgres) # Database port (for mysql/postgres) port: # 数据库登录用户名 # Database login username username: # 数据库登录密码 # Database login password password: "" # 数据库名称 # Database name name: # SSL 模式 (仅限 postgres) # SSL mode (postgres only) ssl-mode: # 数据库表前缀 # Prefix for all database tables table-prefix: "" # 数据库 Schema (仅限 postgres) # Database schema (postgres only) schema: public # 是否开启自动数据库迁移 # Whether to enable automatic database migration auto-migrate: true # 数据库字符集 (默认 utf8mb4) # Database charset (default utf8mb4) charset: # 是否解析时间字段 (针对 mysql) # Whether to parse time fields (for mysql) parse-time: true # 数据库连接池最大打开连接数 # Maximum number of open connections in the pool max-open-conns: 100 # 数据库连接池最大空闲连接数 # Maximum number of idle connections in the pool max-idle-conns: 10 # 连接最大可重用时长 # Maximum duration a connection can be reused conn-max-lifetime: "30m" # 连接处于空闲状态的最大时长 # Maximum duration a connection can remain idle conn-max-idle-time: "10m" # 是否启用数据库异步写入队列 # Whether to enable asynchronous database write queue enable-write-queue: true # 最大并发写入数限制 (当 enable-write-queue 为 false 时) # Maximum concurrent write operations limit (when enable-write-queue is false) max-write-concurrency: 0 log: # 日志级别: debug | info | warn | error # Log level: debug | info | warn | error level: warn # 日志输出文件路径 # File path for log output file: storage/logs/log.log # 是否为生产环境 (开启后使用 JSON 格式输出) # Whether this is a production environment (uses JSON output if true) production: true user: # 是否开启用户注册功能 # Whether to enable user registration register-is-enable: true # 管理员 UID。0 表示任何用户都不能作为超级管理员或是未指定。 # Administrator UID. 0 means no user is designated or restricted. admin-uid: 0 tracer: # 是否开启请求链路追踪 # Whether to enable request tracing enabled: true # 请求头中的 Trace ID 字段名 # Header name for the Trace ID header: "X-Trace-ID" short-link: # 短链服务的基础 URL # Base URL of the short link service base-url: "https://sink.cool" # 短链服务的 API Key # API Key for the short link service api-key: "SinkCool" # 短链服务的访问密码 (可选) # Access password for the short link service (optional) password: "" # 是否开启短链隐藏重定向 (Cloaking) # Whether to enable short link URL cloaking cloaking: false storage: local-fs: # 是否启用本地文件系统存储 # Whether to enable local file system storage is-enable: false # 是否启用 HTTP 文件服务 (用于提供文件的 HTTP 直接访问) # Whether to enable HTTP file server for direct access httpfs-is-enable: true # 文件保存路径 # Directory path for saved files save-path: "storage/uploads" aliyun-oss: # 是否启用 阿里云 OSS 存储 # Whether to enable Aliyun OSS storage is-enable: true aws-s3: # 是否启用 AWS S3 存储 # Whether to enable AWS S3 storage is-enable: true cloudflare-r2: # 是否启用 Cloudflare R2 存储 # Whether to enable Cloudflare R2 storage is-enable: true minio: # 是否启用 MinIO 存储 # Whether to enable MinIO storage is-enable: true webdav: # 是否启用 WebDAV 存储 # Whether to enable WebDAV storage is-enable: true git: # Git 提交记录中的作者名称 # Author name used in git commits name: "FNS Service" # Git 提交记录中的作者邮箱 # Author email used in git commits email: "fns@email.com" webgui: # Web 界面字体设置。留空使用默认,"local" 使用本地字体,或填入字体链接。 # Web GUI font settings. Leave blank for default, "local" for local fonts, or a font URL. font-set: "local" ngrok: # 是否启用 ngrok 内网穿透隧道 # Whether to enable ngrok tunnel enabled: false # ngrok 认证令牌 (Authtoken) # ngrok authentication token auth-token: "" # ngrok 自定义域名 (需付费计划支持) # ngrok custom domain (restricted to paid plans) domain: "" cloudflare: # 是否启用 Cloudflare Tunnel (穿透) # Whether to enable Cloudflare Tunnel enabled: false # Cloudflare Tunnel 访问令牌 (Token) # Cloudflare Tunnel access token token: "" # 是否开启 Cloudflare 隧道相关的详细日志 # Whether to enable detailed logs for Cloudflare Tunnel log-enabled: false ================================================ FILE: docker/Dockerfile ================================================ FROM woahbase/alpine-glibc:latest MAINTAINER HaierKeys ARG TARGETOS ARG TARGETARCH ARG VERSION ARG BUILD_DATE ARG GIT_COMMIT ARG VERSION=${VERSION} ARG BUILD_DATE=${BUILD_DATE} ARG GIT_COMMIT=${GIT_COMMIT} LABEL name="fast-note-sync-service" LABEL version=${VERSION} LABEL description="Provide image resizing, cropping, upload/download, and cloud storage features for Obsidian CIAU." LABEL maintainer="HaierKeys " LABEL org.opencontainers.image.title="Fast Note Sync Service" LABEL org.opencontainers.image.created=${BUILD_DATE} LABEL org.opencontainers.image.authors="HaierKeys " LABEL org.opencontainers.image.version=${VERSION} LABEL org.opencontainers.image.description="Provide image resizing, cropping, upload/download, and cloud storage features for Obsidian CIAU." LABEL org.opencontainers.image.url="https://github.com/haierkeys/fast-note-sync-service" LABEL org.opencontainers.image.source="https://github.com/haierkeys/fast-note-sync-service" LABEL org.opencontainers.image.documentation="https://raw.githubusercontent.com/haierkeys/fast-note-sync-service/refs/heads/main/README.md" LABEL org.opencontainers.image.revision=${GIT_COMMIT} LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL org.opencontainers.image.vendor="HaierKeys" ENV TZ=Asia/Shanghai ENV P_NAME=fast-note-sync ENV P_BIN=fast-note-sync-service RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ apk --no-cache add --no-progress libstdc++ curl ca-certificates bash gcompat tzdata && \ cp /usr/share/zoneinfo/${TZ} /etc/localtime && \ echo ${TZ} > /etc/timezone && \ mkdir -p /${P_NAME}/ EXPOSE 9000 9001 VOLUME /${P_NAME}/config VOLUME /${P_NAME}/storage COPY ./build/${TARGETOS}_${TARGETARCH}/${P_BIN} /${P_NAME}/ COPY ./docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh /${P_NAME}/${P_BIN} ENTRYPOINT ["/entrypoint.sh"] ================================================ FILE: docker/docker-compose.yaml ================================================ services: fast-note-sync-service: image: haierkeys/fast-note-sync-service:latest container_name: fast-note-sync-service ports: - "9000:9000" - "9001:9001" volumes: - /data/fast-note-sync/storage/:/fast-note-sync/storage/ - /data/fast-note-sync/config/:/fast-note-sync/config/ networks: - app-network # 与 image-api 在同一网络1 ================================================ FILE: docker/docker_image_clean.sh ================================================ #!/bin/sh echo "docker images clean shell" projectName=$(basename "$(pwd)") # 删除匹配 repository 名称(任意 tag)的镜像 dockerrm=$(docker images --filter "reference=${projectName}:*" -q | sort -u) if [ -n "$dockerrm" ]; then echo "$dockerrm" | xargs -r docker rmi -f echo "docker images ${projectName} clean OK" fi # 删除 dangling (none) 镜像 dockerrm=$(docker images -f "dangling=true" -q | sort -u) if [ -n "$dockerrm" ]; then echo "$dockerrm" | xargs -r docker rmi -f echo "docker images none clean OK" fi ================================================ FILE: docker/docker_redeploy.sh ================================================ #!/bin/bash ProjectRegistry="registry.cn-shanghai.aliyuncs.com/xxx/xxxxx" ProjectPath=`pwd` ProjectName="xxxxx" Usage() { echo "Usage:" echo "test.sh [-t Git tag]" echo "Description:" exit } while getopts ':t:h:' OPT; do case $OPT in t) TAG="$OPTARG";; h) Usage;; ?) Usage;; esac done if [ ${TAG} ];then docker pull $ProjectRegistry:$TAG echo "Stop "$ProjectName docker stop $ProjectName #docker rm -v docker rm -f $ProjectName echo "Start new xxxxx" docker run -tid --name $ProjectName \ -p 8000:8000 -p 8001:8001 -p 8002:8002 \ -v $ProjectPath/storage/:/api/storage/ \ -v $ProjectPath/configs/:/api/configs/ \ $ProjectRegistry:$TAG fi ================================================ FILE: docker/entrypoint.sh ================================================ #!/bin/sh # 检查环境变量 if [ -z "$P_NAME" ] || [ -z "$P_BIN" ]; then echo "Error: P_NAME or P_BIN not set" exit 1 fi # 切换目录 cd "/${P_NAME}/" || { echo "Failed to cd to /${P_NAME}/"; exit 1; } # 创建日志目录和文件 mkdir -p storage/logs || { echo "Failed to create logs dir"; exit 1; } touch storage/logs/c.log || { echo "Failed to create c.log"; exit 1; } # 备份旧日志,统一后缀为 .log mv storage/logs/c.log "storage/logs/c_$(date '+%Y%m%d%H%M%S').log" || { echo "Failed to rename log"; exit 1; } # 运行程序并记录日志到 c.log "/${P_NAME}/${P_BIN}" run 2>&1 | tee storage/logs/c.log ================================================ FILE: docs/API-EXTENSIONS.md ================================================ # API Extensions for Fast Note Sync Service This document describes the new API endpoints added in the `feature/api-extensions` branch. ## New Endpoints ### Health Check ``` GET /api/health ``` Returns server health status including database connectivity and uptime. **Response:** ```json { "status": "healthy", "version": "1.11.1", "uptime": 123.45, "database": "connected" } ``` ### Note Operations #### Patch Frontmatter ``` PATCH /api/note/frontmatter?vault=&path= ``` Update or remove YAML frontmatter fields without modifying note body. **Body:** ```json { "updates": {"title": "New Title", "tags": ["a", "b"]}, "remove": ["oldField"] } ``` #### Append Content ``` POST /api/note/append?vault=&path= ``` Append content to the end of a note. **Body:** ```json { "content": "\n\n## New Section\nAppended content" } ``` #### Prepend Content ``` POST /api/note/prepend?vault=&path= ``` Prepend content after frontmatter (if present) or at the beginning. **Body:** ```json { "content": "Prepended content\n\n" } ``` #### Find and Replace ``` POST /api/note/replace?vault=&path= ``` Find and replace text in a note. Supports regex. **Body:** ```json { "find": "old text", "replace": "new text", "regex": false, "all": true, "failIfNoMatch": false } ``` **Response includes `matchCount`:** ```json { "matchCount": 3, "note": { ... } } ``` #### Move Note ``` POST /api/note/move?vault=&path= ``` Move/rename a note to a new path. **Body:** ```json { "destination": "new/path/note.md", "overwrite": false } ``` ### Link Operations #### Get Backlinks ``` GET /api/note/backlinks?vault=&path= ``` Get all notes that link TO this note. **Response:** ```json { "data": [ { "path": "other-note.md", "linkText": "alias", "context": "...surrounding text [[link]]..." } ] } ``` #### Get Outlinks ``` GET /api/note/outlinks?vault=&path= ``` Get all links FROM this note. **Response:** ```json { "data": [ { "path": "target-note", "linkText": "display text", "context": "...[[target-note|display text]]..." } ] } ``` ### Note Creation #### Create Only (Don't Update) ``` POST /api/note?vault=&path= ``` With `createOnly: true`, returns error 430 if note already exists. **Body:** ```json { "content": "...", "createOnly": true } ``` ## New Error Codes | Code | Message | |------|---------| | 460 | Destination note already exists | | 461 | No match found | | 462 | Invalid regex pattern | ## Configuration New config option in `config.yaml` (also editable via Admin Settings API): ```yaml app: default-api-folder: "" # Optional: prepend this folder to note paths without / ``` ## Known Limitations ### Backlinks 1. **Exact path matching only**: Backlinks match the stored link path exactly. Obsidian's "shortest path when possible" resolution is not fully replicated server-side. 2. **Extension handling**: Links are stored without `.md` extension (e.g., `[[Note1]]` stores "Note1"). Queries with full paths (e.g., "Note1.md") are normalized by stripping `.md`. 3. **Heading anchors**: Links with `#heading` (e.g., `[[note#section]]`) are stored as-is. Querying backlinks for "note.md" won't find links to "note#section". 4. **Relative paths**: Links using relative paths (e.g., `[[../folder/note]]`) are stored as-is. Cross-folder resolution is not performed. ### Link Indexing - Links are indexed when notes are saved via the API - Existing notes need to be re-saved to populate the link index - Link parsing uses regex: `\[\[([^\]|]+)(?:\|([^\]]+))?\]\]` ### Version History - Version numbers increment on each content change - History entries are created asynchronously (with delay) - Move operations migrate history from source to destination note ## Testing Run the test scripts: ```bash # Basic API tests (41 tests) ./test-api.sh # Edge case tests ./test-edge-cases.sh ``` ## Files Changed ### New Files - `internal/routers/api_router/handler_health.go` - `internal/domain/domain_note_link.go` - `internal/model/note_link.gen.go` - `internal/dao/note_link_repository.go` - `internal/service/note_link_service.go` - `pkg/util/frontmatter.go` - `pkg/util/link_parser.go` - `pkg/util/path.go` - `test-api.sh` - `test-edge-cases.sh` ### Modified Files - `config/config.yaml` - `internal/app/app.go` - `internal/app/config.go` - `internal/dao/note_repository.go` - `internal/domain/repository.go` - `internal/dto/note_dto.go` - `internal/model/model.go` - `internal/routers/api_router/handler_note.go` - `internal/routers/router.go` - `internal/service/note_service.go` - `pkg/code/common.go` ================================================ FILE: docs/CHANGELOG.en.md ================================================ # CHANGELOG All notable changes to this project will be documented in this file. The project adheres to [Keep a Changelog](https://keepachangelog.com/en/0.3.0/) guidelines. --- ## v1.16.2 > *2026/02/14* ### 🚀 Optimized - **WebGui**: Adjusted WebGui interface position. - **Features**: Added display of server information. - **Performance**: Optimized list height for zero-copy access to fix issues with low display height in various lists. - **Sync**: Optimized note/attachment sync logic. --- ## v1.16.1 > *2026/02/14* ### 🚀 Optimized - **WebGui**: Adjusted WebGui interface position. - **Features**: Added display of server information. --- ## v1.15.11 > *2026/02/14* ### 🚀 Optimized - **WebGui**: Optimized WebGui interface and added URL support. --- ## v1.15.10 > *2026/02/14* ### 🚀 Optimized - **Architecture**: Adjusted service toolkit. - **API**: Adjusted API response structure. --- ## v1.15.9 > *2026/02/14* ### ✨ Added - **Tools**: Added access entry for fns docs and ws debug tools. --- ## v1.15.8 > *2026/02/13* ### 🛠️ Fixed - **Stability**: Fixed minor BUG in time processing. --- ## v1.15.7 > *2026/02/13* ### 🛠️ Fixed - **Sync**: Fixed issue with offline deletion not clearing local hash table. --- ## v1.15.6 > *2026/02/13* ### 🛠️ Fixed - **Scripts**: Fixed fns shortcut script running issue on macOS. - **Logging**: Fixed log printing content. --- ## v1.15.5 > *2026/02/12* ### 🚀 Optimized - **CI/CD**: Adjusted GitHub Action to use go mod version for building and publishing. --- ## v1.15.4 > *2026/02/12* ### ✨ Added - **Sync**: Added feature to clear note configuration related messages. --- ## v1.15.3 > *2026/02/10* ### 🛠️ Fixed - **Folder**: Added fallback solution for duplicate folders and startup task to clear duplicates. --- ## v1.15.2 > *2026/02/09* ### 🚀 Optimized - **Database**: Optimized DB performance and structure, performed batch formatting. --- ## v1.15.1 > *2026/02/07* ### ✨ Added - **Folder**: Added folder management features, including models and related logic. - **Sync**: Fixed potential data race issues and optimized note/attachment renaming. --- ## v1.14.1 > *2026/01/31* ### ✨ Added - **Trash**: Added trash and batch recovery for attachment management. ### 🛠️ Fixed - **Stability**: Fixed issue where resources were not created correctly due to identical modified time and content in attachments/config files. ### 🚀 Optimized - **API**: Optimized attachment view/download interfaces with zero-copy access. - **WebGui**: Fixed low display height issues in various lists. --- ## v1.14.0 > *2026/01/31* ### ✨ Added - **Trash**: Added trash for attachment management. - **WebGui**: Added display of server information. - **Sync**: Added note and attachment renaming features. ### 🛠️ Fixed - **Stability**: Fixed potential data race issues. --- ## v1.13.0 > *2026/01/30* ### ✨ Added - **Sync**: Added offline deletion synchronization for attachments, notes, and configs. - **Sync**: Added auto-download of missing files in incremental sync mode. --- ## v1.12.0 > *2026/01/29* ### 🚀 Optimized - **Language**: Translated/updated all code comments and documentation to bilingual (CN/EN) or English. - **API**: Improved internationalization (i18n) for API response messages. - **Stability**: Fixed automatic resource prefix issues. - **API**: Added API extensions: edit operations, backlinks, and health checks. --- ## v1.11.3 > *2026/01/27* ### 🛠️ Fixed - **Attachment**: Fixed attachment download timeout (30s) error; now configurable, default is 1 hour. --- ## v1.11.2 > *2026/01/27* ### ✨ Added - **WebGui**: Added Obsidian SSO auto-authorization mechanism. ### 🚀 Optimized - **WebGui**: Improved authorization configuration UI. --- ## v1.11.1 > *2026/01/26* ### 🚀 Optimized - **Release**: Adjusted version release workflow. --- ## v1.11.0 > *2026/01/26* ### ✨ Added - **Feature**: Added version detection and version information retrieval features. --- ## v1.10.8 > *2026/01/26* ### ✨ Added - **API**: Added attachment status detection interface. --- ## v1.10.7 > *2026/01/25* ### 🛠️ Fixed - **Stability**: Fixed server crash caused by consistency checks during file uploads. --- ## v1.10.6 > *2026/01/24* ### ✨ Added - **WebGui**: Added pagination for the attachment management page. --- ## v1.10.5 > *2026/01/23* ### 🛠️ Fixed - **Trash**: Fixed issues when restoring notes/versions from the trash and history. --- ## v1.10.4 > *2026/01/23* ### 🛠️ Fixed - **Attachment**: Fixed connection drops during attachment uploads and lowered error logging level for shard upload failures. --- ## v1.10.3 > *2026/01/20* ### 🚀 Optimized - **WebGui**: Replaced zoom effect in note vault list with a selected shadow effect. ### 🛠️ Fixed - **WebGui**: Fixed a bug where note vaults with special characters in their names were inaccessible. --- ## v1.10.2 > *2026/01/20* ### 🛠️ Fixed - **Admin**: Fixed bugs preventing new user registration and the ability to disable user registration. --- ## v1.10.1 > *2026/01/20* ### 🛠️ Fixed - **Admin**: Fixed issues with new user registration. --- ## v1.10.0 > *2026/01/19* ### ✨ Added - **Attachment**: Added attachment management functionality. - **Auth**: Added configuration for Token expiration time. - **Share**: Added interfaces for sharing functionality. - **Docs**: Added Swagger API documentation. ### 🚀 Optimized - **WebGui**: Adjusted WebGui deployment path. - **API**: Refined API error messages. ### 🛠️ Fixed - **WebGui**: Fixed notice issues caused by WebGui auto-translation. --- ## v1.9.1 > *2026/01/14* ### 🚀 Optimized - **WebGui**: Added blue color scheme and optimized editor display. --- ## v1.9.0 > *2026/01/14* ### ✨ Added - **WebGui**: Complete UI refactor (contributed by @ZyphrZero). - **WebGui**: Replaced editor with Vditor, supporting rich text and Markdown real-time rendering. - **WebGui**: Supported custom note search, list field sorting, and color themes. - **WebGui**: Added dark mode, online version detection, and trash restoration. - **Settings**: Added historical version retention and save delay settings. ### 🚀 Optimized - **Security**: Optimized service token encryption obfuscation characters. --- ## v1.8.1 > *2026/01/12* ### 🔄 Changed - **Architecture**: Introduced DDD layered architecture (contributed by @ZyphrZero), removed global variables, and implemented Dependency Injection pattern. ### 🚀 Optimized - **Sync**: Optimized offline note merging with line-level conflict detection and 3-way merge. - **Performance**: Added Worker Pool and Per-User Write Queue to solve SQLite concurrency lock issues. - **WebSocket**: Optimized Context lifecycle management and enhanced TraceID tracking. ### 🛠️ Fixed - **Logic**: Fixed a bug where note renaming could lead to note loss and errors. --- ## v1.7.3 > *2026/01/09* ### 🛠️ Fixed - **Database**: Added友好 error message for database creation failures. --- ## v1.7.2 > *2026/01/09* ### ✨ Added - **WebGui**: Added configuration settings functionality and related interfaces. - **Admin**: Added Admin ID setting. --- ## v1.7.1 > *2026/01/09* ### ✨ Added - **Sync**: Added offline device note editing merge functionality (requires plugin v1.7+). --- ## v1.6.3 > *2026/01/08* ### 🚀 Optimized - **WebGui**: Optimized note list search. - **WebGui**: Added icon display. - **WebGui**: Added attachment display and refresh button in note vault. ### 🛠️ Fixed - **Stability**: Fixed potential exceptions during concurrent queries. --- ## v1.6.1 > *2026/01/07* ### 🚀 Optimized - **Performance**: Optimized sync efficiency and data processing for large note vaults (requires plugin v1.6+). - **Cache**: Added browser caching mechanism for static content. > [!CAUTION] > This version involves database structure optimization. It is recommended to delete the DB file under `storage/database` on the server; note modification history will be regenerated. --- ## v1.5.4 > *2026/01/06* ### 🛠️ Fixed - **Attachment**: Fixed occasional errors when uploading attachments. --- ## v1.5.3 > *2026/01/06* ### 🚀 Optimized - **WebGui**: Lazy-loaded editing features to improve home page loading speed. --- ## v1.5.2 > *2026/01/05* ### 🛠️ Fixed - **Sync**: Fixed inaccurate sync task progress display. --- ## v1.5.1 > *2026/01/04* ### 🛠️ Fixed - **Logic**: Fixed a bug where notes couldn't be deleted properly after renaming. - **Stability**: Fixed WebSocket connection resets during large-scale note synchronization. - **i18n**: Fixed WebGui API language errors. --- ## v1.5.0 > *2026/01/04* ### ✨ Added - **Trash**: Added note trash bin feature. - **WebGui**: Added user status detection. - **WebGui**: Added registration closed detection on the sign-up page. - **WebGui**: Added keyboard shortcut support for operation confirmations. ### 🚀 Optimized - **WebGui**: Improved note editing user experience. - **Database**: Optimized and resolved database concurrent access issues. ### 🛠️ Fixed - **Script**: Fixed a bug where shortcut scripts might overwrite configuration files. --- ## v1.4.7 > *2026/01/03* ### 🛠️ Fixed - **Database**: Attempted to solve SQLite concurrency issues and corrected internal error codes. --- ## v1.4.6 > *2026/01/03* ### 🛠️ Fixed - **Docker**: Fixed an issue where the `temp` directory did not exist in Docker environments. --- ## v1.4.5 > *2026/01/03* ### 🛠️ Fixed - **Sync**: Fixed an issue where attachments couldn't be synced during initial or full sync (requires plugin v1.5.14+). --- ## v1.4.4 > *2026/01/02* ### 🛠️ Fixed - **Access**: Fixed accessibility issues with titles containing Emojis. ### ✨ Added - **Docs**: Added help file. --- ## v1.4.3 > *2026/01/02* ### 🔄 Changed - **Vault**: Note vault deletion operation changed to soft delete. --- ## v1.4.2 > *2026/01/01* ### ✨ Added - **WebGui**: Added a red confirmation popup for note deletions to prevent accidental deletion. --- ## v1.4.1 > *2025/12/31* ### 🚀 Optimized - **API**: Added ETag browser caching for note resource (images, etc.) download interface to improve loading speed. --- ## v1.4.0 > *2025/12/31* ### ✨ Added - **WebGui**: Added maximize button to enhance full-screen editing experience. - **WebGui**: Supported display of Obsidian embedded images, PDFs, and other attachments in note view. - **API**: Added resource download interface. --- ## v1.3.8 > *2025/12/31* ### 🚀 Optimized - **Server**: Established a content hash version repository for notes to facilitate future tracing, comparison, and merging. --- ## v1.3.7 > *2025/12/30* ### 🛠️ Fixed - **Stability**: Added panic recovery for tasks and upgrade scripts to prevent service crashes. - **Stability**: Fixed Nil Pointer Panic issues in various layers. --- ## v1.3.6 > *2025/12/30* ### 🛠️ Fixed - **Task Management**: Fixed errors in the task manager. --- ## v1.3.5 > *2025/12/30* ### 🚀 Optimized - **WebGui**: Optimized note viewing display. - **Script**: Optimized one-click installation/management script. --- ## v1.3.4 > *2025/12/30* ### 🛠️ Fixed - **Sync**: Fixed sync command processing errors leading to incorrect file synchronization across clients. - **Script**: Fixed one-click scripts closing the service upon `Ctrl+C`. --- ## v1.3.3 > *2025/12/29* ### 🛠️ Fixed - **Sync**: Resolved potential update confusion across multiple note vaults for a single user. --- ## v1.3.2 > *2025/12/28* ### ✨ Added - **i18n**: Added support for multi-language environments. ### 🚀 Optimized - **WebGui**: Optimized note version diff display. --- ## v1.3.1 > *2025/12/28* ### 🚀 Optimized - **Logic**: Optimized logic for note title modification. --- ## v1.3.0 > *2025/12/28* ### ✨ Added - **WebGui**: Added setting for users to control WebGui font settings. --- ## v1.2.6 > *2025/12/27* ### 🚀 Optimized - **WebGui**: Optimized font loading logic to avoid UI stuttering. --- ## v1.2.5 > *2025/12/27* ### ✨ Added - **Client**: Added record support for client names. ### 🚀 Optimized - **Cleanup**: Added sync cleanup logic after note renaming. --- ## v1.2.4 > *2025/12/27* ### 🛠️ Fixed - **WebGui**: Fixed display bug when history version content is empty. --- ## v1.2.3 > *2025/12/27* ### ✨ Added - **API**: Added note history related interfaces and functions. ### 🚀 Optimized - **Database**: Optimized database query efficiency. - **WebGui**: Changed WebGui display font and fixed various display bugs. ### 🛠️ Fixed - **Stability**: Fixed issues during high concurrent access. --- ## v1.2.2 > *2025/12/27* ### 🛠️ Fixed - **WebGui**: Fixed blank page issues caused by empty note history. --- ## v1.2.1 > *2025/12/27* ### ✨ Added - **API**: Added note history related interfaces and functions. ### 🚀 Optimized - **Database**: Optimized database query efficiency. - **Stability**: Resolved stability issues during high concurrent access. --- ## v1.0.4 > *2025/12/26* ### 🛠️ Fixed - **WebGui**: Fixed blank display issues caused by WebGui build exceptions. --- ## v1.0.3 > *2025/12/25* ### 🛠️ Fixed - **WebGui**: Resolved layout issues caused by long note titles. --- ## v1.0.2 > *2025/12/25* ### 🚀 Optimized - **Attachment**: Optimized attachment upload logic, significantly reducing upload time. ### 🛠️ Fixed - **CI/CD**: Corrected GitHub Action update limits. --- ## v1.0.1 > *2025/12/23* ### 🛠️ Fixed - **Permission**: Fixed permission issues during upload on some systems. --- ## v1.0.0 > *2025/12/22* ### ✨ Added - **Sync**: Added configuration file synchronization features and interfaces. ### 🚀 Optimized - **Script**: Optimized script output display. ### 🛠️ Fixed - **Script**: Fixed script execution control failures. --- ## v0.11.5 > *2025/12/19* ### 🛠️ Fixed - **Docker**: Fixed Docker image execution issues. --- ## v0.11.4 > *2025/12/18* ### ✨ Added - **Auth**: Added version information downlink in the authorization validation interface. --- ## v0.11.3 > *2025/12/16* ### ✨ Added - **Cleanup**: Added auto-cleanup tasks on startup and Session auto-cleanup logic. ### 🛠️ Fixed - **Stability**: Fixed abnormal exit issues during high concurrency due to connection closures. --- ## v0.11.2 > *2025/12/15* ### 🛠️ Fixed - **Stability**: Fixed abnormal exit issues during concurrency due to connection closures. --- ## v0.11.1 > *2025/12/14* ### ✨ Added - **Architecture**: Added prefix to messages for future business expansion. --- ## v0.10.2 > *2025/12/12* ### ✨ Added - **Settings**: Added shard settings for upload/download (default 512KB). --- ## v0.10.1 > *2025/12/12* ### ✨ Added - **Feature**: Added binary file download feature. - **Feature**: Added WebSocket chunked download feature. - **Feature**: Added version control management. --- ## v0.9.6 > *2025/12/11* - Initial release (recording started). ================================================ FILE: docs/CHANGELOG.ja.md ================================================ # 更新履歴 (CHANGELOG) このプロジェクトのすべての重要な変更がこのファイルに記録されます。 このプロジェクトは [Keep a Changelog](https://keepachangelog.com/ja/0.3.0/) 規範に従っています。 --- ## v1.16.2 > *2026/02/14* ### 🚀 改善 - **WebGui**: WebGui インターフェースの位置を調整。 - **機能**: サーバー情報の表示を追加。 - **パフォーマンス**: 各種リストの表示高さが低すぎる問題に対し、ゼロコピーアクセスによる最適化を実施。 - **同期**: ノート/添付ファイルの同期ロジックを最適化。 --- ## v1.16.1 > *2026/02/14* ### 🚀 改善 - **WebGui**: WebGui インターフェースの位置を調整。 - **機能**: サーバー情報の表示を追加。 --- ## v1.15.11 > *2026/02/14* ### 🚀 改善 - **WebGui**: WebGui インターフェースを最適化し、URL サポートを追加。 --- ## v1.15.10 > *2026/02/14* ### 🚀 改善 - **アーキテクチャ**: サービスツールセットを調整。 - **API**: インターフェースのレスポンス構造を調整。 --- ## v1.15.9 > *2026/02/14* ### ✨ 新機能 - **ツール**: fns に docs および ws デバッグツールのアクセスエントリを追加。 --- ## v1.15.8 > *2026/02/13* ### 🛠️ 修正 - **安定性**: 時間処理の軽微なバグを修正。 --- ## v1.15.7 > *2026/02/13* ### 🛠️ 修正 - **同期**: オフライン削除時にローカルハッシュテーブルがクリアされない問題を修正。 --- ## v1.15.6 > *2026/02/13* ### 🛠️ 修正 - **スクリプト**: macOS 下での fns ショートカットスクリプトの実行問題を修正。 - **ログ**: ログ出力内容を修正。 --- ## v1.15.5 > *2026/02/12* ### 🚀 改善 - **CI/CD**: GitHub Action で go mod バージョンを使用してビルド・リリースするように調整。 --- ## v1.15.4 > *2026/02/12* ### ✨ 新機能 - **同期**: ノート設定関連メッセージのクリア機能を追加。 --- ## v1.15.3 > *2026/02/10* ### 🛠️ 修正 - **ディレクトリ**: 重複ディレクトリに対する回避策を追加し、起動時に重複ディレクトリをクリーンアップする機能を追加。 --- ## v1.15.2 > *2026/02/09* ### 🚀 改善 - **データベース**: DB のパフォーマンスと構造を最適化し、一括フォーマットを実施。 --- ## v1.15.1 > *2026/02/07* ### ✨ 新機能 - **ディレクトリ**: ディレクトリ(Folder)管理機能を追加(Model および関連ロジックを含む)。 - **同期**: 潜在的なデータ競合問題を修正し、ノートと添付ファイルのリネーム機能を最適化。 --- ## v1.14.1 > *2026/01/31* ### ✨ 新機能 - **ゴミ箱**: 添付ファイル管理でゴミ箱および一括復元機能をサポート。 ### 🛠️ 修正 - **安定性**: 添付ファイル/設定ファイルで更新時間と内容が一致する場合にリソースが正しく作成されない問題を修正。 ### 🚀 改善 - **API**: 添付ファイルの閲覧/ダウンロードインターフェースをゼロコピーアクセスに最適化。 - **WebGui**: 各種リストの表示高さが低すぎる問題を最適化。 --- ## v1.14.0 > *2026/01/31* ### ✨ 新機能 - **ゴミ箱**: 添付ファイル管理にゴミ箱を追加。 - **WebGui**: サーバー情報の表示を追加。 - **同期**: ノートと添付ファイルのリネーム機能を追加。 ### 🛠️ 修正 - **安定性**: 潜在的なデータ競合問題を修正。 --- ## v1.13.0 > *2026/01/30* ### ✨ 新機能 - **同期**: 添付ファイル、ノート、設定のオフライン削除同期機能を追加。 - **同期**: 増分同期モードで欠落ファイルの自動ダウンロード機能を追加。 --- ## v1.12.0 > *2026/01/29* ### 🚀 改善 - **言語**: すべてのコードコメントとドキュメントを中英併記または英語に統一翻訳/更新。 - **API**: APIレスポンスメッセージの国際化(i18n)対応を改善。 - **安定性**: 自動リソースプレフィックスの問題を修正。 - **API**: API拡張を追加:編集操作、バックリンク(Backlinks)、ヘルスチェック。 --- ## v1.11.3 > *2026/01/27* ### 🛠️ 修正 - **添付ファイル**: 添付ファイルダウンロードのタイムアウト(30秒)エラーを修正。設定可能になり、デフォルトは1時間。 --- ## v1.11.2 > *2026/01/27* ### ✨ 新機能 - **WebGui**: Obsidian SSO自動認証メカニズムを追加。 ### 🚀 改善 - **WebGui**: 認証設定インターフェースのUIを改善。 --- ## v1.11.1 > *2026/01/26* ### 🚀 改善 - **リリース**: バージョンリリースワークフローを調整。 --- ## v1.11.0 > *2026/01/26* ### ✨ 新機能 - **機能**: バージョン検出およびバージョン情報取得機能を追加。 --- ## v1.10.8 > *2026/01/26* ### ✨ 新機能 - **API**: 添付ファイル状態検出インターフェースを追加。 --- ## v1.10.7 > *2026/01/25* ### 🛠️ 修正 - **安定性**: ファイルアップロード時の整合性チェックによるサーバークラッシュを修正。 --- ## v1.10.6 > *2026/01/24* ### ✨ 新機能 - **WebGui**: 添付ファイル管理ページにページネーションを追加。 --- ## v1.10.5 > *2026/01/23* ### 🛠️ 修正 - **ゴミ箱**: ゴミ箱および履歴バージョンからのノート/バージョンの復元に関する問題を修正。 --- ## v1.10.4 > *2026/01/23* ### 🛠️ 修正 - **添付ファイル**: 添付ファイルアップロード中のネットワーク切断による異常を修正し、エラーログレベルを下げました。 --- ## v1.10.3 > *2026/01/20* ### 🚀 改善 - **WebGui**: ノートリポジトリリストのズーム効果を、選択時のシャドウ効果に変更。 ### 🛠️ 修正 - **WebGui**: ノートリポジトリ名に特殊文字が含まれる場合にアクセスできないバグを修正。 --- ## v1.10.2 > *2026/01/20* ### 🛠️ 修正 - **管理**: 新規ユーザー登録およびユーザー登録無効化設定が機能しないバグを修正。 --- ## v1.10.1 > *2026/01/20* ### 🛠️ 修正 - **管理**: 新規ユーザー登録の問題を修正。 --- ## v1.10.0 > *2026/01/19* ### ✨ 新機能 - **添付ファイル**: 添付ファイル管理機能を追加。 - **認証**: Token有効期限の設定を追加。 - **共有**: 共有機能関連のインターフェースを追加。 - **ドキュメント**: Swagger APIドキュメントを追加。 ### 🚀 改善 - **WebGui**: WebGuiのデプロイパスを調整。 - **API**: APIエラーメッセージを詳細化。 ### 🛠️ 修正 - **WebGui**: WebGui自動翻訳による通知の問題を修正。 --- ## v1.9.1 > *2026/01/14* ### 🚀 改善 - **WebGui**: ブルー配色スキームを追加し、エディタの表示を最適化。 --- ## v1.9.0 > *2026/01/14* ### ✨ 新機能 - **WebGui**: UIを全面刷新(@ZyphrZero による貢献)。 - **WebGui**: エディタをVditorに変更し、リッチテキストとMarkdownのリアルタイムレンダリングをサポート。 - **WebGui**: カスタムノート検索、リスト項目のソート、カラーテーマをサポート。 - **WebGui**: ダークモード、オンラインバージョン検出、ゴミ箱からの復元機能を追加。 - **設定**: 履歴バージョンの保持数および保存遅延設定を追加。 ### 🚀 改善 - **セキュリティ**: サービスプロークンの暗号化難読化文字を最適化。 --- ## v1.8.1 > *2026/01/12* ### 🔄 変更 - **アーキテクチャ**: DDDレイヤードアーキテクチャを導入(@ZyphrZero による貢献)、グローバル変数を削除し、依存性注入(DI)パターンを実装。 ### 🚀 改善 - **同期**: オフラインノートのマージを最適化し、行レベルの競合検出と3-wayマージを実現。 - **パフォーマンス**: Worker PoolとPer-User Write Queueを追加し、SQLiteの並行書き込みロック問題を解決。 - **WebSocket**: Contextのライフサイクル管理を最適化し、TraceIDの追跡能力を強化。 ### 🛠️ 修正 - **ロジック**: ノートの名称変更によるノートの紛失およびエラーの問題を修正。 --- ## v1.7.3 > *2026/01/09* ### 🛠️ 修正 - **データベース**: データベース作成失敗時のフレンドリーなエラー表示を追加。 --- ## v1.7.2 > *2026/01/09* ### ✨ 新機能 - **WebGui**: 設定機能および関連インターフェースを追加。 - **管理**: 管理者ID設定を追加。 --- ## v1.7.1 > *2026/01/09* ### ✨ 新機能 - **同期**: オフラインデバイスのノート編集マージ機能を追加(プラグインv1.7+が必要)。 --- ## v1.6.3 > *2026/01/08* ### 🚀 改善 - **WebGui**: ノートリストの検索機能を最適化。 - **WebGui**: アイコン表示を追加。 - **WebGui**: ノートリポジトリに添付ファイル表示と更新ボタンを追加。 ### 🛠️ 修正 - **安定性**: 並行クエリ時の潜在的な例外を修正。 --- ## v1.6.1 > *2026/01/07* ### 🚀 改善 - **パフォーマンス**: 大規模なノートリポジトリの同期効率とデータ処理を最適化(プラグインv1.6+が必要)。 - **キャッシュ**: 静的コンテンツにブラウザキャッシュメカニズムを追加。 > [!CAUTION] > このバージョンではデータベース構造の最適化が行われています。サーバー上の `storage/database` にあるDBファイルを削除することをお勧めします。ノートの変更履歴は再生成されます。 --- ## v1.5.4 > *2026/01/06* ### 🛠️ 修正 - **添付ファイル**: 添付ファイルのアップロード時に稀に発生するエラーを修正。 --- ## v1.5.3 > *2026/01/06* ### 🚀 改善 - **WebGui**: 編集機能を遅延読み込みし、ホームページの読み込み速度を向上。 --- ## v1.5.2 > *2026/01/05* ### 🛠️ 修正 - **同期**: 同期タスクの進捗表示が不正確な問題を修正。 --- ## v1.5.1 > *2026/01/04* ### 🛠️ 修正 - **ロジック**: ノートの名称変更後に正常に削除できない問題を修正。 - **安定性**: 大規模なノート同期時にWebSocket接続がリセットされる問題を修正。 - **多言語**: WebGui APIの言語エラーを修正。 --- ## v1.5.0 > *2026/01/04* ### ✨ 新機能 - **ゴミ箱**: ノートゴミ箱機能を追加。 - **WebGui**: ユーザー状態検出を追加。 - **WebGui**: 登録ページに登録受付終了の検出を追加。 - **WebGui**: 操作確認のキーボードショートカットをサポート。 ### 🚀 改善 - **WebGui**: ノート編集のユーザーエクスペリエンスを向上。 - **データベース**: データベースの並行アクセス問題を最適化し解決。 ### 🛠️ 修正 - **スクリプト**: ショートカットスクリプトが設定ファイルを上書きする問題を修正。 --- ## v1.4.7 > *2026/01/03* ### 🛠️ 修正 - **データベース**: SQLiteの並行問題を解決し、内部エラーコードを修正。 --- ## v1.4.6 > *2026/01/03* ### 🛠️ 修正 - **Docker**: Docker環境で `temp` ディレクトリが存在しない問題を修正。 --- ## v1.4.5 > *2026/01/03* ### 🛠️ 修正 - **同期**: 初回同期または全量同期時に添付ファイルが同期されない問題を修正(プラグインv1.5.14+が必要)。 --- ## v1.4.4 > *2026/01/02* ### 🛠️ 修正 - **アクセス**: タイトルにEmojiが含まれる場合のアクセス問題を修正。 ### ✨ 新機能 - **ドキュメント**: ヘルプファイルを追加。 --- ## v1.4.3 > *2026/01/02* ### 🔄 変更 - **リポジトリ**: ノートリポジトリの削除操作を論理削除に変更。 --- ## v1.4.2 > *2026/01/01* ### ✨ 新機能 - **WebGui**: ノート削除時に赤色の確認ポップアップを表示し、誤削除を防止。 --- ## v1.4.1 > *2025/12/31* ### 🚀 改善 - **API**: ノートリソース(画像など)のダウンロードインターフェースにETagブラウザキャッシュを追加し、読み込み速度を向上。 --- ## v1.4.0 > *2025/12/31* ### ✨ 新機能 - **WebGui**: 最大化ボタンを追加し、全画面編集のエクスペリエンスを向上。 - **WebGui**: ノート表示ページでObsidian埋め込み画像、PDF、およびその他の添付ファイルの表示をサポート。 - **API**: リソースダウンロードインターフェースを追加。 --- ## v1.3.8 > *2025/12/31* ### 🚀 改善 - **サーバー**: ノートのコンテンツハッシュバージョン管理を確立し、今後の追跡、比較、マージを容易にしました。 --- ## v1.3.7 > *2025/12/30* ### 🛠️ 修正 - **安定性**: タスクおよびアップグレードスクリプトにリカバリ機能を追加し、サービスダウンを防止。 - **安定性**: 各レイヤーで発生していたNilポインタによるPanicを修正。 --- ## v1.3.6 > *2025/12/30* ### 🛠️ 修正 - **タスク管理**: タスクマネージャーのエラーを修正。 --- ## v1.3.5 > *2025/12/30* ### 🚀 改善 - **WebGui**: ノート表示ページを最適化。 - **スクリプト**: 一件インストール/管理スクリプトを最適化。 --- ## v1.3.4 > *2025/12/30* ### 🛠️ 修正 - **同期**: 同期コマンドの処理エラーによる、不正確なファイル同期の問題を修正。 - **スクリプト**: 一件スクリプトで `Ctrl+C` 時にサービスが一緒に終了する問題を修正。 --- ## v1.3.3 > *2025/12/29* ### 🛠️ 修正 - **同期**: 単一ユーザーによる複数ノートリポジトリ更新時の混乱の問題を解決。 --- ## v1.3.2 > *2025/12/28* ### ✨ 新機能 - **多言語**: 多言語環境のサポートを追加。 ### 🚀 改善 - **WebGui**: ノート履歴の差異表示を最適化。 --- ## v1.3.1 > *2025/12/28* ### 🚀 改善 - **ロジック**: ノートタイトル変更時のロジック処理を最適化。 --- ## v1.3.0 > *2025/12/28* ### ✨ 新機能 - **WebGui**: WebGuiのフォント設定をユーザーが制御できる設定を追加。 --- ## v1.2.6 > *2025/12/27* ### 🚀 改善 - **WebGui**: フォント読み込みロジックを最適化し、UIのスタッタリングを回避。 --- ## v1.2.5 > *2025/12/27* ### ✨ 新機能 - **クライアント**: クライアント名の記録をサポート。 ### 🚀 改善 - **クリーンアップ**: ノートの名称変更後の同期クリーンアップロジックを追加。 --- ## v1.2.4 > *2025/12/27* ### 🛠️ 修正 - **WebGui**: 履歴バージョンが空の場合の表示バグを修正。 --- ## v1.2.3 > *2025/12/27* ### ✨ 新機能 - **API**: ノート履歴関連のインターフェースおよび機能を追加。 ### 🚀 改善 - **データベース**: データベースのクエリ効率を最適化。 - **WebGui**: WebGuiの表示フォントを変更し、各種表示バグを修正。 ### 🛠️ 修正 - **安定性**: 高並行アクセス時の問題を修正。 --- ## v1.2.2 > *2025/12/27* ### 🛠️ 修正 - **WebGui**: ノート履歴が空の場合にページが白くなる問題を修正。 --- ## v1.2.1 > *2025/12/27* ### ✨ 新機能 - **API**: ノート履歴関連のインターフェースおよび機能を追加。 ### 🚀 改善 - **データベース**: データベースのクエリ効率を最適化。 - **安定性**: 高並行アクセス時の安定性問題を解決。 --- ## v1.0.4 > *2025/12/26* ### 🛠️ 修正 - **WebGui**: WebGuiビルド時の例外による表示の問題を修正。 --- ## v1.0.3 > *2025/12/25* ### 🛠️ 修正 - **WebGui**: ノートタイトルが長い場合のレイアウトの問題を修正。 --- ## v1.0.2 > *2025/12/25* ### 🚀 改善 - **添付ファイル**: 添付ファイルのアップロードロジックを最適化し、アップロード時間を大幅に短縮。 ### 🛠️ 修正 - **CI/CD**: GitHub Actionの更新制限を修正。 --- ## v1.0.1 > *2025/12/23* ### 🛠️ 修正 - **権限**: 一部のシステムでアップロード時に権限不足が発生する問題を修正。 --- ## v1.0.0 > *2025/12/22* ### ✨ 新機能 - **同期**: 設定ファイルの同期機能およびインターフェースを追加。 ### 🚀 改善 - **スクリプト**: スクリプトの出力表示を最適化。 ### 🛠️ 修正 - **スクリプト**: スクリプトの実行制御の失敗を修正。 --- ## v0.11.5 > *2025/12/19* ### 🛠️ 修正 - **Docker**: Dockerイメージの実行問題を修正。 --- ## v0.11.4 > *2025/12/18* ### ✨ 新機能 - **認証**: 認証検証インターフェースでバージョン情報の返却を追加。 --- ## v0.11.3 > *2025/12/16* ### ✨ 新機能 - **クリーンアップ**: 起動時の自動クリーンアップタスクおよびSession自動クリーンアップロジックを追加。 ### 🛠️ 修正 - **安定性**: 高並行時の接続切断による異常終了問題を修正。 --- ## v0.11.2 > *2025/12/15* ### 🛠️ 修正 - **安定性**: 並行時の接続切断による異常終了問題を修正。 --- ## v0.11.1 > *2025/12/14* ### ✨ 新機能 - **アーキテクチャ**: メッセージにプレフィックスを追加し、今後の機能拡張を容易に。 --- ## v0.10.2 > *2025/12/12* ### ✨ 新機能 - **設定**: アップロード/ダウンロードのチャンク設定を追加(デフォルト 512KB)。 --- ## v0.10.1 > *2025/12/12* ### ✨ 新機能 - **機能**: バイナリファイルのダウンロード機能を追加。 - **機能**: WebSocketによるチャンクダウンロード機能を追加。 - **機能**: バージョン管理を追加。 --- ## v0.9.6 > *2025/12/11* - 初期バージョン(記録開始)。 ================================================ FILE: docs/CHANGELOG.ko.md ================================================ # 변경 로그 (CHANGELOG) 이 프로젝트의 모든 주요 변경 사항은 이 파일에 기록됩니다. 이 프로젝트는 [Keep a Changelog](https://keepachangelog.com/en/0.3.0/) 규격에 따라 관리됩니다. --- ## v1.16.2 > *2026/02/14* ### 🚀 최적화 - **WebGui**: WebGui 인터페이스 위치 조정. - **기능**: 서버 정보 표시 추가. - **성능**: 각종 리스트 표시 높이가 너무 낮은 문제에 대해 제로 카피(Zero-copy) 접근으로 최적화. - **동기화**: 노트/첨부 파일 동기화 로직 최적화. --- ## v1.16.1 > *2026/02/14* ### 🚀 최적화 - **WebGui**: WebGui 인터페이스 위치 조정. - **기능**: 서버 정보 표시 추가. --- ## v1.15.11 > *2026/02/14* ### 🚀 최적화 - **WebGui**: WebGui 인터페이스 최적화 및 URL 지원 추가. --- ## v1.15.10 > *2026/02/14* ### 🚀 최적화 - **아키텍처**: 서비스 도구 세트 조정. - **API**: 인터페이스 응답 구조 조정. --- ## v1.15.9 > *2026/02/14* ### ✨ 새 기능 - **도구**: fns에 docs 및 ws 디버깅 도구 접근 항목 추가. --- ## v1.15.8 > *2026/02/13* ### 🛠️ 수정 - **안정성**: 시간 처리 관련 경미한 버그 수정. --- ## v1.15.7 > *2026/02/13* ### 🛠️ 수정 - **동기화**: 오프라인 삭제 시 로컬 해시 테이블이 정리되지 않던 문제 수정. --- ## v1.15.6 > *2026/02/13* ### 🛠️ 수정 - **스크립트**: macOS 환경에서 fns 바로가기 스크립트 실행 문제 수정. - **로그**: 로그 출력 내용 수정. --- ## v1.15.5 > *2026/02/12* ### 🚀 최적화 - **CI/CD**: GitHub Action에서 go mod 버전을 사용하여 빌드 및 릴리스하도록 조정. --- ## v1.15.4 > *2026/02/12* ### ✨ 새 기능 - **동기화**: 노트 설정 관련 메시지 정리 기능 추가. --- ## v1.15.3 > *2026/02/10* ### 🛠️ 수정 - **디렉토리**: 중복 디렉토리에 대한 회피책 추가 및 시작 시 중복 디렉토리 정리 기능 추가. --- ## v1.15.2 > *2026/02/09* ### 🚀 최적화 - **데이터베이스**: DB 성능 및 구조 최적화, 일괄 포맷팅 실시. --- ## v1.15.1 > *2026/02/07* ### ✨ 새 기능 - **디렉토리**: 디렉토리(Folder) 관리 기능 추가(Model 및 관련 로직 포함). - **동기화**: 잠재적인 데이터 경합 문제 수정 및 노트/첨부 파일 이름 변경 기능 최적화. --- ## v1.14.1 > *2026/01/31* ### ✨ 새 기능 - **휴지통**: 첨부 파일 관리에서 휴지통 및 일괄 복구 기능 지원. ### 🛠️ 수정 - **안정성**: 첨부 파일/설정 파일 업데이트 시간과 내용이 일치할 때 리소스가 정상적으로 생성되지 않던 문제 수정. ### 🚀 최적화 - **API**: 첨부 파일 조회/다운로드 인터페이스를 제로 카피 접근으로 최적화. - **WebGui**: 각종 리스트 표시 높이가 너무 낮은 문제 최적화. --- ## v1.14.0 > *2026/01/31* ### ✨ 새 기능 - **휴지통**: 첨부 파일 관리에 휴지통 추가. - **WebGui**: 서버 정보 표시 추가. - **동기화**: 노트 및 첨부 파일 이름 변경 기능 추가. ### 🛠️ 수정 - **안정성**: 잠재적인 데이터 경합 문제 수정. --- ## v1.13.0 > *2026/01/30* ### ✨ 새 기능 - **동기화**: 첨부 파일, 노트, 설정의 오프라인 삭제 동기화 기능을 추가했습니다. - **동기화**: 증분 동기화 모드에서 누락된 파일 자동 다운로드 기능을 추가했습니다. --- ## v1.12.0 > *2026/01/29* ### 🚀 최적화 - **언어**: 모든 코드 주석 및 문서를 한/영 병기 또는 영어로 번역/업데이트했습니다. - **API**: API 응답 메시지에 대한 국제화(i18n) 지원을 개선했습니다. - **안정성**: 자동 리소스 접두사 문제를 수정했습니다. - **API**: API 확장 기능 추가: 편집 작업, 백링크(Backlinks), 상태 확인(Health Check). --- ## v1.11.3 > *2026/01/27* ### 🛠️ 수정 - **첨부 파일**: 첨부 파일 다운로드 요청 시 타임아웃(30초) 오류가 발생하던 문제를 수정했습니다. 이제 설정이 가능하며 기본값은 1시간입니다. --- ## v1.11.2 > *2026/01/27* ### ✨ 새 기능 - **WebGui**: Obsidian SSO 자동 인증 메커니즘을 추가했습니다. ### 🚀 최적화 - **WebGui**: 인증 설정 인터페이스 UI를 개선했습니다. --- ## v1.11.1 > *2026/01/26* ### 🚀 최적화 - **배포**: 버전 배포 워크플로우를 조정했습니다. --- ## v1.11.0 > *2026/01/26* ### ✨ 새 기능 - **기능**: 버전 감지 및 버전 정보 가져오기 기능을 추가했습니다. --- ## v1.10.8 > *2026/01/26* ### ✨ 새 기능 - **API**: 첨부 파일 상태 감지 인터페이스를 추가했습니다. --- ## v1.10.7 > *2026/01/25* ### 🛠️ 수정 - **안정성**: 파일 업로드 시 일관성 검사로 인해 서버가 다운되던 문제를 수정했습니다. --- ## v1.10.6 > *2026/01/24* ### ✨ 새 기능 - **WebGui**: 첨부 파일 관리 페이지에 페이지네이션을 추가했습니다. --- ## v1.10.5 > *2026/01/23* ### 🛠️ 수정 - **휴지통**: 휴지통 및 히스토리 버전에서 노트나 버전을 복구할 때 발생하던 문제를 수정했습니다. --- ## v1.10.4 > *2026/01/23* ### 🛠️ 수정 - **첨부 파일**: 첨부 파일 업로드 중 네트워크 연결이 끊길 때의 이상 현상을 수정하고 오류 로그 수준을 낮췄습니다. --- ## v1.10.3 > *2026/01/20* ### 🚀 최적화 - **WebGui**: 노트 보관소 리스트의 확대 효과를 선택 시 그림자 효과로 변경했습니다. ### 🛠️ 수정 - **WebGui**: 노트 보관소 이름에 특수 문자가 포함된 경우 접근할 수 없던 버그를 수정했습니다. --- ## v1.10.2 > *2026/01/20* ### 🛠️ 수정 - **관리**: 새 사용자 등록 및 사용자 등록 비활성화 설정이 작동하지 않던 버그를 수정했습니다. --- ## v1.10.1 > *2026/01/20* ### 🛠️ 수정 - **관리**: 새 사용자 등록 문제를 수정했습니다. --- ## v1.10.0 > *2026/01/19* ### ✨ 새 기능 - **첨부 파일**: 첨부 파일 관리 기능을 추가했습니다. - **인증**: Token 만료 시간 설정을 추가했습니다. - **공유**: 공유 기능 관련 인터페이스를 추가했습니다. - **문서**: Swagger API 문서를 추가했습니다. ### 🚀 최적화 - **WebGui**: WebGui 배포 경로를 조정했습니다. - **API**: API 오류 메시지를 상세화했습니다. ### 🛠️ 수정 - **WebGui**: WebGui 자동 번역으로 인한 알림 문제를 수정했습니다. --- ## v1.9.1 > *2026/01/14* ### 🚀 최적화 - **WebGui**: 블루 색상 테마를 추가하고 에디터 표시 효과를 최적화했습니다. --- ## v1.9.0 > *2026/01/14* ### ✨ 새 기능 - **WebGui**: UI를 전면 개편했습니다 (@ZyphrZero 기여). - **WebGui**: 에디터를 Vditor로 변경하여 리치 텍스트 및 Markdown 실시간 렌더링을 지원합니다. - **WebGui**: 사용자 지정 노트 검색, 리스트 필드 정렬 및 색상 테마를 지원합니다. - **WebGui**: 다크 모드, 온라인 버전 감지 및 휴지통 복구 기능을 추가했습니다. - **설정**: 히스토리 버전 보존 개수 및 저장 지연 설정을 추가했습니다. ### 🚀 최적화 - **보안**: 서비스 토큰 암호화 난독화 문자를 최적화했습니다. --- ## v1.8.1 > *2026/01/12* ### 🔄 변경 - **아키텍처**: DDD 계층형 아키텍처를 도입하고 (@ZyphrZero 기여), 전역 변수를 제거하며 의존성 주입(DI) 패턴을 구현했습니다. ### 🚀 최적화 - **동기화**: 오프라인 노트 병합을 최적화하여 행 단위의 충돌 감지 및 3-way 병합을 구현했습니다. - **성능**: Worker Pool과 Per-User Write Queue를 도입하여 SQLite 동시 쓰기 잠금 문제를 해결했습니다. - **WebSocket**: Context 생명주기 관리를 최적화하고 TraceID 추적 기능을 강화했습니다. ### 🛠️ 수정 - **로직**: 노트 이름 변경으로 인해 노트가 유실되거나 오류가 발생하던 문제를 수정했습니다. --- ## v1.7.3 > *2026/01/09* ### 🛠️ 수정 - **데이터베이스**: 데이터베이스 생성 실패 시 사용자 친화적인 오류 메시지를 추가했습니다. --- ## v1.7.2 > *2026/01/09* ### ✨ 새 기능 - **WebGui**: 설정 기능 및 관련 인터페이스를 추가했습니다. - **관리**: 관리자 ID 설정을 추가했습니다. --- ## v1.7.1 > *2026/01/09* ### ✨ 새 기능 - **동기화**: 오프라인 기기의 노트 편집 병합 기능을 추가했습니다 (플러그인 v1.7+ 필요). --- ## v1.6.3 > *2026/01/08* ### 🚀 최적화 - **WebGui**: 노트 리스트 검색 기능을 최적화했습니다. - **WebGui**: 아이콘 표시를 추가했습니다. - **WebGui**: 노트 보관소에 첨부 파일 표시 및 새로고침 버튼을 추가했습니다. ### 🛠️ 수정 - **안정성**: 동시 쿼리 시 발생할 수 있는 예외 상황을 수정했습니다. --- ## v1.6.1 > *2026/01/07* ### 🚀 최적화 - **성능**: 대규모 노트 보관소의 동기화 효율 및 데이터 처리를 최적화했습니다 (플러그인 v1.6+ 필요). - **캐시**: 정적 콘텐츠에 대해 브라우저 캐싱 메커니즘을 추가했습니다. > [!CAUTION] > 이 버전은 데이터베이스 구조 최적화가 포함되어 있습니다. 서버의 `storage/database` 디렉토리에 있는 DB 파일을 삭제할 것을 권장합니다. 노트 수정 이력은 다시 생성됩니다. --- ## v1.5.4 > *2026/01/06* ### 🛠️ 수정 - **첨부 파일**: 첨부 파일 업로드 시 간헐적으로 발생하던 오류를 수정했습니다. --- ## v1.5.3 > *2026/01/06* ### 🚀 최적화 - **WebGui**: 편집 기능을 지연 로딩하여 홈 페이지 로딩 속도를 향상했습니다. --- ## v1.5.2 > *2026/01/05* ### 🛠️ 수정 - **동기화**: 동기화 작업의 진행률 표시가 부정확하던 문제를 수정했습니다. --- ## v1.5.1 > *2026/01/04* ### 🛠️ 수정 - **로직**: 노트 이름 변경 후 정상적으로 삭제되지 않던 문제를 수정했습니다. - **안정성**: 대규모 노트 동기화 시 WebSocket 연결이 리셋되던 문제를 수정했습니다. - **다국어**: WebGui API 언어 오류를 수정했습니다. --- ## v1.5.0 > *2026/01/04* ### ✨ 새 기능 - **휴지통**: 노트 휴지통 기능을 추가했습니다. - **WebGui**: 사용자 상태 감지 기능을 추가했습니다. - **WebGui**: 가입 페이지에 가입 중단 감지 기능을 추가했습니다. - **WebGui**: 작업 확인을 위한 키보드 단축키 지원을 추가했습니다. ### 🚀 최적화 - **WebGui**: 노트 편집 사용자 경험을 개선했습니다. - **데이터베이스**: 데이터베이스 동시 액세스 문제를 최적화하고 해결했습니다. ### 🛠️ 수정 - **스크립트**: 바로가기 스크립트가 설정 파일을 덮어쓰던 문제를 수정했습니다. --- ## v1.4.7 > *2026/01/03* ### 🛠️ 수정 - **데이터베이스**: SQLite 동시성 문제를 시도하고 내부 오류 코드를 수정했습니다. --- ## v1.4.6 > *2026/01/03* ### 🛠️ 수정 - **Docker**: Docker 환경에서 `temp` 디렉토리가 존재하지 않던 문제를 수정했습니다. --- ## v1.4.5 > *2026/01/03* ### 🛠️ 수정 - **동기화**: 초기 동기화 또는 전체 동기화 시 첨부 파일이 동기화되지 않던 문제를 수정했습니다 (플러그인 v1.5.14+ 필요). --- ## v1.4.4 > *2026/01/02* ### 🛠️ 수정 - **접근**: 제목에 Emoji가 포함된 경우의 접근성 문제를 수정했습니다. ### ✨ 새 기능 - **문서**: 도움말 파일을 추가했습니다. --- ## v1.4.3 > *2026/01/02* ### 🔄 변경 - **보관소**: 노트 보관소 삭제 작업을 소프트 삭제로 변경했습니다. --- ## v1.4.2 > *2026/01/01* ### ✨ 새 기능 - **WebGui**: 노트 삭제 시 실수로 삭제하는 것을 방지하기 위해 빨간색 확인 팝업을 추가했습니다. --- ## v1.4.1 > *2025/12/31* ### 🚀 최적화 - **API**: 노트 리소스(이미지 등) 다운로드 인터페이스에 ETag 브라우저 캐싱을 추가하여 로딩 속도를 개선했습니다. --- ## v1.4.0 > *2025/12/31* ### ✨ 새 기능 - **WebGui**: 전체 화면 편집 경험을 높이기 위해 최대화 버튼을 추가했습니다. - **WebGui**: 노트 보기 페이지에서 Obsidian 임베디드 이미지, PDF 및 기타 첨부 파일의 정상 표시를 지원합니다. - **API**: 리소스 다운로드 인터페이스를 추가했습니다. --- ## v1.3.8 > *2025/12/31* ### 🚀 최적화 - **서버**: 향후 추적, 비교 및 병합이 용이하도록 노트용 콘텐츠 해시 버전 저장소를 구축했습니다. --- ## v1.3.7 > *2025/12/30* ### 🛠️ 수정 - **안정성**: 작업 및 업그레이드 스크립트에 복구 기능을 추가하여 서비스 장애를 방지했습니다. - **안정성**: 각 레이어에서 발생하던 Nil 포인터로 인한 Panic을 수정했습니다. --- ## v1.3.6 > *2025/12/30* ### 🛠️ 수정 - **작업 관리**: 작업 관리자의 오류를 수정했습니다. --- ## v1.3.5 > *2025/12/30* ### 🚀 최적화 - **WebGui**: 노트 보기 표시를 최적화했습니다. - **스크립트**: 원클릭 설치/관리 스크립트를 최적화했습니다. --- ## v1.3.4 > *2025/12/30* ### 🛠️ 수정 - **동기화**: 동기화 명령 처리 오류로 인해 잘못된 파일 동기화가 발생하는 문제를 수정했습니다. - **스크립트**: 원클릭 스크립트에서 `Ctrl+C` 시 서비스가 함께 종료되던 문제를 수정했습니다. --- ## v1.3.3 > *2025/12/29* ### 🛠️ 수정 - **동기화**: 단일 사용자의 여러 노트 보관소 업데이트 시 발생할 수 있는 혼란 문제를 해결했습니다. --- ## v1.3.2 > *2025/12/28* ### ✨ 새 기능 - **다국어**: 다국어 환경 지원을 추가했습니다. ### 🚀 최적화 - **WebGui**: 노트 이력의 차이 표시를 최적화했습니다. --- ## v1.3.1 > *2025/12/28* ### 🚀 최적화 - **로직**: 노트 제목 변경 시의 로직 처리를 최적화했습니다. --- ## v1.3.0 > *2025/12/28* ### ✨ 새 기능 - **WebGui**: 사용자가 WebGui 폰트 설정을 제어할 수 있는 설정을 추가했습니다. --- ## v1.2.6 > *2025/12/27* ### 🚀 최적화 - **WebGui**: UI 끊김 현상을 방지하기 위해 폰트 로딩 로직을 최적화했습니다. --- ## v1.2.5 > *2025/12/27* ### ✨ 새 기능 - **클라이언트**: 클라이언트 이름 기록 지원을 추가했습니다. ### 🚀 최적화 - **정리**: 노트 이름 변경 후의 동기화 정리 로직을 추가했습니다. --- ## v1.2.4 > *2025/12/27* ### 🛠️ 수정 - **WebGui**: 이력 버전 내용이 비어 있을 때의 표시 버그를 수정했습니다. --- ## v1.2.3 > *2025/12/27* ### ✨ 새 기능 - **API**: 노트 이력 관련 인터페이스 및 기능을 추가했습니다. ### 🚀 최적화 - **데이터베이스**: 데이터베이스 쿼리 효율을 최적화했습니다. - **WebGui**: WebGui 표시 폰트를 변경하고 각종 표시 버그를 수정했습니다. ### 🛠️ 수정 - **안정성**: 고동시성 액세스 시의 문제를 수정했습니다. --- ## v1.2.2 > *2025/12/27* ### 🛠️ 수정 - **WebGui**: 노트 이력이 비어 있을 때 페이지가 하얗게 나오던 문제를 수정했습니다. --- ## v1.2.1 > *2025/12/27* ### ✨ 새 기능 - **API**: 노트 이력 관련 인터페이스 및 기능을 추가했습니다. ### 🚀 최적화 - **데이터베이스**: 데이터베이스 쿼리 효율을 최적화했습니다. - **안정성**: 고동시성 액세스 시의 안정성 문제를 해결했습니다. --- ## v1.0.4 > *2025/12/26* ### 🛠️ 수정 - **WebGui**: WebGui 빌드 시의 예외로 인한 표시 문제를 수정했습니다. --- ## v1.0.3 > *2025/12/25* ### 🛠️ 수정 - **WebGui**: 노트 제목이 긴 경우의 레이아웃 문제를 해결했습니다. --- ## v1.0.2 > *2025/12/25* ### 🚀 최적화 - **첨부 파일**: 첨부 파일 업로드 로직을 최적화하여 업로드 시간을 대폭 단축했습니다. ### 🛠️ 수정 - **CI/CD**: GitHub Action 업데이트 제한을 수정했습니다. --- ## v1.0.1 > *2025/12/23* ### 🛠️ 수정 - **권한**: 일부 시스템에서 업로드 시 권한 부족으로 인해 발생하던 문제를 수정했습니다. --- ## v1.0.0 > *2025/12/22* ### ✨ 새 기능 - **동기화**: 설정 파일 동기화 기능 및 인터페이스를 추가했습니다. ### 🚀 최적화 - **스크립트**: 스크립트 출력 표시를 최적화했습니다. ### 🛠️ 수정 - **스크립트**: 스크립트 실행 제어 실패 문제를 수정했습니다. --- ## v0.11.5 > *2025/12/19* ### 🛠️ 수정 - **Docker**: Docker 이미지 실행 문제를 수정했습니다. --- ## v0.11.4 > *2025/12/18* ### ✨ 새 기능 - **인증**: 인증 확인 인터페이스에서 버전 정보 반환 기능을 추가했습니다. --- ## v0.11.3 > *2025/12/16* ### ✨ 새 기능 - **정리**: 시작 시 자동 정리 작업 및 Session 자동 정리 로직을 추가했습니다. ### 🛠️ 수정 - **안정성**: 고동시성 상황에서 연결 종료로 인한 비정상 종료 문제를 수정했습니다. --- ## v0.11.2 > *2025/12/15* ### 🛠️ 수정 - **안정성**: 동시성 상황에서 연결 종료로 인한 비정상 종료 문제를 수정했습니다. --- ## v0.11.1 > *2025/12/14* ### ✨ 새 기능 - **아키텍처**: 향후 기능 확장을 용이하게 하기 위해 메시지에 접두사를 추가했습니다. --- ## v0.10.2 > *2025/12/12* ### ✨ 새 기능 - **설정**: 업로드/다운로드 청크 설정을 추가했습니다 (기본 512KB). --- ## v0.10.1 > *2025/12/12* ### ✨ 새 기능 - **기능**: 바이너리 파일 다운로드 기능을 추가했습니다. - **기능**: WebSocket을 통한 청크 다운로드 기능을 추가했습니다. - **기능**: 버전 관리 기능을 추가했습니다. --- ## v0.9.6 > *2025/12/11* - 초기 버전 (기록 시작). ================================================ FILE: docs/CHANGELOG.zh-CN.md ================================================ # 更新日志 (CHANGELOG) 本项目的所有重大变更都将记录在此文件中。 本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/0.3.0/) 规范。 --- ## v1.16.2 > *2026/02/14* ### 🚀 优化 - **WebGui**: 调整 WebGui 界面位置。 - **功能**: 增加服务器信息的展示。 - **性能**: 针对各类列表显示高度过低的问题,优化为零拷贝访问。 - **同步**: 优化笔记/附件同步逻辑。 --- ## v1.16.1 > *2026/02/14* ### 🚀 优化 - **WebGui**: 调整 WebGui 界面位置。 - **功能**: 增加服务器信息的展示。 --- ## v1.15.11 > *2026/02/14* ### 🚀 优化 - **WebGui**: 优化 WebGui 界面,增加对 URL 的支持。 --- ## v1.15.10 > *2026/02/14* ### 🚀 优化 - **架构**: 调整服务工具集。 - **API**: 调整接口返回的结构。 --- ## v1.15.9 > *2026/02/14* ### ✨ 新增 - **工具**: 为 fns 增加 docs 和 ws 调试工具访问入口。 --- ## v1.15.8 > *2026/02/13* ### 🛠️ 修复 - **稳定性**: 修复时间处理的小 BUG。 --- ## v1.15.7 > *2026/02/13* ### 🛠️ 修复 - **同步**: 修复离线删除清理本地哈希表的问题。 --- ## v1.15.6 > *2026/02/13* ### 🛠️ 修复 - **脚本**: 修复 fns 快捷脚本在 macOS 下的运行问题。 - **日志**: 修正日志打印内容。 --- ## v1.15.5 > *2026/02/12* ### 🚀 优化 - **CI/CD**: 调整 GitHub Action 使用 go mod 版本进行编译发布。 --- ## v1.15.4 > *2026/02/12* ### ✨ 新增 - **同步**: 增加清理笔记配置相关消息功能。 --- ## v1.15.3 > *2026/02/10* ### 🛠️ 修复 - **目录**: 给重复目录做一层兜底方案,增加启动服务清理重复目录功能。 --- ## v1.15.2 > *2026/02/09* ### 🚀 优化 - **数据库**: 优化 DB 性能及结构,进行批量格式化。 --- ## v1.15.1 > *2026/02/07* ### ✨ 新增 - **目录**: 新增目录(Folder)管理功能,包括 Model 及相关逻辑。 - **同步**: 修复潜在的数据竞争问题,优化笔记和附件重命名功能。 --- ## v1.14.1 > *2026/01/31* ### ✨ 新增 - **回收站**: 附件管理支持回收站及批量恢复功能。 ### 🛠️ 修复 - **稳定性**: 修复附件/配置文件因修改时间与内容一致导致的资源未正常创建问题。 ### 🚀 优化 - **API**: 优化附件查看/下载接口,改为零拷贝访问。 - **WebGui**: 优化各类列表显示高度过低的问题。 --- ## v1.14.0 > *2026/01/31* ### ✨ 新增 - **回收站**: 增加附件管理回收站。 - **WebGui**: 增加服务器信息的展示。 - **同步**: 增加笔记和附件重命名功能。 ### 🛠️ 修复 - **稳定性**: 修复潜在的数据竞争问题。 --- ## v1.13.0 > *2026/01/30* ### ✨ 新增 - **同步**: 新增附件、笔记、配置离线删除同步功能。 - **同步**: 增量同步模式下增加自动下载缺失文件功能。 --- ## v1.12.0 > *2026/01/29* ### 🚀 优化 - **语言**: 将所有代码注释及文档统一翻译/更新为中英双语或英文。 - **API**: 优化项目返回信息的国际化支持。 - **稳定性**: 修正自动资源前缀问题。 - **API**: 新增 API 扩展:编辑操作、反向链接 (Backlinks)、健康检查。 --- ## v1.11.3 > *2026/01/27* ### 🛠️ 修复 - **附件**: 修正附件下载请求超时(30s)导致报错的问题,调整为可配置且默认 1 小时。 --- ## v1.11.2 > *2026/01/27* ### ✨ 新增 - **WebGui**: 增加 Obsidian SSO 自动授权机制。 ### 🚀 优化 - **WebGui**: 优化授权配置界面 UI。 --- ## v1.11.1 > *2026/01/26* ### 🚀 优化 - **发布**: 调整版本发布流程。 --- ## v1.11.0 > *2026/01/26* ### ✨ 新增 - **功能**: 新增版本检测及版本信息获取功能。 --- ## v1.10.8 > *2026/01/26* ### ✨ 新增 - **API**: 新增附件状态检测接口。 --- ## v1.10.7 > *2026/01/25* ### 🛠️ 修复 - **稳定性**: 修复由于上传文件校验一致性判断引发的服务崩溃问题。 --- ## v1.10.6 > *2026/01/24* ### ✨ 新增 - **WebGui**: 附件管理页面增加分页功能。 --- ## v1.10.5 > *2026/01/23* ### 🛠️ 修复 - **回收站**: 修正从回收站和历史版本中恢复笔记/版本的问题。 --- ## v1.10.4 > *2026/01/23* ### 🛠️ 修复 - **附件**: 修复附件上传过程中网络断开导致的异常,降低报错等级。 --- ## v1.10.3 > *2026/01/20* ### 🚀 优化 - **WebGui**: 取消笔记仓库放大效果,改为选中阴影效果。 ### 🛠️ 修复 - **WebGui**: 修复笔记仓库名称含特殊字符时无法访问的问题。 --- ## v1.10.2 > *2026/01/20* ### 🛠️ 修复 - **管理**: 修复新用户无法注册及无法关闭用户注册设置的 Bug。 --- ## v1.10.1 > *2026/01/20* ### 🛠️ 修复 - **管理**: 修复新用户无法注册的问题。 --- ## v1.10.0 > *2026/01/19* ### ✨ 新增 - **附件**: 增加附件管理相关功能。 - **鉴权**: 增加 Token 过期时间配置。 - **分享**: 增加分享功能相关接口。 - **文档**: 增加 Swagger API 文档。 ### 🚀 优化 - **WebGui**: 调整 WebGui 部署路径。 - **API**: 细化接口错误提示。 ### 🛠️ 修复 - **WebGui**: 修正 WebGui 自动翻译导致的提示问题。 --- ## v1.9.1 > *2026/01/14* ### 🚀 优化 - **WebGui**: 增加蓝色配色方案,优化编辑器显示效果。 --- ## v1.9.0 > *2026/01/14* ### ✨ 新增 - **WebGui**: 界面全新重构(由 @ZyphrZero 贡献)。 - **WebGui**: 更换编辑器为 Vditor,支持富文本和 Markdown 即时渲染。 - **WebGui**: 支持自定义笔记搜索、列表字段排序及颜色主题。 - **WebGui**: 新增暗黑模式、在线版本检测及回收站笔记恢复功能。 - **设置**: 新增历史记录保留版本数及保存延迟设置。 ### 🚀 优化 - **安全**: 优化服务令牌加密混淆字符。 --- ## v1.8.1 > *2026/01/12* ### 🔄 变更 - **架构**: 引入 DDD 分层架构(由 @ZyphrZero 贡献),移除全局变量,实现依赖注入模式。 ### 🚀 优化 - **同步**: 离线笔记合并优化,实现基于行级的冲突检测与三方合并。 - **性能**: 新增 Worker Pool 和 Per-User Write Queue,解决 SQLite 并发锁定问题。 - **WebSocket**: 优化 Context 生命周期管理,增强 TraceID 追踪能力。 ### 🛠️ 修复 - **逻辑**: 修复笔记重命名导致的笔记丢失和报错问题。 --- ## v1.7.3 > *2026/01/09* ### 🛠️ 修复 - **数据库**: 增加数据库创建失败时的友好报错提示。 --- ## v1.7.2 > *2026/01/09* ### ✨ 新增 - **WebGui**: 增加配置设置功能及相关接口。 - **管理**: 增加管理员 ID 设置。 --- ## v1.7.1 > *2026/01/09* ### ✨ 新增 - **同步**: 新增离线设备笔记编辑合并功能(需插件端 v1.7+ 开启)。 --- ## v1.6.3 > *2026/01/08* ### 🚀 优化 - **WebGui**: 优化笔记列表搜索功能。 - **WebGui**: 增加图标显示。 - **WebGui**: 笔记仓库增加附件显示和刷新按钮。 ### 🛠️ 修复 - **稳定性**: 修复并发查询时可能出现的异常。 --- ## v1.6.1 > *2026/01/07* ### 🚀 优化 - **性能**: 优化大笔记库同步效率及数据处理(需插件端 v1.6+)。 - **缓存**: 为静态内容增加浏览器缓存机制。 > [!CAUTION] > 本版本涉及数据库结构优化,建议删除原服务端 `storage/database` 目录下的 DB 文件,笔记修改历史将重新生成。 --- ## v1.5.4 > *2026/01/06* ### 🛠️ 修复 - **附件**: 修正上传附件时偶尔出现的报错问题。 --- ## v1.5.3 > *2026/01/06* ### 🚀 优化 - **WebGui**: 对编辑功能进行延时加载,提升首页加载速度。 --- ## v1.5.2 > *2026/01/05* ### 🛠️ 修复 - **同步**: 修正同步任务进度显示不准确的问题。 --- ## v1.5.1 > *2026/01/04* ### 🛠️ 修复 - **逻辑**: 修正笔记重命名后无法正常删除的问题。 - **稳定性**: 修正大规模笔记同步时导致 WebSocket 连接重置的问题。 - **多语言**: 修复 WebGui 接口语言错误。 --- ## v1.5.0 > *2026/01/04* ### ✨ 新增 - **回收站**: 增加笔记回收站功能。 - **WebGui**: 增加用户状态检测。 - **WebGui**: 注册页面增加关闭注册的检测。 - **WebGui**: 增加操作确认的键盘快捷键支持。 ### 🚀 优化 - **WebGui**: 优化笔记编辑页面的使用体验。 - **数据库**: 优化并解决数据库并发访问问题。 ### 🛠️ 修复 - **脚本**: 修正快捷脚本可能覆盖配置文件的问题。 --- ## v1.4.7 > *2026/01/03* ### 🛠️ 修复 - **数据库**: 尝试解决 SQLite 并发问题,修正内部错误码。 --- ## v1.4.6 > *2026/01/03* ### 🛠️ 修复 - **Docker**: 修正 Docker 环境下运行报 `temp` 目录不存在的问题。 --- ## v1.4.5 > *2026/01/03* ### 🛠️ 修复 - **同步**: 修正首次同步或全量同步时无法同步附件的问题(需插件端 v1.5.14+)。 --- ## v1.4.4 > *2026/01/02* ### 🛠️ 修复 - **访问**: 修正 Emoji 标题无法访问的问题。 ### ✨ 新增 - **文档**: 增加帮助文件。 --- ## v1.4.3 > *2026/01/02* ### 🔄 变更 - **仓库**: 笔记仓库删除操作改为软删除。 --- ## v1.4.2 > *2026/01/01* ### ✨ 新增 - **WebGui**: 笔记删除操作增加红色的二次确认弹窗,防止误删。 --- ## v1.4.1 > *2025/12/31* ### 🚀 优化 - **API**: 笔记资源(图片等)下载接口增加 ETag 浏览器缓存机制,提升加载速度。 --- ## v1.4.0 > *2025/12/31* ### ✨ 新增 - **WebGui**: 增加最大化按钮,提升全屏编辑体验。 - **WebGui**: 笔记查看页面增加对 Obsidian 内嵌图片、PDF 以及其他附件的正常显示支持。 - **API**: 增加资源下载接口。 --- ## v1.3.8 > *2025/12/31* ### 🚀 优化 - **服务端**: 为笔记建立内容哈希版本库,方便后续进行溯源、对比和合并操作。 --- ## v1.3.7 > *2025/12/30* ### 🛠️ 修复 - **稳定性**: 为任务和升级脚本增加宕机恢复机制,避免单个任务报错导致整个服务崩溃。 - **稳定性**: 修复原有各层级出现的空指针(nil pointer)导致的 Panic 问题。 --- ## v1.3.6 > *2025/12/30* ### 🛠️ 修复 - **任务管理**: 修复任务管理器中存在的报错问题。 --- ## v1.3.5 > *2025/12/30* ### 🚀 优化 - **WebGui**: 优化笔记查看页面的显示效果。 - **脚本**: 优化一键安装/管理脚本。 --- ## v1.3.4 > *2025/12/30* ### 🛠️ 修复 - **同步**: 修复同步命令处理错误导致的文件错误同步创建到所有客户端的问题。 - **脚本**: 修复一键脚本在 `Ctrl+C` 时导致已启动的服务被同步关闭的问题。 --- ## v1.3.3 > *2025/12/29* ### 🛠️ 修复 - **同步**: 解决单用户多笔记仓库时可能出现的更新混淆问题。 --- ## v1.3.2 > *2025/12/28* ### ✨ 新增 - **多语言**: 增加对多语言环境的支持。 ### 🚀 优化 - **WebGui**: 优化笔记历史差异对比的显示效果。 --- ## v1.3.1 > *2025/12/28* ### 🚀 优化 - **逻辑处理**: 优化笔记修改标题时的逻辑处理流程。 --- ## v1.3.0 > *2025/12/28* ### ✨ 新增 - **WebGui**: 增加设置项,允许用户控制 WebGui 的字体设置。 --- ## v1.2.6 > *2025/12/27* ### 🚀 优化 - **WebGui**: 优化字体加载逻辑,避免字体加载导致的界面卡顿。 --- ## v1.2.5 > *2025/12/27* ### ✨ 新增 - **客户端**: 增加对客户端名称的记录支持。 ### 🚀 优化 - **清理逻辑**: 增加笔记重命名后的同步清理逻辑。 --- ## v1.2.4 > *2025/12/27* ### 🛠️ 修复 - **WebGui**: 修复历史版本内容为空时导致的显示 Bug。 --- ## v1.2.3 > *2025/12/27* ### ✨ 新增 - **API**: 增加笔记历史相关的接口和功能。 ### 🚀 优化 - **数据库**: 优化数据库查询效率。 - **WebGui**: 修改 WebGui 显示字体并修复各类显示 Bug。 ### 🛠️ 修复 - **稳定性**: 修复高并发访问时出现的问题。 --- ## v1.2.2 > *2025/12/27* ### 🛠️ 修复 - **WebGui**: 修正笔记历史为空时导致的页面空白问题。 --- ## v1.2.1 > *2025/12/27* ### ✨ 新增 - **API**: 增加笔记历史相关接口和功能。 ### 🚀 优化 - **数据库**: 优化数据库查询效率。 - **稳定性**: 解决大量并发访问时的稳定性问题。 --- ## v1.0.4 > *2025/12/26* ### 🛠️ 修复 - **WebGui**: 修正 WebGui 页面构建(Build)异常导致的空白显示问题。 --- ## v1.0.3 > *2025/12/25* ### 🛠️ 修复 - **WebGui**: 解决笔记标题过长导致的排版显示问题。 --- ## v1.0.2 > *2025/12/25* ### 🚀 优化 - **附件**: 优化附件上传逻辑,显著减少上传时间。 ### 🛠️ 修复 - **CI/CD**: 修正 GitHub Action 的更新限制问题。 --- ## v1.0.1 > *2025/12/23* ### 🛠️ 修复 - **权限**: 修复部分系统在上传时由于权限不足导致的问题。 --- ## v1.0.0 > *2025/12/22* ### ✨ 新增 - **同步**: 新增配置文件同步相关功能及接口。 ### 🚀 优化 - **脚本**: 优化显示脚本的输出。 ### 🛠️ 修复 - **脚本**: 修正脚本控制运行失败的问题。 --- ## v0.11.5 > *2025/12/19* ### 🛠️ 修复 - **Docker**: 修正 Docker 镜像无法执行的问题。 --- ## v0.11.4 > *2025/12/18* ### ✨ 新增 - **鉴权**: 在验证授权接口中增加下发版本信息的功能。 --- ## v0.11.3 > *2025/12/16* ### ✨ 新增 - **清理**: 增加程序启动时的自动清理任务以及 Session 自动清理逻辑。 ### 🛠️ 修复 - **稳定性**: 修正高并发下由于连接关闭导致的异常退出问题。 --- ## v0.11.2 > *2025/12/15* ### 🛠️ 修复 - **稳定性**: 修正并发下由于连接关闭导致的程序异常退出。 --- ## v0.11.1 > *2025/12/14* ### ✨ 新增 - **架构**: 给消息增加前缀,方便后续业务功能扩容。 --- ## v0.10.2 > *2025/12/12* ### ✨ 新增 - **设置**: 增加上传下载分片设置项(默认 512KB)。 --- ## v0.10.1 > *2025/12/12* ### ✨ 新增 - **功能**: 增加二进制文件下载功能。 - **功能**: 增加 WebSocket 分块下载功能。 - **功能**: 增加版本控制管理。 --- ## v0.9.6 > *2025/12/11* - 初始版本(记录开始)。 ================================================ FILE: docs/CHANGELOG.zh-TW.md ================================================ # 更新日誌 (CHANGELOG) 本專案的所有重大變更都將記錄在此文件中。 本專案遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/0.3.0/) 規範。 --- ## v1.16.2 > *2026/02/14* ### 🚀 優化 - **WebGui**: 調整 WebGui 介面位置。 - **功能**: 新增伺服器資訊顯示。 - **性能**: 針對各類列表顯示高度過低的問題,採用零拷貝(Zero-copy)方式進行優化。 - **同步**: 優化筆記/附件同步邏輯。 --- ## v1.16.1 > *2026/02/14* ### 🚀 優化 - **WebGui**: 調整 WebGui 介面位置。 - **功能**: 新增伺服器資訊顯示。 --- ## v1.15.11 > *2026/02/14* ### 🚀 優化 - **WebGui**: 優化 WebGui 介面並增加 URL 支援。 --- ## v1.15.10 > *2026/02/14* ### 🚀 優化 - **架構**: 調整服務工具集。 - **API**: 調整介面回應結構。 --- ## v1.15.9 > *2026/02/14* ### ✨ 新功能 - **工具**: 在 fns 中增加 docs 及 ws 調試工具的存取入口。 --- ## v1.15.8 > *2026/02/13* ### 🛠️ 修復 - **穩定性**: 修復時間處理相關的輕微 Bug。 --- ## v1.15.7 > *2026/02/13* ### 🛠️ 修復 - **同步**: 修復離線刪除時,本地雜湊表未清理的問題。 --- ## v1.15.6 > *2026/02/13* ### 🛠️ 修復 - **指令碼**: 修復 macOS 環境下 fns 捷徑指令碼的執行問題。 - **日誌**: 修正日誌輸出內容。 --- ## v1.15.5 > *2026/02/12* ### 🚀 優化 - **CI/CD**: 調整 GitHub Action 使用 go mod 版本進行構建與發佈。 --- ## v1.15.4 > *2026/02/12* ### ✨ 新功能 - **同步**: 新增筆記配置相關訊息清理功能。 --- ## v1.15.3 > *2026/02/10* ### 🛠️ 修復 - **目錄**: 增加重複目錄的規避方案,並在啟動時增加重複目錄清理功能。 --- ## v1.15.2 > *2026/02/09* ### 🚀 優化 - **資料庫**: DB 性能及結構優化,並進行批次格式化。 --- ## v1.15.1 > *2026/02/07* ### ✨ 新功能 - **目錄**: 新增目錄(Folder)管理功能(含 Model 及相關邏輯)。 - **同步**: 修復潛在的資料競爭問題,優化筆記/附件重新命名功能。 --- ## v1.14.1 > *2026/01/31* ### ✨ 新功能 - **回收站**: 附件管理支援回收站及批次恢復功能。 ### 🛠️ 修復 - **穩定性**: 修復附件/配置文件更新時間與內容一致時資源未平滑產生的問題。 ### 🚀 優化 - **API**: 採用零拷貝方式優化附件查詢/下載介面。 - **WebGui**: 優化各類列表顯示高度過低的問題。 --- ## v1.14.0 > *2026/01/31* ### ✨ 新功能 - **回收站**: 附件管理新增回收站。 - **WebGui**: 新增伺服器資訊顯示。 - **同步**: 新增筆記及附件重新命名功能。 ### 🛠️ 修復 - **穩定性**: 修復潛在的資料競爭問題。 --- ## v1.13.0 > *2026/01/30* ### ✨ 新增 - **同步**: 新增附件、筆記、配置離線刪除同步功能。 - **同步**: 增量同步模式下增加自動下載缺失文件功能。 --- ## v1.12.0 > *2026/01/29* ### 🚀 優化 - **語言**: 將所有代碼註釋及文檔統一翻譯/更新為中英雙語或英文。 - **API**: 優化項目返回信息的國際化支持。 - **穩定性**: 修正自動資源前綴問題。 - **API**: 新增 API 擴展:編輯操作、反連結 (Backlinks)、健康檢查。 --- ## v1.11.3 > *2026/01/27* ### 🛠️ 修復 - **附件**: 修正附件下載請求超時(30s)導致報錯的問題,調整為可配置且默認 1 小時。 --- ## v1.11.2 > *2026/01/27* ### ✨ 新增 - **WebGui**: 增加 Obsidian SSO 自動授權機制。 ### 🚀 優化 - **WebGui**: 優化授權配置界面 UI。 --- ## v1.11.1 > *2026/01/26* ### 🚀 優化 - **發佈**: 調整版本發佈流程。 --- ## v1.11.0 > *2026/01/26* ### ✨ 新增 - **功能**: 新增版本檢測及版本信息獲取功能。 --- ## v1.10.8 > *2026/01/26* ### ✨ 新增 - **API**: 新增附件狀態檢測接口。 --- ## v1.10.7 > *2026/01/25* ### 🛠️ 修復 - **穩定性**: 修復由於上傳文件校驗一致性判斷引發的服務崩潰問題。 --- ## v1.10.6 > *2026/01/24* ### ✨ 新增 - **WebGui**: 附件管理頁面增加分頁功能。 --- ## v1.10.5 > *2026/01/23* ### 🛠️ 修復 - **回收站**: 修正從回收站和歷史版本中恢復筆記/版本的問題。 --- ## v1.10.4 > *2026/01/23* ### 🛠️ 修復 - **附件**: 修復附件上傳過程中網絡斷開導致的異常,降低報錯等級。 --- ## v1.10.3 > *2026/01/20* ### 🚀 優化 - **WebGui**: 取消筆記倉庫放大效果,改為選中陰影效果。 ### 🛠️ 修復 - **WebGui**: 修復筆記倉庫名稱含特殊字符時無法訪問的 Bug。 --- ## v1.10.2 > *2026/01/20* ### 🛠️ 修復 - **管理**: 修復新用戶無法註冊及無法關閉用戶註冊設置的 Bug。 --- ## v1.10.1 > *2026/01/20* ### 🛠️ 修復 - **管理**: 修復新用戶無法註冊的問題。 --- ## v1.10.0 > *2026/01/19* ### ✨ 新增 - **附件**: 增加附件管理相關功能。 - **鑑權**: 增加 Token 過期時間配置。 - **分享**: 增加分享功能相關接口。 - **文檔**: 增加 Swagger API 文檔。 ### 🚀 優化 - **WebGui**: 調整 WebGui 部署路徑。 - **API**: 細化接口錯誤提示。 ### 🛠️ 修復 - **WebGui**: 修正 WebGui 自動翻譯導致的提示問題。 --- ## v1.9.1 > *2026/01/14* ### 🚀 優化 - **WebGui**: 增加藍色配色方案,優化編輯器顯示效果。 --- ## v1.9.0 > *2026/01/14* ### ✨ 新增 - **WebGui**: 界面全新重構(由 @ZyphrZero 貢獻)。 - **WebGui**: 更換編輯器為 Vditor,支持富文本和 Markdown 即時渲染。 - **WebGui**: 支持自定義筆記搜索、列表字段排序及顏色主題。 - **WebGui**: 新增暗黑模式、在線版本檢測及回收站筆記恢復功能。 - **設置**: 新增歷史記錄保留版本數及保存延遲設置。 ### 🚀 優化 - **安全**: 優化服務令牌加密混淆字符。 --- ## v1.8.1 > *2026/01/12* ### 🔄 變更 - **架構**: 引入 DDD 分層架構(由 @ZyphrZero 貢獻),移除全局變量,實現依賴注入模式。 ### 🚀 優化 - **同步**: 離線筆記合併優化,實現基於行級的衝突檢測與三方合併。 - **性能**: 新增 Worker Pool 和 Per-User Write Queue,解決 SQLite 併發鎖定問題。 - **WebSocket**: 優化 Context 生命周期管理,增強 TraceID 追蹤能力。 ### 🛠️ 修復 - **邏輯**: 修復筆記重命名導致的筆記丟失和報錯問題。 --- ## v1.7.3 > *2026/01/09* ### 🛠️ 修復 - **數據庫**: 增加數據庫創建失敗時的友好報錯提示。 --- ## v1.7.2 > *2026/01/09* ### ✨ 新增 - **WebGui**: 增加配置設置功能及相關接口。 - **管理**: 增加管理員 ID 設置。 --- ## v1.7.1 > *2026/01/09* ### ✨ 新增 - **同步**: 新增離線設備筆記編輯合併功能(需插件端 v1.7+ 開啟)。 --- ## v1.6.3 > *2026/01/08* ### 🚀 優化 - **WebGui**: 優化筆記列表搜索功能。 - **WebGui**: 增加圖標顯示。 - **WebGui**: 筆記倉庫增加附件顯示和刷新按鈕。 ### 🛠️ 修復 - **穩定性**: 修復併發查詢時可能出現的異常。 --- ## v1.6.1 > *2026/01/07* ### 🚀 優化 - **性能**: 優化大筆記庫同步效率及數據處理(需插件端 v1.6+)。 - **緩存**: 為靜態內容增加瀏覽器緩存機制。 > [!CAUTION] > 本版本涉及數據庫結構優化,建議刪除原服務端 `storage/database` 目錄下的 DB 文件,筆記修改歷史將重新生成。 --- ## v1.5.4 > *2026/01/06* ### 🛠️ 修復 - **附件**: 修正上傳附件時偶爾出現的報錯問題。 --- ## v1.5.3 > *2026/01/06* ### 🚀 優化 - **WebGui**: 對編輯功能進行延時加載,提升首頁加載速度。 --- ## v1.5.2 > *2026/01/05* ### 🛠️ 修復 - **同步**: 修正同步任務進度顯示不準確的問題。 --- ## v1.5.1 > *2026/01/04* ### 🛠️ 修復 - **邏輯**: 修正筆記重命名後無法正常刪除的問題。 - **穩定性**: 修正大規模筆記同步時導致 WebSocket 連接重置的問題。 - **多語言**: 修復 WebGui 接口語言錯誤。 --- ## v1.5.0 > *2026/01/04* ### ✨ 新增 - **回收站**: 增加筆記回收站功能。 - **WebGui**: 增加用戶狀態檢測。 - **WebGui**: 註冊頁面增加關閉註冊的檢測。 - **WebGui**: 增加操作確認的鍵盤快捷鍵支持。 ### 🚀 優化 - **WebGui**: 優化筆記編輯頁面的使用體驗。 - **數據庫**: 優化並解決數據庫併發訪問問題。 ### 🛠️ 修復 - **腳本**: 修正快捷腳本可能覆蓋配置文件的問題。 --- ## v1.4.7 > *2026/01/03* ### 🛠️ 修復 - **數據庫**: 嘗試解決 SQLite 併發問題,修正內部錯誤碼。 --- ## v1.4.6 > *2026/01/03* ### 🛠️ 修復 - **Docker**: 修正 Docker 環境下運行報 `temp` 目錄不存在的問題。 --- ## v1.4.5 > *2026/01/03* ### 🛠️ 修復 - **同步**: 修正首次同步或全量同步時無法同步附件的問題(需插件端 v1.5.14+)。 --- ## v1.4.4 > *2026/01/02* ### 🛠️ 修復 - **訪問**: 修正 Emoji 標題無法訪問的問題。 ### ✨ 新增 - **文檔**: 增加幫助文件。 --- ## v1.4.3 > *2026/01/02* ### 🔄 變更 - **倉庫**: 筆記倉庫刪除操作改為軟刪除。 --- ## v1.4.2 > *2026/01/01* ### ✨ 新增 - **WebGui**: 筆記刪除操作增加紅色的二次確認彈窗,防止誤刪。 --- ## v1.4.1 > *2025/12/31* ### 🚀 優化 - **API**: 筆記資源(圖片等)下載接口增加 ETag 瀏覽器緩存機制,提升加載速度。 --- ## v1.4.0 > *2025/12/31* ### ✨ 新增 - **WebGui**: 增加最大化按鈕,提升全屏編輯體驗。 - **WebGui**: 筆記查看頁面增加對 Obsidian 內嵌圖片、PDF 以及其他附件的正常顯示支持。 - **API**: 增加資源下載接口。 --- ## v1.3.8 > *2025/12/31* ### 🚀 優化 - **服務端**: 為筆記建立內容哈希版本庫,方便後續進行溯源、對比和合併操作。 --- ## v1.3.7 > *2025/12/30* ### 🛠️ 修復 - **穩定性**: 為任務和升級腳本增加宕機恢復機制,避免單個任務報錯導致整個服務崩潰。 - **穩定性**: 修復原有各層級出現的空指針(nil pointer)導致的 Panic 問題。 --- ## v1.3.6 > *2025/12/30* ### 🛠️ 修復 - **任務管理**: 修復任務管理器中存在的報錯問題。 --- ## v1.3.5 > *2025/12/30* ### 🚀 優化 - **WebGui**: 優化筆記查看頁面的顯示效果。 - **腳本**: 優化一鍵安裝/管理腳本。 --- ## v1.3.4 > *2025/12/30* ### 🛠️ 修復 - **同步**: 修復同步命令處理錯誤導致的文件錯誤同步創建到所有客戶端的問題。 - **腳本**: 修復一鍵腳本在 `Ctrl+C` 時導致已啟動的服務被同步關閉的問題。 --- ## v1.3.3 > *2025/12/29* ### 🛠️ 修復 - **同步**: 解決單用戶多筆記倉庫時可能出現的更新混淆問題。 --- ## v1.3.2 > *2025/12/28* ### ✨ 新增 - **多語言**: 增加對多語言環境的支持。 ### 🚀 優化 - **WebGui**: 優化筆記歷史差異對比的顯示效果。 --- ## v1.3.1 > *2025/12/28* ### 🚀 優化 - **邏輯處理**: 優化筆記修改標題時的邏輯處理流程。 --- ## v1.3.0 > *2025/12/28* ### ✨ 新增 - **WebGui**: 增加設置項,允許用戶控制 WebGui 的字體設置。 --- ## v1.2.6 > *2025/12/27* ### 🚀 優化 - **WebGui**: 優化字體加載邏輯,避免字體加載導致的界面卡頓。 --- ## v1.2.5 > *2025/12/27* ### ✨ 新增 - **客戶端**: 增加對客戶端名稱的記錄支持。 ### 🚀 優化 - **清理邏輯**: 增加筆記重命名後的同步清理邏輯。 --- ## v1.2.4 > *2025/12/27* ### 🛠️ 修復 - **WebGui**: 修復歷史版本內容為空時導致的顯示 Bug。 --- ## v1.2.3 > *2025/12/27* ### ✨ 新增 - **API**: 增加筆記歷史相關的接口和功能。 ### 🚀 優化 - **數據庫**: 優化數據庫查詢效率。 - **WebGui**: 修改 WebGui 顯示字體並修復各類顯示 Bug。 ### 🛠️ 修復 - **穩定性**: 修復高併發訪問時出現的問題。 --- ## v1.2.2 > *2025/12/27* ### 🛠️ 修復 - **WebGui**: 修正筆記歷史為空時導致的頁面空白問題。 --- ## v1.2.1 > *2025/12/27* ### ✨ 新增 - **API**: 增加筆記歷史相關接口和功能。 ### 🚀 優化 - **數據庫**: 優化數據庫查詢效率。 - **穩定性**: 解決大量併發訪問時的穩定性問題。 --- ## v1.0.4 > *2025/12/26* ### 🛠️ 修復 - **WebGui**: 修正 WebGui 頁面構建(Build)異常導致的空白顯示問題。 --- ## v1.0.3 > *2025/12/25* ### 🛠️ 修復 - **WebGui**: 解決筆記標題過長導致的排版顯示問題。 --- ## v1.0.2 > *2025/12/25* ### 🚀 優化 - **附件**: 優化附件上傳邏輯,顯著減少上傳時間。 ### 🛠️ 修復 - **CI/CD**: 修正 GitHub Action 的更新限制問題。 --- ## v1.0.1 > *2025/12/23* ### 🛠️ 修復 - **權限**: 修復部分系統在上傳時由於權限不足導致的問題。 --- ## v1.0.0 > *2025/12/22* ### ✨ 新增 - **同步**: 新增配置文件同步相關功能及接口。 ### 🚀 優化 - **腳本**: 優化顯示腳本的輸出。 ### 🛠️ 修復 - **腳本**: 修正腳本控制運行失敗的問題。 --- ## v0.11.5 > *2025/12/19* ### 🛠️ 修復 - **Docker**: 修正 Docker 鏡像無法執行的問題。 --- ## v0.11.4 > *2025/12/18* ### ✨ 新增 - **鑑權**: 在驗證授權接口中增加下發版本信息的功能。 --- ## v0.11.3 > *2025/12/16* ### ✨ 新增 - **清理**: 增加程序啟動時的自動清理任務以及 Session 自動清理邏輯。 ### 🛠️ 修復 - **穩定性**: 修正高併發下由於連接關閉導致的異常退出問題。 --- ## v0.11.2 > *2025/12/15* ### 🛠️ 修復 - **穩定性**: 修正併發下由於連接關閉導致的程序異常退出。 --- ## v0.11.1 > *2025/12/14* ### ✨ 新增 - **架構**: 給消息增加前綴,方便後續業務功能擴容。 --- ## v0.10.2 > *2025/12/12* ### ✨ 新增 - **設置**: 增加上傳下載分片設置項(默認 512KB)。 --- ## v0.10.1 > *2025/12/12* ### ✨ 新增 - **功能**: 增加二進制文件下載功能。 - **功能**: 增加 WebSocket 分塊下載功能。 - **功能**: 增加版本控制管理。 --- ## v0.9.6 > *2025/12/11* - 初始版本(記錄開始)。 ================================================ FILE: docs/CHANGELOG_GUIDELINE.md ================================================ # CHANGELOG 维护规范 本文档定义了本项目 `docs/CHANGELOG.md` 的维护标准,确保变更记录清晰、专业且符合行业通用规范。 ## 1. 基本准则 - **面向用户**:变更日志应让用户(开发者或最终用户)轻松了解每个版本的具体变化。 - **分类清晰**:所有变更必须归入特定的类别。 - **版本对齐**:每个版本号必须对应 Git 中的一个 Release Tag。 - **日期格式**:使用 `YYYY/MM/DD` 格式。 ## 2. 核心架构 本项目遵循 [Keep a Changelog (0.3.0)](https://keepachangelog.com/zh-CN/0.3.0/) 核心思想,并结合 Emoji 进行视觉增强。 ### 修改类别定义 在每个版本标题下,按以下固定类别组织内容: | 类别 (Category) | 标识符 (Emoji) | 适用场景 | | :--- | :---: | :--- | | **新增** | ✨ | 实现了全新的功能或特性。 | | **优化** | 🚀 | 对现有功能的改进、性能提升或体验优化。 | | **修复** | 🛠️ | 修复了 Bug、崩溃或非预期的行为。 | | **变更** | 🔄 | 对现有功能进行了重大调整(可能涉及破坏性变更)。 | | **废弃** | 🗑️ | 标记即将在后续版本中移除的功能。 | | **安全** | 🔒 | 修复了漏洞或提升了系统安全性。 | ## 3. 编写格式规范 ### 版本标题 使用二级标题,格式为: ``` ## v版本号 > *YYYY/MM/DD* ``` > 示例: ``` ## v1.3.8 > *2025/12/31* ``` ### 条目描述 - **模块加粗**:每个条目开头应标明所属模块(如 **WebGui: ...** 或 **API: ...**)。 - **动词开头**:描述应以“增加”、“优化”、“修复”、“调整”等动词开头。 - **简洁有力**:单行描述不宜过长,复杂的变更可使用子列表。 ```markdown ### ✨ 新增 - **WebGui**: 增加最大化按钮,提升全屏编辑体验。 - **API**: 新增资源下载接口 `/api/v1/download`。 ### 🛠️ 修复 - **稳定性**: 修复高并发下可能导致的内存泄漏问题。 ``` ## 4. 自动化与工具建议 1. **手动同步**:在发布新 Tag 前,手动根据 Git Commits 整理内容并更新 `CHANGELOG.md`。 2. **Commit 信息**:建议在提交代码时遵循 [Conventional Commits](https://www.conventionalcommits.org/),以便未来实现自动生成日志。 --- > [!NOTE] > 本规范旨在通过统一的视觉和语境语言,让项目的发展脉络一目了然。 ================================================ FILE: docs/PR-DESCRIPTION.md ================================================ # PR: Folder Tree Endpoint + Folder API Bug Fixes ## Summary Adds a new `GET /api/folder/tree` endpoint that returns the complete folder hierarchy with note/file counts, and fixes duplicate folder bugs in the existing folder API endpoints. ## New Feature: Folder Tree Endpoint ``` GET /api/folder/tree?vault=&depth= ``` Returns the full folder tree structure with note and file counts per folder. Supports optional `depth` parameter to limit tree depth. **Response example:** ```json { "folders": [ { "path": "projects", "name": "projects", "noteCount": 3, "fileCount": 0, "children": [ { "path": "projects/golf-email-series", "name": "golf-email-series", "noteCount": 6, "fileCount": 0, "children": [...] } ] } ], "rootNoteCount": 26, "rootFileCount": 2 } ``` ## Bug Fixes ### 1. Folder AutoMigrate missing (upstream bug) `folder_repository.go` was using `UseQuery()` instead of `UseQueryWithOnceFunc()`, so the folder table was never auto-created on fresh databases. Added `folder()` helper method with `model.AutoMigrate(g, "Folder")`, matching the pattern used in `note_repository.go`. ### 2. Duplicate folders in API responses **Root cause:** `EnsurePathFID` has a check-then-create race condition. When multiple notes sync concurrently (even from a single device), each goroutine independently checks if a folder exists and creates it if not. Without atomicity, multiple goroutines can all see "not found" and all insert a record for the same path. Confirmed via direct DB inspection — e.g. 3 rows for "projects" path in a single-device setup. **Query-side fix applied to all affected endpoints:** | Endpoint | Fix | |----------|-----| | `GET /api/folders` (List) | Resolves all folder IDs per path via `GetAllByPathHash`, queries children across all matching parent FIDs, deduplicates results by PathHash | | `GET /api/folder/notes` (ListNotes) | Resolves all folder IDs per path, uses `FID IN (...)` query to find notes across all duplicate folder records | | `GET /api/folder/files` (ListFiles) | Same approach as ListNotes for files | | `GET /api/folder/tree` (GetTree) | Path-based deduplication, merges note/file counts across all duplicate folder records | **Note:** The `ListByUpdatedTimestamp` method (used by the sync endpoint) already had PathHash deduplication — the same pattern was missing from List, ListNotes, and ListFiles. **Root cause not fixed in this PR.** A proper fix would require either a `singleflight` keyed by `(vaultID, path)` in `EnsurePathFID`, or a `UNIQUE` constraint on `(vault_id, path_hash)` in the folder table. See comment on `EnsurePathFID` in `folder_service.go` for details. The query-side fixes make the API correct regardless of duplicate rows. ### 3. Swagger docs regenerated Updated generated Swagger files to include the new `/api/folder/tree` endpoint. ## Files Changed | File | Change | |------|--------| | `internal/dto/folder_dto.go` | Added FolderTreeRequest, FolderTreeNode, FolderTreeResponse DTOs | | `internal/domain/repository.go` | Added `GetAllByPathHash` (FolderRepo), `ListByFIDs`/`ListByFIDsCount` (NoteRepo, FileRepo) | | `internal/dao/folder_repository.go` | Fixed AutoMigrate, implemented `GetAllByPathHash` | | `internal/dao/note_repository.go` | Implemented `ListByFIDs`, `ListByFIDsCount` | | `internal/dao/file_repository.go` | Implemented `ListByFIDs`, `ListByFIDsCount` | | `internal/service/folder_service.go` | Added `GetTree`, fixed `List`/`ListNotes`/`ListFiles` dedup, documented race condition | | `internal/routers/api_router/handler_folder.go` | Added `Tree` handler with Swagger annotations | | `internal/routers/router.go` | Registered `/folder/tree` route | | `docs/docs.go`, `docs/swagger.json`, `docs/swagger.yaml` | Regenerated Swagger docs | ## Testing 23/23 API tests pass (test-folder-api.sh), including: - All existing folder CRUD endpoints - New folder tree endpoint (full tree + depth-limited) - Note/file listing within folders - Backlinks, outlinks, note edit operations ## Breaking Changes None. All changes are additive. Existing API behavior is preserved (with duplicates now correctly deduplicated). ================================================ FILE: docs/README.ja.md ================================================ [简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github.com/haierkeys/fast-note-sync-service/blob/master/README.md) / [日本語](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ja.md) / [한국어](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ko.md) / [繁體中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-TW.md) ご質問がある場合は、新しい [issue](https://github.com/haierkeys/fast-note-sync-service/issues/new) を作成するか、Telegramの交流グループに参加して助けを求めてください: [https://t.me/obsidian_users](https://t.me/obsidian_users) 中国本土地域では、Tencentの `cnb.cool` ミラーリポジトリの使用を推奨します: [https://cnb.cool/haierkeys/fast-note-sync-service](https://cnb.cool/haierkeys/fast-note-sync-service)

Fast Note Sync Service

release alpha-release license Go

高性能・低遅延なノート同期、オンライン管理、リモートREST APIサービスプラットフォーム
Golang + Websocket + Reactで構築

データを利用するには、クライアントプラグインを併用する必要があります:Obsidian Fast Note Sync Plugin

fast-note-sync-service-preview fast-note-sync-service-preview
fast-note-sync-service-preview fast-note-sync-service-preview
--- ## 🎯 コア機能 * **🧰 MCP (Model Context Protocol) のネイティブサポート**: * `FNS` はMCPサーバーとして `Cherry Studio` や `Cursor` などの互換性のあるAIクライアントに接続できます。これにより、AIはあなたのプライベートノートや添付ファイルの読み書き能力を持ち、すべての変更が即座に各デバイスに同期されます。 * **🚀 REST APIのサポート**: * 標準的なREST APIインターフェースを提供し、プログラム(自動化スクリプトやAIアシスタントの統合など)によるObsidianノートの作成、読み取り、更新、削除をサポートします。 * 詳細は [RESTful API ドキュメント](/docs/REST_API.md) または [OpenAPI ドキュメント](/docs/swagger.yaml) を参照してください。 * **💻 Web管理パネル**: * モダンな管理インターフェースを内蔵し、ユーザーの作成、プラグイン設定の生成、Vaultやノート内容の管理を簡単に行うことができます。 * **🔄 マルチデバイスによるノート同期**: * **Vault (保管庫)** の自動作成をサポート。 * ノート管理(追加、削除、変更、検索)をサポートし、変更内容をミリ秒レベルでオンラインの全デバイスにリアルタイム配信します。 * **🖼️ 添付ファイル同期のサポート**: * 画像などの非ノートファイルの同期を完全にサポート。 * 大規模な添付ファイルの分割アップロード・ダウンロードをサポートし、分割サイズの設定も可能で、同期効率を向上させます。 * **⚙️ 設定の同期**: * `.obsidian` 設定ファイルの同期をサポート。 * `PDF` の進行状況の同期をサポート。 * **📝 ノートの履歴機能**: * Webページの管理画面およびプラグイン側から、各ノートの歴史的な変更バージョンを確認できます。 * (サーバー v1.2+ が必要) * **🗑️ ごみ箱機能**: * ノート削除後、自動的にごみ箱に移動させることができます。 * ごみ箱からのノート復元をサポートします。(添付ファイルの復元機能は順次追加予定) * **🚫 オフライン同期戦略**: * オフラインでのノート編集の自動マージをサポート。(プラグイン側での設定が必要) * オフラインでの削除に対し、再接続時に自動的に補完、あるいは削除同期を実行。(プラグイン側での設定が必要) * **🔗 共有機能**: * ノートの共有の作成/取り消しが可能。 * 共有ノート内で参照されている画像、音声、動画などの添付ファイルを自動で解析します。 * 共有アクセスの統計機能を提供。 * 共有ノートにパスワードを設定可能。 * 共有ノートへの短縮URL(ショートリンク)を生成可能。 * **📂 ディレクトリ同期**: * フォルダの作成/名前変更/移動/削除の同期をサポート。 * **🌳 Gitの自動化**: * 添付ファイルやノートに変更があった際、自動的にリモートGitリポジトリへ更新およびプッシュを実行。 * タスク終了後に自動的にシステムのメモリを解放。 * **☁️ マルチストレージバックアップと一方向ミラー同期**: * S3/OSS/R2/WebDAV/ローカル など、複数のストレージプロトコルに対応。 * 全体/差分ZIPスケジュールアーカイブバックアップをサポート。 * Vaultリソースのリモートストレージへの一方向ミラー同期をサポート。 * 有効期限切れのバックアップの自動クリーンアップに対応し、保存期間のカスタマイズが可能。 * **🗄️ マルチデータベース対応**: * SQLite、MySQL、PostgreSQL など主流データベースをネイティブでサポートし、個人からチームまでの多様なデプロイニーズに応えます。 ## ☕ スポンサーとサポート - このプラグインが非常に役立つと感じ、今後も開発を継続してほしい場合は、以下の方法でサポートをご検討ください: | Ko-fi *中国以外の地域* | | WeChat QRコード *中国地域* | |--------------------------------------------------------------------------------------------------|----|------------------------------------------------| | [BuyMeACoffee](https://ko-fi.com/haierkeys) | または | | - サポートリスト: - Support.ja.md - Support.ja.md (cnb.cool ミラー) ## ⏱️ 更新履歴 (Changelog) - ♨️ [更新履歴を確認する](/docs/CHANGELOG.ja.md) ## 🗺️ ロードマップ (Roadmap) - [ ] 各階層を網羅する **Mock** テストの追加。 - [ ] WebSocketの `Protobuf` 転送フォーマットのサポート追加し、同期転送効率を強化。 - [ ] バックエンドに同期ログや操作ログなど、各種ログデータの参照機能を追加。 - [ ] 既存の認証メカニズムの分離および最適化を行い、全体的なセキュリティの向上を図る。 - [ ] WebGuiのノートのリアルタイム更新機能を追加。 - [ ] クライアント間のピアツーピアメッセージ転送機能の追加(ノートおよび添付ファイル以外。localsendのような機能。クライアント側での保存はサポートせず、サーバー側のみ保存可能)。 - [ ] 各種ヘルプドキュメントの充実。 - [ ] より多くの内向きネットワーク接続(中継ゲートウェイ)のサポート。 - [ ] 迅速なデプロイ計画: * サーバーアドレス(パブリックネットワーク)とアカウント、パスワードを提供するだけでFNSサーバーのデプロイが完了する仕組みの構築。 - [ ] 現在のオフラインでのノートの自動統合アルゴリズムを最適化し、競合の解決メカニズムを追加。 継続的に改善を行っており、以下の将来の開発計画があります: > **改善の提案や新しいアイデアがある場合は、issueを送信して私たちと共有してください。内容を慎重に評価し、適切な提案を採用させていただきます。** ## 🚀 迅速なデプロイ(Quick Deployment) 複数のインストール方法が提供されています。**ワンクリックインストールスクリプト** または **Docker** の使用を推奨します。 ### 方法1:ワンクリックインストールスクリプト(推奨) システム環境を自動検出し、インストールとサービスの登録を完了します。 ```bash bash <(curl -fsSL https://raw.githubusercontent.com/haierkeys/fast-note-sync-service/master/scripts/quest_install.sh) ``` 中国地域の場合は、Tencentの `cnb.cool` ミラーリポジトリを使用できます。 ```bash bash <(curl -fsSL https://cnb.cool/haierkeys/fast-note-sync-service/-/git/raw/master/scripts/quest_install.sh) --cnb ``` **スクリプトの主な動作:** * 現在のシステムに最適なReleaseバイナリファイルを自動的にダウンロードします。 * デフォルトで `/opt/fast-note` にインストールされ、`/usr/local/bin/fns` にグローバルなショートカットコマンド `fns` を生成します。 * Systemd(Linux)または Launchd(macOS)のサービスを設定・起動し、PC起動時の自動起動を実現します。 * **管理用コマンド**: `fns [install|uninstall|start|stop|status|update|menu]` * **インタラクティブメニュー**: `fns` を直接実行することで、インタラクティブなメニュー画面を呼び出し、インストール/アップグレード、コントロール、自動起動設定、GitHub / CNBミラー間の切り替えなどをサポートします。 ----- ### 方法2:Dockerでのデプロイ #### Docker Run ```bash # 1. イメージのプル docker pull haierkeys/fast-note-sync-service:latest # 2. コンテナの起動 docker run -tid --name fast-note-sync-service \ -p 9000:9000 \ -v /data/fast-note-sync/storage/:/fast-note-sync/storage/ \ -v /data/fast-note-sync/config/:/fast-note-sync/config/ \ haierkeys/fast-note-sync-service:latest ``` #### Docker Compose `docker-compose.yaml` ファイルを作成します: ```yaml version: '3' services: fast-note-sync-service: image: haierkeys/fast-note-sync-service:latest container_name: fast-note-sync-service restart: always ports: - "9000:9000" # RESTful API & WebSocketポート( /api/user/sync がWebSocketインターフェースのアドレスになります) volumes: - ./storage:/fast-note-sync/storage # データストレージ領域 - ./config:/fast-note-sync/config # 設定ファイル領域 ``` サービスを起動します: ```bash docker compose up -d ``` ----- ### 方法3:手動でのバイナリインストール ご使用のシステムに対応する最新バージョンを [Releases](https://github.com/haierkeys/fast-note-sync-service/releases) からダウンロードし、解凍して以下を実行します: ```bash ./fast-note-sync-service run -c config/config.yaml ``` ## 📖 ご利用ガイド 1. **管理パネルへのアクセス**: ブラウザで `http://{サーバーIP}:9000` にアクセスします。 2. **初期設定**: 初回アクセス時はアカウントの登録が必要です。*(登録機能をオフにしたい場合は、設定ファイルで `user.register-is-enable: false` に設定してください)* 3. **クライアントの設定**: 管理パネルにログインし、**「API設定をコピー(Copy API Configuration)」** をクリックします。 4. **Obsidianへの接続**: Obsidianのプラグイン設定画面を開き、先ほどコピーした設定情報を貼り付けて適用します。 ## ⚙️ 設定に関する説明 デフォルトの設定ファイル「`config.yaml`」は、プログラムによって **ルートディレクトリ** または **config/** ディレクトリで自動的に検索されます。 完全な設定の例を確認する: [config/config.yaml](https://github.com/haierkeys/fast-note-sync-service/blob/master/config/config.yaml) ## 🌐 Nginxリバースプロキシ設定の例 完全な設定の例を確認する: [https-nginx-example.conf](https://github.com/haierkeys/fast-note-sync-service/blob/master/scripts/https-nginx-example.conf) ## 🧰 MCP (Model Context Protocol) サポート FNSは **MCP (Model Context Protocol)** をネイティブサポートしています。 FNSをMCPサーバーとして、Cherry Studio、Cursorなどの互換性のあるAIクライアントに直接接続できます。接続後、AIはプライベートノートや添付ファイルの読み書き能力を備えます。さらに、MCPから発生したすべての変更は、WebSocketを通じてリアルタイムで各デバイス端末に同期されます。 ### アクセス設定 (SSEモード) FNSは **SSEプロトコル** を通じてMCPインターフェースを提供します。一般的なパラメータの要件は次のとおりです: - **インターフェースアドレス**: `http://<あなたのサーバーIPまたはドメイン>:<ポート>/api/mcp/sse` - **認証Header**: `Authorization: Bearer <あなたのAPIトークン>`(WebGUIの「API設定をコピー」機能から取得できます) - **オプションのHeader**: `X-Default-Vault-Name: <ノート Vault の名前>`(MCP 操作でのデフォルトのノート Vault を指定するために使用されます。ツール呼び出し時に `vault` パラメータが指定されていない場合は、これが使用されます) - **オプションのHeader**: `X-Client: <クライアントの種類>`(MCPへの接続に使用するクライアントの種類。例:Cherry Studio / OpenClaw) - **オプションのHeader**: `X-Client-Version: <クライアントのバージョン>`(MCPへの接続に使用するクライアントのバージョン。例:1.1) - **オプションのHeader**: `X-Client-Name: <クライアント名>`(MCPへの接続に指定されたクライアント名。例:Mac) #### 例:Cherry Studio / Cursor / Cline など ご自身のMCPクライアント設定にて、以下の記述をご参考ください: *(注: ``、``、``、および `` を各自の実際の情報に置き換えてください)* ```json { "mcpServers": { "fns": { "url": "http://:/api/mcp/sse", "type": "sse", "headers": { "Content-Type": "application/json", "Authorization": "Bearer ", "X-Default-Vault-Name": "", "X-Client": "", "X-Client-Version": "", "X-Client-Name": "" } } } } ``` ## 🔗 クライアントとクライアントプラグイン * Obsidian Fast Note Sync プラグイン * [Obsidian Fast Note Sync Plugin](https://github.com/haierkeys/obsidian-fast-note-sync) / [cnb.cool ミラー](https://cnb.cool/haierkeys/obsidian-fast-note-sync) * サードパーティクライアント * [FastNodeSync-CLI ](https://github.com/Go1c/FastNodeSync-CLI) PythonおよびFNS WS APIを利用した双方向リアルタイム同期コマンドラインクライアント。GUIを持たないLinuxサーバー(OpenClawなど)向けに特化しており、Obsidianデスクトップやモバイル版に相当する完全な同期能力を提供します。 ================================================ FILE: docs/README.ko.md ================================================ [简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github.com/haierkeys/fast-note-sync-service/blob/master/README.md) / [日本語](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ja.md) / [한국어](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ko.md) / [繁體中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-TW.md) 문제가 발생한 경우, 새로운 [issue](https://github.com/haierkeys/fast-note-sync-service/issues/new)를 생성하시거나 Telegram 커뮤니티 그룹에 가입하여 도움을 요청해 주세요: [https://t.me/obsidian_users](https://t.me/obsidian_users) 중국 본토 지역의 경우, Tencent `cnb.cool`의 미러 리포지토리 사용을 권장합니다: [https://cnb.cool/haierkeys/fast-note-sync-service](https://cnb.cool/haierkeys/fast-note-sync-service)

Fast Note Sync Service

release alpha-release license Go

고성능, 저지연의 노트 동기화, 온라인 관리, 원격 REST API 서비스 플랫폼
Golang + Websocket + React 기반으로 구축

데이터를 사용하려면 클라이언트 플러그인과 함께 사용해야 합니다: Obsidian Fast Note Sync Plugin

fast-note-sync-service-preview fast-note-sync-service-preview
fast-note-sync-service-preview fast-note-sync-service-preview
--- ## 🎯 핵심 기능 * **🧰 MCP (Model Context Protocol) 기본 지원**: * `FNS`는 MCP 서버로서 `Cherry Studio`, `Cursor`와 같은 호환 가능한 AI 클라이언트에 연결할 수 있습니다. 이를 통해 AI가 사용자 개인 노트 및 첨부 파일을 읽고 쓸 수 있는 기능을 갖게 되며, 모든 변경 사항은 실시간으로 각 클라이언트 디바이스와 동기화됩니다. * **🚀 REST API 지원**: * 표준 REST API 인터페이스를 제공하며, 자동화 스크립트나 AI 어시스턴트 통합 같은 프로그래밍 방식으로 Obsidian 노트를 생성, 읽기, 수정 및 삭제(CRUD)할 수 있도록 지원합니다. * 자세한 내용은 [RESTful API 문서](/docs/REST_API.md) 또는 [OpenAPI 문서](/docs/swagger.yaml)를 참조해 주세요. * **💻 웹 관리 패널**: * 최신 관리 인터페이스가 내장되어 있어, 사용자 생성, 플러그인 구성 생성, Vault(저장소) 및 노트 내용을 쉽게 관리할 수 있습니다. * **🔄 다중 장치 노트 동기화**: * **Vault (저장소)** 자동 생성을 지원합니다. * 노트 관리(추가, 삭제, 수정, 조회)를 지원하며, 변경 사항은 밀리초 단위로 온라인 상태의 모든 장치에 실시간 분산 전송됩니다. * **🖼️ 첨부 파일 동기화 지원**: * 이미지 등 노트 외 파일의 동기화를 완벽하게 지원합니다. * 큰 첨부 파일의 분할 업로드/다운로드를 지원하며 분할 크기도 설정할 수 있으므로, 동기화 효율을 향상시켰습니다. * **⚙️ 설정 동기화**: * `.obsidian` 설정 파일의 동기화를 지원합니다. * `PDF` 진행 상태 동기화를 지원합니다. * **📝 노트 내역 기능**: * 웹 관리 화면과 클라이언트 플러그인 애플리케이션 내에서 각 노트의 과거 수정 버전을 확인할 수 있습니다. * (서버 v1.2+ 버전 필요) * **🗑️ 휴지통 기능**: * 노트를 삭제한 후 이가 휴지통에 자동으로 들어가는 것을 지원합니다. * 휴지통에서 노트를 복구하는 기능을 지원합니다. (첨부 파일의 복구 기능도 지속적으로 추가될 예정입니다) * **🚫 오프라인 동기화 모드 전략**: * 오프라인 상태에서 노트 편집에 대해 자동으로 병합하는 것을 지원합니다. (플러그인 옵션 설정이 필요합니다) * 오프라인에서 항목을 삭제한 경우, 재연결 후 서버측에서 자동으로 삭제된 노트를 보완하거나 삭제된 로컬과 동기화합니다. (플러그인 설정 필요) * **🔗 공유 기능**: * 노트의 '공유'에 대한 생성 및 취소가 가능합니다. * 공유 노트에 삽입된 이미지, 오디오, 비디오 등 첨부 파일을 자동으로 분석합니다. * 공유 노트 접속 통계 기능을 제공합니다. * 공유 노트에 접속 암호를 설정할 수 있습니다. * 공유된 노트에 대한 짧은 링크 생성을 지원합니다. * **📂 디렉토리(폴더) 동기화**: * 폴더의 생성, 이름 변경, 이동, 삭제 항목의 동기화를 지원합니다. * **🌳 Git 자동화**: * 노트 및 파일에 변경이 있을 때 자동으로 Git 원격 저장소로 내역을 업데이트하고 푸시 작업을 수행합니다. * 모든 동기화 작업이 종료된 후, 메모리를 반납하여 시스템 활용성을 높입니다. * **☁️ 다중 스토리지 백업 및 단방향 미러 동기화**: * S3, OSS, R2, WebDAV, 로컬 볼륨 등의 다양한 스토리지 프로토콜과 호환됩니다. * 전체 / 증분 ZIP 예약 보관 데이터 백업을 수행할 수 있습니다. * Vault 리소스를 원격 스토리지에 대해 단방향 미러 동기화가 가능합니다. * 기간이 만료된 백업을 자동으로 정리하고, 사용자가 필요한 보존 날짜(유지 보관일수) 지정을 지원합니다. * **🗄️ 다중 데이터베이스 지원**: * SQLite, MySQL, PostgreSQL 등 주류 데이터베이스를 네이티브로 지원하여, 개인 단위부터 팀 협업 모델까지 필요한 배포 요구를 수용합니다. ## ☕ 후원 및 지원 - 이 플러그인이 유용하며 개발이 지속되기를 원하신다면 다음 채널을 통해 후원 부탁드립니다: | Ko-fi *중국 이외 지역* | | WeChat QR 기부 *중국 지역* | |--------------------------------------------------------------------------------------------------|----|------------------------------------------------| | [BuyMeACoffee](https://ko-fi.com/haierkeys) | 또는 | | - 스폰서 목록: - Support.ko.md - Support.ko.md (cnb.cool 미러) ## ⏱️ 업데이트 로그 (Changelog) - ♨️ [업데이트 로그 확인하기](/docs/CHANGELOG.ko.md) ## 🗺️ 로드맵 (Roadmap) - [ ] 각 레이어에 대대적인 **Mock** 테스트 추가. - [ ] WebSocket 계층에서 `Protobuf` 통신 프로토콜을 구현하여 동기화 전송 효율을 향상. - [ ] 백엔드 단에서 동기화 로그 및 동작 로그 등, 시스템 및 유저 감사/운영 기록에 대해 조회가 가능한 기능 생성. - [ ] 전체 보안을 최상위 상태로 끌어올리도록, 현행 인증 메커니즘을 격리하고 최적화를 달성. - [ ] WebGui에서도 노트 업데이트 내역을 실시간으로 반영 가능. - [ ] 클라이언트 간 P2P (Peer-to-Peer) 메시지 전송 기능 추가 (노트 & 첨부파일이 아님, localsend 방식. 클라이언트 간의 저장 없이 서버에 기록 보전됨) - [ ] 각 분야의 도움말 / 사용 설명서 개선. - [ ] 인트라넷 환경에서의 우회 액세스 기능 강화 (포트 포워딩 릴레이 지원 등 포함). - [ ] 빠른 시스템 구축 및 배포 계획: * 인프라 서버의 IP(공용 IP)와 계정 패스워드만을 공급하면 단숨에 FNS 서버를 런칭하고 운용. - [ ] 현존하는 오프라인 노트 병합 전략을 대폭 개선하고 충돌 처리 알고리즘 추가 반영. 우리는 지속적으로 개선 작업을 진행 중이며, 앞으로의 개발 계획은 다음과 같습니다: > **개선 아이디어나 새로운 제안이 있을 때 Issue를 남겨서 우리에게 의견을 남겨주시면 감사드리겠습니다. 우리는 제출받은 각 항목에 대해 신중히 평가하여 적합한 의견을 도용할 것입니다.** ## 🚀 빠른 배포 가이드 우리는 사용자의 설치 편의성을 고려하여 다양한 옵션을 수록하였습니다.가장 권장하는 방식은 **원클릭 스크립트**와 **Docker** 설치 방식입니다. ### 첫 번째: 원클릭 스크립트 (권장) 시스템 환경 요소의 사양을 자체적으로 검색하여 필수 프로그램을 설치 및 최적의 서비스 등록을 구성합니다. ```bash bash <(curl -fsSL https://raw.githubusercontent.com/haierkeys/fast-note-sync-service/master/scripts/quest_install.sh) ``` 중국 대륙 내부에서는 Tencent `cnb.cool`의 미러 다운로드 권한을 사용하십시오: ```bash bash <(curl -fsSL https://cnb.cool/haierkeys/fast-note-sync-service/-/git/raw/master/scripts/quest_install.sh) --cnb ``` **스크립트 동작 가이드 프로세스:** * 가장 최신의 설치 환경 내장 Release 바이너리 응용프로그램을 다운로드합니다. * 기본 디폴트 경로(/opt/fast-note)로 파일들이 압축 해제되며, 시스템 환경변수에 단축 명령인 `fns`(/usr/local/bin/fns)를 저장합니다. * Systemd (Linux 기반) 및 Launchd (macOS 환경) 등에 알맞게 서비스가 환경에 저장되어 부수적인 절차 없이 운영 체제가 런칭할 때 함께 자사 프로그램이 동작합니다. * **전역 애플리케이션 명령어**: `fns [install|uninstall|start|stop|status|update|menu]` * **대화식 콘솔창 지원 기능**: 구동 중인 앱에다 추가 파라미터가 배제된 `fns` 명령을 입력하게 되면 대화형의 화면 기반 콘솔이 뜹니다. 설치, 업데이트/앱 제어/자동 실행 컨테이너 적용 외 GitHub 및 CNB 클라우드 리포지토리 미러 간 변경이 손쉽게 작동 가능합니다. ----- ### 두 번째: Docker 배포 구성방식 #### Docker Run ```bash # 1. 컨테이너 이미지 불러오기 docker pull haierkeys/fast-note-sync-service:latest # 2. 이미지 컨테이너 기동시키기 docker run -tid --name fast-note-sync-service \ -p 9000:9000 \ -v /data/fast-note-sync/storage/:/fast-note-sync/storage/ \ -v /data/fast-note-sync/config/:/fast-note-sync/config/ \ haierkeys/fast-note-sync-service:latest ``` #### Docker Compose 로컬 컴퓨터에 `docker-compose.yaml` 파일을 하나 구축합니다: ```yaml version: '3' services: fast-note-sync-service: image: haierkeys/fast-note-sync-service:latest container_name: fast-note-sync-service restart: always ports: - "9000:9000" # RESTful API & WebSocket 통신 포트 / 내부 설정상 /api/user/sync를 웹소켓 파이프라인으로 운용합니다 volumes: - ./storage:/fast-note-sync/storage # 데이터/볼륨 스토리지 연동영역 - ./config:/fast-note-sync/config # 클라우드/컨테이너 시스템을 위한 Config 마운트 ``` 서비스 작동: ```bash docker compose up -d ``` ----- ### 세 번째: 수동 바이너리 설치 본인의 운영 체제 버전과 일치하는 팩을 [Releases](https://github.com/haierkeys/fast-note-sync-service/releases)에서 다운로드 받은 후 적절한 디렉토리에서 스크립트를 기동하십시오: ```bash ./fast-note-sync-service run -c config/config.yaml ``` ## 📖 사용 설명서 1. **관리자 화면 접속**: 각자의 브라우저 주소창에 `http://{운용 중인 서버 IP 주소}:9000` 로컬 네트워크 연결. 2. **프로젝트 가동 세팅 구성**: 최초 접속 시 계정 생성이 필수로 적용됩니다. *(혹시 외부 가입 통제를 기획 중인 관리자는 서버 Config 파일에 `user.register-is-enable: false` 입력 권장)* 3. **클라이언트와의 API 조인 연동법**: 서버 관리 콘솔/로그인 후 우측의 **“API 복사 (Copy API Configuration)”** 버튼 누름. 4. **Obsidian 옵션 조립 및 세팅**: Obsidian 플러그인 설정 창의 셋업 페이지로 향한 후 방금 복사해 둔 환경 데이터를 그대로 붙여 넣기. ## ⚙️ 설정 파일 정보 서버 엔진의 기본 파일 양식 설정 명칭은 « `config.yaml` »이며, 이 프로그램은 환경 디렉터리를 가동 중인 **메인 Root 공간**이거나 자체 **config/**라는 환경 설정 디렉터리 내에서 자동 스캐닝하게 됩니다. Configuration 샘플 코드를 점검 및 학습하십시오: [config/config.yaml](https://github.com/haierkeys/fast-note-sync-service/blob/master/config/config.yaml) ## 🌐 Nginx 리버스 프록시 적용 예제 리버스 프록시(HTTPS 접속 용도) 전체 적용 사례 열람 안내: [https-nginx-example.conf](https://github.com/haierkeys/fast-note-sync-service/blob/master/scripts/https-nginx-example.conf) ## 🧰 MCP (Model Context Protocol) 지원 FNS는 현재 **MCP (Model Context Protocol)**를 기본적으로 지원합니다. FNS를 MCP 서버로 설치하여 Cherry Studio, Cursor 등과 같이 호환되는 AI 클라이언트에 직접 접속하게 연동시킬 수 있습니다. 연동 시점 직후, AI 개체 및 서비스 엔진은 사유 데이터(개인 비메모 및 관련 어태치먼트)의 읽기, 편집 역할을 갖게 됩니다. 더불어 MCP로부터 송부되는 일련의 작업 결과들은 웹 소켓을 경유해 사용 중인 기타의 모든 장치 환경에 실시간 주입 및 저장 단계를 밟습니다. ### 접속 구성 안내 (SSE 모드) FNS 엔진은 다용도 서버로서 MCP 호출 및 기능을 원격지의 사용자에게 제공할수 있게 **SSE 프로토콜**을 할당합니다. 접속 통신의 요구 파라미터는 다음과 같이 지정되었습니다: - **인터페이스 주소(URL)**: `http://<호스트 서버의 IP 주소 및 도메인 주소명>:<포트>/api/mcp/sse` - **강화 인증 (Header)**: `Authorization: Bearer ` - **추가 옵션 (Header)**: `X-Default-Vault-Name: <선택한 노트 Vault 명칭>` (MCP 동작 시의 기본 타깃 Vault 구성 목적 / 호출 시점에서 `vault` 변수가 공백일 케이스에 이 구성값이 대체적용) - **추가 옵션 (Header)**: `X-Client: <클라이언트 종류>` (MCP 연결에 활용하는 구체적인 클라이언트 유형. 예: Cherry Studio / OpenClaw) - **추가 옵션 (Header)**: `X-Client-Version: <클라이언트 버전>` (MCP에 연결되는 클라이언트 서비스 종류의 버전값. 예: 1.1) - **추가 옵션 (Header)**: `X-Client-Name: <클라이언트 이름>` (서버 연결을 기동시키는 플랫폼/클라이언트의 운영 명칭. 예: Mac) #### 예시: Cherry Studio / Cursor / Cline 등 세팅 가이드 운용하시려는 MCP 도메인 서비스 측의 애플리케이션 접속 클라이언트 측에 하단의 설정 코드를 병기 및 수정 후 추가 기입할 것을 권장합니다: *(안내: 파라미터로 지정된 코드 베이스 내의 ``, ``, ``, 그리고 ``의 위치에 이용자 각자의 적합도 정보를 적으십시오)* ```json { "mcpServers": { "fns": { "url": "http://:/api/mcp/sse", "type": "sse", "headers": { "Content-Type": "application/json", "Authorization": "Bearer ", "X-Default-Vault-Name": "", "X-Client": "", "X-Client-Version": "", "X-Client-Name": "" } } } } ``` ## 🔗 클라이언트 및 클라이언트 플러그인 지원 목록 * Obsidian Fast Note Sync 플러그인 * [Obsidian Fast Note Sync Plugin](https://github.com/haierkeys/obsidian-fast-note-sync) / [cnb.cool 미러 저장소](https://cnb.cool/haierkeys/obsidian-fast-note-sync) * 서드 파티 연동 플러그인 * [FastNodeSync-CLI ](https://github.com/Go1c/FastNodeSync-CLI) 파이썬(Python)의 기능과 백엔드 FNS WS 인터페이스 기술에 결합되어 제작된, 쌍방향 실시간 동기화를 달성한 CLI(명령 프롬프트 베이스)의 클라이언트 운영 체제 모듈입니다. 이는 디스플레이나 구체적 인터페이스 화면(GUI 환경)을 미처 지원하지 못해 구동 환경이 제한되는 서버 콘솔 환경(OpenClaw) 내에서 데스크톱 기반 Obsidian 동기화 능력을 확보하는데 최적화되었습니다. ================================================ FILE: docs/README.zh-CN.md ================================================ [简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github.com/haierkeys/fast-note-sync-service/blob/master/README.md) / [日本語](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ja.md) / [한국어](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ko.md) / [繁體中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-TW.md) 有问题请新建 [issue](https://github.com/haierkeys/fast-note-sync-service/issues/new) , 或加入电报交流群寻求帮助: [https://t.me/obsidian_users](https://t.me/obsidian_users) 中国大陆地区,推荐使用腾讯 `cnb.cool` 镜像库: [https://cnb.cool/haierkeys/fast-note-sync-service](https://cnb.cool/haierkeys/fast-note-sync-service)

Fast Note Sync Service

release alpha-release license Go

高性能、低延迟的笔记同步, 在线管理, 远端 REST API 服务平台
基于 Golang + Websocket + React 构建

数据提供需配合客户端插件使用:Obsidian Fast Note Sync Plugin

fast-note-sync-service-preview fast-note-sync-service-preview
fast-note-sync-service-preview fast-note-sync-service-preview
--- ## 🎯 核心功能 * **🧰 MCP (Model Context Protocol) 原生支持**: * `FNS` 可以作为 MCP 服务端接入 `Cherry Studio`、`Cursor` 等兼容的 AI 客户端,即可让 AI 具备读写私人笔记与附件的能力,且所有变更实时同步到各端。 * **🚀 REST API 支持**: * 提供标准的 REST API 接口,支持通过编程方式(如自动化脚本、AI 助手集成)对 Obsidian 笔记进行增删改查。 * 详情请参阅 [RESTful API 文档](/docs/REST_API.md) 或 [OpenAPI 文档](/docs/swagger.yaml)。 * **💻 Web 管理面板**: * 内置现代化管理界面,轻松创建用户、生成插件配置、管理仓库及笔记内容。 * **🔄 多端笔记同步**: * 支持 **Vault (仓库)** 自动创建。 * 支持笔记管理(增、删、改、查),变更毫秒级实时分发至所有在线设备。 * **🖼️ 附件同步支持**: * 完美支持图片等非笔记文件同步。 * 支持大附件 分片上传下载,分片大小可配置,提升同步效率。 * **⚙️ 配置同步**: * 支持 `.obsidian` 配置文件的同步。 * 支持 `PDF` 进度状态同步。 * **📝 笔记历史**: * 可以在 Web 页面,插件端查看每一个笔记的 历史修改版本。 * (需服务端 v1.2+ ) * **🗑️ 回收站**: * 支持笔记删除后,自动进入回收站。 * 支持从回收站恢复笔记。(后续会陆续新增附件恢复功能) * **🚫 离线同步策略**: * 支持笔记离线编辑自动合并。(需要插件端设置) * 离线删除,重连之后自动补全或删除同步。(需要插件端设置) * **🔗 分享功能**: * 可以 创建/取消 笔记分享。 * 自动解析分享笔记中引用的图片、音视频等附件。 * 提供分享访问统计功能。 * 可以设置分享笔记的访问密码。 * 可以对分享笔记生成短链接。 * **📂 目录同步**: * 支持文件夹的 创建/重命名/移动/删除 同步。 * **🌳 Git 自动化**: * 当附件和笔记发生变更时,自动更新并推送至远程 Git 仓库。 * 任务结束后自动释放系统内存。 * **☁️ 多存储备份与单向镜像同步**: * 适配 S3/OSS/R2/WebDAV/本地 等多种存储协议。 * 支持全量/增量 ZIP 定时归档备份。 * 支持 Vault 资源单向镜像同步至远程存储。 * 自动清理过期备份,支持自定义保留天数。 * **🗄️ 多数据库支持**: * 原生支持 SQLite、MySQL、PostgreSQL 等多种主流数据库,满足从个人到团队的不同部署需求。 ## ☕ 赞助与支持 - 如果觉得这个插件很有用,并且想要它继续开发,请在以下方式支持我: | Ko-fi *非中国地区* | | 微信扫码打赏 *中国地区* | |--------------------------------------------------------------------------------------------------|----|------------------------------------------------| | [BuyMeACoffee](https://ko-fi.com/haierkeys) | 或 | | - 已支持名单: - Support.zh-CN.md - Support.zh-CN.md (cnb.cool 镜像库) ## ⏱️ 更新日志 - ♨️ [访问查看更新日志](/docs/CHANGELOG.zh-CN.md) ## 🗺️ 路线图 (Roadmap) - [ ] 增加 **Mock**测试, 覆盖到 各层级. - [ ] 增加 WebSocket `Protobuf` 传输格式的支持, 强化同步传输效率. - [ ] 后端增加 同步日志 & 操作日志 等各类操作日志的查询. - [ ] 对现有授权机制进行隔离以及优化, 提升整体安全性. - [ ] 增加 WebGui 笔记实时更新 - [ ] 增加客户端 点对点 消息传送(非笔记&附件,类似localsend功能,不支持客户端保存, 可保存到服务端) - [ ] 各类帮助文档完善 - [ ] 更多的内网穿透(中继网关)的支持 - [ ] 快速部署计划 * 只需要提供服务器地址(公网), 账号密码 即可完成 FNS 服务端的部署 - [ ] 优化现有的离线笔记合并方案, 增加冲突处理机制 我们正在持续改进,以下是未来的开发计划: > **如果您有改进建议或新想法,欢迎通过提交 issue 与我们分享——我们会认真评估并采纳合适的建议。** ## 🚀 快速部署 我们提供多种安装方式,推荐使用 **一键脚本** 或 **Docker**。 ### 方式一:一键脚本(推荐) 自动检测系统环境并完成安装、服务注册。 ```bash bash <(curl -fsSL https://raw.githubusercontent.com/haierkeys/fast-note-sync-service/master/scripts/quest_install.sh) ``` 中国地区可以使用腾讯 `cnb.cool` 镜像源 ```bash bash <(curl -fsSL https://cnb.cool/haierkeys/fast-note-sync-service/-/git/raw/master/scripts/quest_install.sh) --cnb ``` **脚本主要行为:** * 自动下载适配当前系统的 Release 二进制文件。 * 默认安装至 `/opt/fast-note`,并在 `/usr/local/bin/fns` 创建全局快捷命令 `fns`。 * 配置并启动 Systemd(Linux)或 Launchd(macOS)服务,实现开机自启。 * **管理命令**:`fns [install|uninstall|start|stop|status|update|menu]` * **交互菜单**:直接运行 `fns` 可进入交互菜单,支持安装/升级、服务控制、开机自启配置,以及在 GitHub / CNB 镜像之间切换。 ----- ### 方式二:Docker 部署 #### Docker Run ```bash # 1. 拉取镜像 docker pull haierkeys/fast-note-sync-service:latest # 2. 启动容器 docker run -tid --name fast-note-sync-service \ -p 9000:9000 \ -v /data/fast-note-sync/storage/:/fast-note-sync/storage/ \ -v /data/fast-note-sync/config/:/fast-note-sync/config/ \ haierkeys/fast-note-sync-service:latest ``` #### Docker Compose 创建 `docker-compose.yaml` 文件: ```yaml version: '3' services: fast-note-sync-service: image: haierkeys/fast-note-sync-service:latest container_name: fast-note-sync-service restart: always ports: - "9000:9000" # RESTful API & WebSocket 端口 其中 /api/user/sync 为 WebSocket 接口地址 volumes: - ./storage:/fast-note-sync/storage # 数据存储 - ./config:/fast-note-sync/config # 配置文件 ``` 启动服务: ```bash docker compose up -d ``` ----- ### 方式三:手动二进制安装 从 [Releases](https://github.com/haierkeys/fast-note-sync-service/releases) 下载对应系统的最新版本,解压后运行: ```bash ./fast-note-sync-service run -c config/config.yaml ``` ## 📖 使用指南 1. **访问管理面板**: 在浏览器打开 `http://{服务器IP}:9000`。 2. **初始化设置**: 首次访问需注册账号。*(如需关闭注册功能,请在配置文件中设置 `user.register-is-enable: false`)* 3. **配置客户端**: 登录管理面板,点击 **“复制 API 配置”**。 4. **连接 Obsidian**: 打开 Obsidian 插件设置页面,粘贴刚才复制的配置信息即可。 ## ⚙️ 配置说明 默认配置文件为 `config.yaml`,程序会自动在 **根目录** 或 **config/** 目录下查找。 查看完整配置示例:[config/config.yaml](https://github.com/haierkeys/fast-note-sync-service/blob/master/config/config.yaml) ## 🌐 Nginx 反代配置示例 查看完整配置示例:[https-nginx-example.conf](https://github.com/haierkeys/fast-note-sync-service/blob/master/scripts/https-nginx-example.conf) ## 🧰 MCP (模型上下文协议) 支持 FNS 现已原生支持 **MCP (Model Context Protocol)**。 您可以将 FNS 作为 MCP 服务端直接接入 Cherry Studio、Cursor 等兼容的 AI 客户端。接入后,AI 即可具备读写私人笔记和附件的能力。同时,所有由 MCP 产生的修改,都会通过 WebSocket 实时同步到您的各个设备终端。 ### 接入配置 (SSE 模式) FNS 通过 **SSE 协议**提供 MCP 接口,通用参数要求如下: - **接口地址**:`http://<您的服务器IP或域名>:<端口>/api/mcp/sse` - **鉴权 Header**:`Authorization: Bearer <您的 API Token>`(在 WebGUI 的复制 API 配置中获取) - **可选 Header**:`X-Default-Vault-Name: <笔记库名称>`(用于指定 MCP 操作的默认笔记库,若工具调用时未指定 `vault` 参数,则使用此值) - **可选 Header**:`X-Client: <客户端类型>`(用于连接MCP的客户端类型,如:Cherry Studio / OpenClaw) - **可选 Header**:`X-Client-Version: <客户端类型版本>`(用于连接MCP的客户端类型版本,如:1.1) - **可选 Header**:`X-Client-Name: <客户端名称>`(用于连接MCP的客户端名称,如: Mac) #### 示例:Cherry Studio / Cursor / Cline 等 请在您的 MCP 客户端配置中参考如下配置: *(注:请将 ``、``、`` 和 `` 替换为您自己的实际信息)* ```json { "mcpServers": { "fns": { "url": "http://:/api/mcp/sse", "type": "sse", "headers": { "Content-Type": "application/json", "Authorization": "Bearer ", "X-Default-Vault-Name": "", "X-Client": "", "X-Client-Version": "", "X-Client-Name": "" } } } } ``` ## 🔗 客户端 & 客户端插件 * Obsidian Fast Note Sync 插件 * [Obsidian Fast Note Sync Plugin](https://github.com/haierkeys/obsidian-fast-note-sync) / [cnb.cool 镜像库](https://cnb.cool/haierkeys/obsidian-fast-note-sync) * 三方客户端 * [FastNodeSync-CLI ](https://github.com/Go1c/FastNodeSync-CLI) 基于 Python 和 FNS WS接口实现的双向实时同步的命令行客户端, 适用于无 GUI 的 Linux 服务器环境(如 OpenClaw),实现与 Obsidian 桌面/移动端等价的同步能力。 ================================================ FILE: docs/README.zh-TW.md ================================================ [简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github.com/haierkeys/fast-note-sync-service/blob/master/README.md) / [日本語](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ja.md) / [한국어](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.ko.md) / [繁體中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-TW.md) 有問題請新建 [issue](https://github.com/haierkeys/fast-note-sync-service/issues/new) , 或加入電報交流群尋求幫助: [https://t.me/obsidian_users](https://t.me/obsidian_users) 中國大陸地區,推薦使用騰訊 `cnb.cool` 鏡像庫: [https://cnb.cool/haierkeys/fast-note-sync-service](https://cnb.cool/haierkeys/fast-note-sync-service)

Fast Note Sync Service

release alpha-release license Go

高效能、低延遲的筆記同步,線上管理,遠端 REST API 服務平台
基於 Golang + Websocket + React 建構

資料提供需配合用戶端外掛程式使用:Obsidian Fast Note Sync Plugin

fast-note-sync-service-preview fast-note-sync-service-preview
fast-note-sync-service-preview fast-note-sync-service-preview
--- ## 🎯 核心功能 * **🧰 MCP (Model Context Protocol) 原生支援**: * `FNS` 可以作為 MCP 伺服器端接入 `Cherry Studio`、`Cursor` 等相容的 AI 用戶端,即可讓 AI 具備讀寫私人筆記與附件的能力,且所有變更會即時同步到各端。 * **🚀 REST API 支援**: * 提供標準的 REST API 介面,支援透過程式語言方式(如自動化腳本、AI 助手整合)對 Obsidian 筆記進行增刪改查。 * 詳情請參閱 [RESTful API 文件](/docs/REST_API.md) 或 [OpenAPI 文件](/docs/swagger.yaml)。 * **💻 Web 管理面板**: * 內建現代化管理介面,輕鬆建立使用者、產生外掛程式設定、管理倉庫及筆記內容。 * **🔄 多端筆記同步**: * 支援 **Vault (筆記庫)** 自動建立。 * 支援筆記管理(增、刪、改、查),變更毫秒級即時分發至所有線上設備。 * **🖼️ 附件同步支援**: * 完美支援圖片等非筆記檔案同步。 * 支援大附件 分區塊上傳下載,區塊大小可設定,提升同步效率。 * **⚙️ 設定同步**: * 支援 `.obsidian` 設定檔的同步。 * 支援 `PDF` 閱讀進度狀態同步。 * **📝 筆記歷史**: * 可以在 Web 頁面,外掛程式端查看每一個筆記的 歷史修改版本。 * (需伺服器端 v1.2+ 支援) * **🗑️ 資源回收筒**: * 支援筆記刪除後,自動進入資源回收筒。 * 支援從資源回收筒復原筆記。(後續會陸續新增附件復原功能) * **🚫 離線同步策略**: * 支援筆記離線編輯自動合併。(需要外掛程式端設定) * 離線刪除,重連之後自動補全或刪除同步。(需要外掛程式端設定) * **🔗 分享功能**: * 可以 建立/取消 筆記分享。 * 自動解析分享筆記中引用的圖片、音訊與視訊等附件。 * 提供分享存取統計功能。 * 可以設定分享筆記的存取密碼。 * 可以對分享筆記產生短連結。 * **📂 目錄同步**: * 支援資料夾的 建立/重新命名/移動/刪除 同步。 * **🌳 Git 自動化**: * 當附件和筆記發生變更時,自動更新並推播至遠端 Git 倉庫。 * 任務結束後自動釋放系統記憶體。 * **☁️ 多儲存備份與單向鏡像同步**: * 適配 S3/OSS/R2/WebDAV/本地端 等多種儲存協定。 * 支援全量/增量 ZIP 定時封存備份。 * 支援 Vault 資源單向鏡像同步至遠端儲存。 * 自動清理過期備份,支援自訂保留天數。 * **🗄️ 多資料庫支援**: * 原生支援 SQLite、MySQL、PostgreSQL 等多種主流資料庫,滿足從個人到團隊的不同部署需求。 ## ☕ 贊助與支援 - 如果覺得這個外掛程式很有用,並且想要它繼續開發,請在以下方式支持我: | Ko-fi *非中國地區* | | 微信掃碼打賞 *中國地區* | |--------------------------------------------------------------------------------------------------|----|------------------------------------------------| | [BuyMeACoffee](https://ko-fi.com/haierkeys) | 或 | | - 已支持名單: - Support.zh-TW.md - Support.zh-TW.md (cnb.cool 鏡像庫) ## ⏱️ 更新日誌 - ♨️ [點擊檢視更新日誌](/docs/CHANGELOG.zh-TW.md) ## 🗺️ 路線圖 (Roadmap) - [ ] 增加 **Mock**測試, 覆蓋到 各層級。 - [ ] 增加 WebSocket `Protobuf` 傳輸格式的支援, 強化同步傳輸效率。 - [ ] 後端增加 同步日誌 & 操作日誌 等各類操作日誌的查詢。 - [ ] 對現有授權機制進行隔離以及最佳化, 提升整體安全性。 - [ ] 增加 WebGui 筆記即時更新 - [ ] 增加用戶端 點對點 訊息傳送 (非筆記 & 附件, 類似 localsend 功能, 不支援用戶端保存, 可保存到伺服器端) - [ ] 各類幫助文件完善 - [ ] 更多的內網穿透 (中繼閘道)的支援 - [ ] 快速部署計畫 * 只需要提供伺服器網址 (公網),帳號密碼 即可完成 FNS 伺服器端的部署 - [ ] 最佳化現有的離線筆記合併方案, 增加衝突處理機制 我們正在持續改進,以下是未來的開發計畫: > **如果您有改進建議或新想法,歡迎透過提交 issue 與我們分享——我們會認真評估並採納合適的建議。** ## 🚀 快速部署 我們提供多種安裝方式,推薦使用 **一鍵腳本** 或 **Docker**。 ### 方式一:一鍵腳本(推薦) 自動檢測系統環境並完成安裝、服務註冊。 ```bash bash <(curl -fsSL https://raw.githubusercontent.com/haierkeys/fast-note-sync-service/master/scripts/quest_install.sh) ``` 中國地區可以使用騰訊 `cnb.cool` 鏡像源 ```bash bash <(curl -fsSL https://cnb.cool/haierkeys/fast-note-sync-service/-/git/raw/master/scripts/quest_install.sh) --cnb ``` **腳本主要行為:** * 自動下載適配當前系統的 Release 二進位檔案。 * 預設安裝至 `/opt/fast-note`,並在 `/usr/local/bin/fns` 建立全域快捷命令 `fns`。 * 設定並啟動 Systemd (Linux) 或 Launchd (macOS) 服務,實現開機自啟動。 * **管理命令**:`fns [install|uninstall|start|stop|status|update|menu]` * **互動式選單**:直接執行 `fns` 可進入互動式選單,支援安裝/升級、服務控制、開機自啟動設定,以及在 GitHub / CNB 鏡像來源之間切換。 ----- ### 方式二:Docker 部署 #### Docker Run ```bash # 1. 抓取映像檔 docker pull haierkeys/fast-note-sync-service:latest # 2. 啟動容器 docker run -tid --name fast-note-sync-service \ -p 9000:9000 \ -v /data/fast-note-sync/storage/:/fast-note-sync/storage/ \ -v /data/fast-note-sync/config/:/fast-note-sync/config/ \ haierkeys/fast-note-sync-service:latest ``` #### Docker Compose 建立 `docker-compose.yaml` 檔案: ```yaml version: '3' services: fast-note-sync-service: image: haierkeys/fast-note-sync-service:latest container_name: fast-note-sync-service restart: always ports: - "9000:9000" # RESTful API & WebSocket 連接埠 其中 /api/user/sync 為 WebSocket API 網址 volumes: - ./storage:/fast-note-sync/storage # 資料儲存 - ./config:/fast-note-sync/config # 設定檔 ``` 啟動服務: ```bash docker compose up -d ``` ----- ### 方式三:手動二進位安裝 從 [Releases](https://github.com/haierkeys/fast-note-sync-service/releases) 下載對應系統的最新版本,解壓縮後執行: ```bash ./fast-note-sync-service run -c config/config.yaml ``` ## 📖 使用指南 1. **存取管理面板**: 在瀏覽器開啟 `http://{伺服器IP}:9000`。 2. **初始化設定**: 首次存取需註冊帳號。*(如需關閉註冊功能,請在設定檔中設定 `user.register-is-enable: false`)* 3. **設定用戶端**: 登入管理面板,點擊 **「複製 API 設定」**。 4. **連接 Obsidian**: 打開 Obsidian 外掛程式設定頁面,貼上剛才複製的設定資訊即可。 ## ⚙️ 設定說明 預設設定檔為 `config.yaml`,程式會自動在 **根目錄** 或 **config/** 目錄下尋找。 查看完整設定範例:[config/config.yaml](https://github.com/haierkeys/fast-note-sync-service/blob/master/config/config.yaml) ## 🌐 Nginx 反向代理設定範例 查看完整設定範例:[https-nginx-example.conf](https://github.com/haierkeys/fast-note-sync-service/blob/master/scripts/https-nginx-example.conf) ## 🧰 MCP (模型上下文協定) 支援 FNS 現已原生支援 **MCP (Model Context Protocol)**。 您可以將 FNS 作為 MCP 伺服器端直接接入 Cherry Studio、Cursor 等相容的 AI 用戶端。接入後,AI 即可具備讀寫私人筆記和附件的能力。同時,所有由 MCP 產生的修改,都會透過 WebSocket 即時同步到您的各個設備終端。 ### 接入設定 (SSE 模式) FNS 透過 **SSE 協定**提供 MCP 介面,通用參數要求如下: - **介面網址**:`http://<您的伺服器IP或網域>:<連接埠>/api/mcp/sse` - **鑑權 Header**:`Authorization: Bearer <您的 API Token>`(在 WebGUI 的複製 API 設定中取得) - **選填 Header**:`X-Default-Vault-Name: <筆記庫名稱>`(用於指定 MCP 操作的預設筆記庫,若工具呼叫時未指定 `vault` 參數,則使用此值) - **選填 Header**:`X-Client: <用戶端類型>`(用於連接 MCP 的用戶端類型,如:Cherry Studio / OpenClaw) - **選填 Header**:`X-Client-Version: <用戶端類型版本>`(用於連接 MCP 的用戶端類型版本,如:1.1) - **選填 Header**:`X-Client-Name: <用戶端名稱>`(用於連接 MCP 的用戶端名稱,如:Mac) #### 範例:Cherry Studio / Cursor / Cline 等 請在您的 MCP 用戶端設定中參考如下設定: *(註:請將 ``、``、`` 和 `` 替換為您自己的實際資訊)* ```json { "mcpServers": { "fns": { "url": "http://:/api/mcp/sse", "type": "sse", "headers": { "Content-Type": "application/json", "Authorization": "Bearer ", "X-Default-Vault-Name": "", "X-Client": "", "X-Client-Version": "", "X-Client-Name": "" } } } } ``` ## 🔗 用戶端 & 用戶端外掛程式 * Obsidian Fast Note Sync 外掛程式 * [Obsidian Fast Note Sync Plugin](https://github.com/haierkeys/obsidian-fast-note-sync) / [cnb.cool 鏡像庫](https://cnb.cool/haierkeys/obsidian-fast-note-sync) * 第三方用戶端 * [FastNodeSync-CLI ](https://github.com/Go1c/FastNodeSync-CLI) 基於 Python 和 FNS WS 介面實現的雙向即時同步的命令列用戶端, 適用於無 GUI 的 Linux 伺服器環境(如 OpenClaw),實現與 Obsidian 桌面端/行動端等價的同步能力。 ================================================ FILE: docs/REST_API.md ================================================ # Fast Note Sync Service - REST API Documentation This document is generated from `swagger.json` and provides the latest API definitions. --- ## General Information ### Base URL ``` http://{host}:9000/api ``` ### Authentication Endpoints requiring authentication must include the Token in the request header: ``` Authorization: {token} ``` The Token is obtained via the login interface. ### Standard Response Structure ```typescript interface Response { code: number; // Status code (0=fail, 1+=success) status: boolean; // Operation status message: string; // Status message data: T; // Business data details?: string[]; // Error details (optional) } ``` ### Paginated Response Structure ```typescript interface ListResponse { code: number; status: boolean; message: string; data: { list: T[]; pager: { page: number; pageSize: number; totalRows: number; } } } ``` ### Pagination Parameters | Parameter | Type | Description | Default | |-----------|------|-------------|---------| | page | number | Page number | 1 | | page_size | number | Items per page | 10 (Max 100) | --- ## Error Codes Reference | Code | Description | |------|-------------| | 0 | Failure | | 1-6 | Success states | | 400-446 | Business errors | | 500-534 | System/Sync errors | ### Common Error Codes | Code | Description | |------|-------------| | 405 | User registration is closed | | 407 | Username does not exist | | 408 | Username already exists | | 414 | Note Vault does not exist | | 428 | Note does not exist | | 445 | This operation requires administrator privileges | | 505 | Invalid Params | | 507 | Not logged in | | 508 | Session expired | --- ## Backup APIs ### Update backup configuration **Endpoint**: `POST /api/backup/config` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.BackupConfigRequest | ✓ | Backup Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Delete backup configuration **Endpoint**: `DELETE /api/backup/config` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | id | query | integer | - | | **Success Response (200)**: Schema: `app.Res` --- ### Get backup configurations **Endpoint**: `GET /api/backup/configs` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Trigger a backup manually **Endpoint**: `POST /api/backup/execute` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.BackupExecuteRequest | ✓ | Backup Execute Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get backup history list **Endpoint**: `GET /api/backup/historys` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | configId | query | integer | ✓ | | | page | query | integer | - | | | pageSize | query | integer | - | | **Success Response (200)**: Schema: `app.Res` --- ## Config APIs ### Get full admin config **Endpoint**: `GET /api/admin/config` Get full system configuration information, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Update admin config **Endpoint**: `POST /api/admin/config` Modify full system configuration information, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | api_router.adminConfig | ✓ | Config Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get Cloudflare config **Endpoint**: `GET /api/admin/config/cloudflare` Get Cloudflare tunnel configuration, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Update Cloudflare config **Endpoint**: `POST /api/admin/config/cloudflare` Modify Cloudflare tunnel configuration, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | api_router.cloudflareConfig | ✓ | Config Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get Ngrok config **Endpoint**: `GET /api/admin/config/ngrok` Get Ngrok tunnel configuration, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Update Ngrok config **Endpoint**: `POST /api/admin/config/ngrok` Modify Ngrok tunnel configuration, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | api_router.ngrokConfig | ✓ | Config Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get WebGUI basic config **Endpoint**: `GET /api/webgui/config` Get non-sensitive configuration required for frontend display, such as font settings, registration status, etc. **Parameters**: None **Success Response (200)**: Schema: `app.Res` --- ## File APIs ### Get attachment content **Endpoint**: `GET /api/file` Get raw binary data of an attachment by path, supports strong cache control **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | isRecycle | query | boolean | - | Is in recycle bin // 是否在回收站 | | path | query | string | ✓ | File path // 文件路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `file` --- ### Delete attachment **Endpoint**: `DELETE /api/file` Permanently delete a specific attachment record and its physical file **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | ✓ | File path // 文件路径 | | pathHash | query | string | ✓ | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Get attachment info **Endpoint**: `GET /api/file/info` Get attachment metadata (FileDTO) by path **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | isRecycle | query | boolean | - | Is in recycle bin // 是否在回收站 | | path | query | string | ✓ | File path // 文件路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Clear recycle bin **Endpoint**: `DELETE /api/file/recycle-clear` Permanently clear selected files from recycle bin **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.FileRecycleClearRequest | ✓ | Clear Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Rename attachment **Endpoint**: `POST /api/file/rename` Rename an attachment to a new path **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.FileRenameRequest | ✓ | Rename Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Restore attachment **Endpoint**: `PUT /api/file/restore` Restore deleted attachment from trash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.FileRestoreRequest | ✓ | Restore Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get file list **Endpoint**: `GET /api/files` Get attachment list for current user with pagination, search, filter, and sort support **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | isRecycle | query | boolean | - | Is in recycle bin // 是否在回收站 | | keyword | query | string | - | Search keyword // 搜索关键词 | | sortBy | query | string | - | Sort by field // 排序字段 | | sortOrder | query | string | - | Sort order // 排序顺序 | | vault | query | string | ✓ | Vault name // 保险库名称 | | page | query | integer | - | Page number // 页码 | | pageSize | query | integer | - | Page size // 每页数量 | **Success Response (200)**: Schema: `app.Res` --- ## Folder APIs ### Get folder info **Endpoint**: `GET /api/folder` Get folder info for current user by path or pathHash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | - | Folder path // 文件夹路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Create folder **Endpoint**: `POST /api/folder` Create a new folder or restore a deleted one by path **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.FolderCreateRequest | ✓ | Create Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Delete folder **Endpoint**: `DELETE /api/folder` Soft delete a folder by path or pathHash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.FolderDeleteRequest | ✓ | Delete Parameters | **Success Response (200)**: Schema: `app.Res` --- ### List files in folder **Endpoint**: `GET /api/folder/files` List non-deleted files in a specific folder with pagination and sorting **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | - | Folder path // 文件夹路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | sortBy | query | string | - | Sort by field // 排序字段 | | sortOrder | query | string | - | Sort order // 排序顺序 | | vault | query | string | ✓ | Vault name // 保险库名称 | | page | query | integer | - | Page number // 页码 | | pageSize | query | integer | - | Page size // 每页数量 | **Success Response (200)**: Schema: `app.Res` --- ### List notes in folder **Endpoint**: `GET /api/folder/notes` List non-deleted notes in a specific folder with pagination and sorting **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | - | Folder path // 文件夹路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | sortBy | query | string | - | Sort by field // 排序字段 | | sortOrder | query | string | - | Sort order // 排序顺序 | | vault | query | string | ✓ | Vault name // 保险库名称 | | page | query | integer | - | Page number // 页码 | | pageSize | query | integer | - | Page size // 每页数量 | **Success Response (200)**: Schema: `app.Res` --- ### Get folder tree **Endpoint**: `GET /api/folder/tree` Get the complete folder tree structure for a vault **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | depth | query | integer | - | Tree depth // 树深度 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Get folder list **Endpoint**: `GET /api/folders` Get folder list for current user by parent path or pathHash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | - | Folder path // 文件夹路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ## GitSync APIs ### Update git sync configuration **Endpoint**: `POST /api/git-sync/config` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.GitSyncConfigRequest | ✓ | Git Sync Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Delete git sync configuration **Endpoint**: `DELETE /api/git-sync/config` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.GitSyncDeleteRequest | ✓ | Git Sync ID | **Success Response (200)**: Schema: `app.Res` --- ### Clean local git workspace **Endpoint**: `DELETE /api/git-sync/config/clean` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.GitSyncCleanRequest | ✓ | Clean Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Trigger a manual git sync **Endpoint**: `POST /api/git-sync/config/execute` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.GitSyncExecuteRequest | ✓ | Execute Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get git sync configurations **Endpoint**: `GET /api/git-sync/configs` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Get git sync histories **Endpoint**: `GET /api/git-sync/histories` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | configId | query | integer | - | | | page | query | integer | - | | | pageSize | query | integer | - | | **Success Response (200)**: Schema: `app.Res` --- ### Validate git sync parameters **Endpoint**: `POST /api/git-sync/validate` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.GitSyncValidateRequest | ✓ | Validation Parameters | **Success Response (200)**: Schema: `app.Res` --- ## Note APIs ### Get note details **Endpoint**: `GET /api/note` Get specific note content and metadata by path or path hash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | isRecycle | query | boolean | - | Is in recycle bin // 是否在回收站 | | path | query | string | ✓ | Note path // 笔记路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Create or update note **Endpoint**: `POST /api/note` Handle note creation, modification, or renaming (identified by path change) **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteModifyOrCreateRequest | ✓ | Note Content | **Success Response (200)**: Schema: `app.Res` --- ### Delete note **Endpoint**: `DELETE /api/note` Move note to trash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | ✓ | Note path // 笔记路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Append content to note **Endpoint**: `POST /api/note/append` Append content to the end of a note **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteAppendRequest | ✓ | Append Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get backlinks **Endpoint**: `GET /api/note/backlinks` Get all other notes that link to the specified note **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | ✓ | Note path // 笔记路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Modify note frontmatter **Endpoint**: `PATCH /api/note/frontmatter` Update or delete note frontmatter fields **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NotePatchFrontmatterRequest | ✓ | Frontmatter Modification Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Move note **Endpoint**: `POST /api/note/move` Move a note to a new path **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteMoveRequest | ✓ | Move Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get outgoing links **Endpoint**: `GET /api/note/outlinks` Get other notes that the specified note links to **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | ✓ | Note path // 笔记路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Prepend content to note **Endpoint**: `POST /api/note/prepend` Insert content at the beginning of a note (after frontmatter) **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NotePrependRequest | ✓ | Prepend Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Clear recycle bin **Endpoint**: `DELETE /api/note/recycle-clear` Permanently clear selected notes from recycle bin **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteRecycleClearRequest | ✓ | Clear Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Rename note **Endpoint**: `POST /api/note/rename` Rename a note to a new path **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteRenameRequest | ✓ | Rename Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Find and replace in note **Endpoint**: `POST /api/note/replace` Perform find and replace operation in a note, supporting regular expressions **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteReplaceRequest | ✓ | Find and Replace Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Restore note **Endpoint**: `PUT /api/note/restore` Restore deleted note from trash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteRestoreRequest | ✓ | Restore Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get note list **Endpoint**: `GET /api/notes` Get note list for current user with pagination **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | isRecycle | query | boolean | - | Is in recycle bin // 是否在回收站 | | keyword | query | string | - | Search keyword // 搜索关键词 | | searchContent | query | boolean | - | Whether to search content // 是否搜索内容 | | searchMode | query | string | - | Search mode (path, content, regex) // 搜索模式(路径、内容、正则) | | sortBy | query | string | - | Sort by field // 排序字段 | | sortOrder | query | string | - | Sort order // 排序顺序 | | vault | query | string | ✓ | Vault name // 保险库名称 | | page | query | integer | - | Page number // 页码 | | pageSize | query | integer | - | Page size // 每页数量 | **Success Response (200)**: Schema: `app.Res` --- ## Note History APIs ### Get note history list **Endpoint**: `GET /api/note/histories` Get all history records for a specific note with pagination **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | isRecycle | query | boolean | - | Is in recycle bin // 是否在回收站 | | path | query | string | ✓ | Note path // 笔记路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | | page | query | integer | - | Page number // 页码 | | pageSize | query | integer | - | Page size // 每页数量 | **Success Response (200)**: Schema: `app.Res` --- ### Get note history details **Endpoint**: `GET /api/note/history` Get specific note history content by history record ID **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | id | query | integer | ✓ | History Record ID | **Success Response (200)**: Schema: `app.Res` --- ### Restore note from history **Endpoint**: `PUT /api/note/history/restore` Restore note content to a specific history version **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.NoteHistoryRestoreRequest | ✓ | Restore Parameters | **Success Response (200)**: Schema: `app.Res` --- ## Setting APIs ### Get setting info **Endpoint**: `GET /api/setting` Get setting info for current user by path or pathHash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | - | Setting path // 配置路径 | | pathHash | query | string | - | Path hash // 路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Create or update setting **Endpoint**: `POST /api/setting` Create a new setting or update an existing one **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.SettingModifyOrCreateRequest | ✓ | Create/Update Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Delete setting **Endpoint**: `DELETE /api/setting` Soft delete a setting by path or pathHash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.SettingDeleteRequest | ✓ | Delete Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Rename setting **Endpoint**: `POST /api/setting/rename` Rename a setting and update its path and pathHash **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.SettingRenameRequest | ✓ | Rename Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get setting list **Endpoint**: `GET /api/settings` Get setting list for current user with pagination and keyword filtering **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | keyword | query | string | - | Keyword // 关键词 | | vault | query | string | ✓ | Vault name // 保险库名称 | | page | query | integer | - | Page number // 页码 | | pageSize | query | integer | - | Page size // 每页数量 | **Success Response (200)**: Schema: `app.Res` --- ## Share APIs ### Query share by path **Endpoint**: `GET /api/share` Get share token and info by vault and path **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | path | query | string | ✓ | Resource path // 资源路径 | | pathHash | query | string | ✓ | Resource path Hash // 资源路径哈希 | | vault | query | string | ✓ | Vault name // 保险库名称 | **Success Response (200)**: Schema: `app.Res` --- ### Create resource share **Endpoint**: `POST /api/share` Create a share token for a specific note or attachment, automatically resolve attachment references and authorize **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.ShareCreateRequest | ✓ | Share Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Cancel share **Endpoint**: `DELETE /api/share` Cancel a share by ID or path parameters **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.ShareCancelRequest | ✓ | Cancel Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get shared attachment content **Endpoint**: `GET /api/share/file` Get raw binary data of a specific attachment via share token **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | Share-Token | header | string | ✓ | Auth Token | | id | query | integer | ✓ | Resource ID // 资源 ID | **Success Response (200)**: Schema: `file` --- ### Get shared note details **Endpoint**: `GET /api/share/note` Get specific note content (restricted read-only access) via share token **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | Share-Token | header | string | ✓ | Auth Token | | id | query | integer | ✓ | Resource ID // 资源 ID | **Success Response (200)**: Schema: `app.Res` --- ### List shares **Endpoint**: `GET /api/shares` Get all active and inactive shares of the user, supports sorting and pagination **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | sort_by | query | string | - | Sort field: created_at, updated_at, expires_at (default: created_at) | | sort_order | query | string | - | Sort direction: asc or desc (default: desc) | | page | query | integer | - | Page number | | pageSize | query | integer | - | Page size | **Success Response (200)**: Schema: `app.Res` --- ## Storage APIs ### Get storage configuration list **Endpoint**: `GET /api/storage` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Create or update storage configuration **Endpoint**: `POST /api/storage` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.StoragePostRequest | ✓ | Storage Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Delete storage configuration **Endpoint**: `DELETE /api/storage` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | id | query | integer | ✓ | Storage ID | **Success Response (200)**: Schema: `app.Res` --- ### Get enabled storage types **Endpoint**: `GET /api/storage/enabled_types` Get list of enabled storage types. Possible values: localfs, oss, s3, r2, minio, webdav **Parameters**: None **Success Response (200)**: Schema: `app.Res` --- ### Validate storage connection **Endpoint**: `POST /api/storage/validate` **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.StoragePostRequest | ✓ | Storage Parameters | **Success Response (200)**: Schema: `app.Res` --- ## System APIs ### Download cloudflared binary **Endpoint**: `GET /api/admin/cloudflared_tunnel_download` Trigger the download of cloudflared binary for the current platform **Parameters**: None **Success Response (200)**: Schema: `app.Res` --- ### Trigger manual GC **Endpoint**: `GET /api/admin/gc` Manually run Go runtime GC and release memory to OS, requires admin privileges **Parameters**: None **Success Response (200)**: Schema: `app.Res` --- ### Trigger server restart **Endpoint**: `GET /api/admin/restart` Gracefully restart the server **Parameters**: None **Success Response (200)**: Schema: `app.Res` --- ### Get system and runtime info **Endpoint**: `GET /api/admin/systeminfo` Get system information and Go runtime data, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Trigger server upgrade **Endpoint**: `GET /api/admin/upgrade` Download latest version and restart server **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | version | query | string | ✓ | Version to upgrade (e.g. 2.0.10 or latest) | **Success Response (200)**: Schema: `app.Res` --- ### Get connected WebSocket clients **Endpoint**: `GET /api/admin/ws_clients` Get a list of all current WebSocket connections, requires admin privileges **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Health check **Endpoint**: `GET /api/health` Check service health status, including database connection **Parameters**: None **Success Response (200)**: Schema: `api_router.HealthResponse` --- ### Get support records **Endpoint**: `GET /api/support` Get support records for the specified language with pagination and sorting **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | lang | query | string | - | Language code (default: en) | | sortBy | query | string | - | Sort by field (amount, time, name, item) | | sortOrder | query | string | - | Sort order (asc, desc) | | page | query | integer | - | Page number | | pageSize | query | integer | - | Page size | **Success Response (200)**: Schema: `app.Res` --- ### Get server version info **Endpoint**: `GET /api/version` Get current server software version, Git tag, and build time **Parameters**: None **Success Response (200)**: Schema: `app.Res` --- ## User APIs ### Change user password **Endpoint**: `POST /api/user/change_password` Handle password change request for current user, validate old password and update new password. 处理当前用户的修改密码请求,验证旧密码并更新新密码。 **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.UserChangePasswordRequest | ✓ | Change Password Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Get user info **Endpoint**: `GET /api/user/info` Handle request to get current user info. 处理获取当前用户信息的请求。 **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### User login **Endpoint**: `POST /api/user/login` Handle user login HTTP request, validate parameters and return auth token. 处理用户登录 HTTP 请求,验证参数并返回认证 Token。 **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | params | body | dto.UserLoginRequest | ✓ | Login Parameters | **Success Response (200)**: Schema: `app.Res` --- ### User registration **Endpoint**: `POST /api/user/register` Handle user registration HTTP request, validate parameters and call UserService. Registration may be disabled in server settings. 处理用户注册 HTTP 请求,验证参数并调用 UserService。注册功能可能在服务器设置中被禁用。 **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | params | body | dto.UserCreateRequest | ✓ | Register Parameters | **Success Response (200)**: Schema: `app.Res` --- ## Vault APIs ### Get vault list **Endpoint**: `GET /api/vault` Get all note vaults for current user **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | **Success Response (200)**: Schema: `app.Res` --- ### Create or update vault **Endpoint**: `POST /api/vault` Be used to create a new vault or update an existing vault configuration based on the ID in the request parameters **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | params | body | dto.VaultPostRequest | ✓ | Vault Parameters | **Success Response (200)**: Schema: `app.Res` --- ### Delete vault **Endpoint**: `DELETE /api/vault` Permanently delete a specific note vault and all associated notes and attachments **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | id | query | integer | ✓ | Vault ID // 保险库 ID | **Success Response (200)**: Schema: `app.Res` --- ### Get vault details **Endpoint**: `GET /api/vault/get` Get specific vault configuration details by vault ID **Parameters**: | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | token | header | string | ✓ | Auth Token | | id | query | integer | ✓ | Vault ID | **Success Response (200)**: Schema: `app.Res` --- ## Timestamp Format All timestamp fields (`ctime`, `mtime`, `updatedTimestamp`, `lastTime`) are **Unix timestamps in milliseconds**. --- ## Hash Algorithms `pathHash` and `contentHash` use a 32-bit hash algorithm (e.g., FNV-1a). Clients can compute these automatically or receive them from the server. --- ## Full-Text Search (FTS) The server includes a built-in full-text search engine based on SQLite FTS5 for efficient searching of note paths and content. ================================================ FILE: docs/Support.csv ================================================ 收款时间,收款项,金额,单位,留言,昵称 2026/03/27 00:36:52,任意打赏,128.00,¥,特别棒,一直在用,希望越做越好。,Geeson 2026/04/18 21:15:46,四杯咖啡☕,100.00,¥,支持一下[抱拳],lien 2026/03/29 11:20:42,四杯咖啡☕,100.00,¥,支持!加油!,猛将兄 2026/03/27 15:05:35,四杯咖啡☕,100.00,¥,做得太棒了,Bais 2026/03/24 09:02:45,四杯咖啡☕,100.00,¥,辛苦了,cc 2026/03/22 12:16:07,四杯咖啡☕,100.00,¥,强烈支持版本迭代,cw 2026/03/19 13:41:05,四杯咖啡☕,100.00,¥,快去加班更新,背背背疼 2026/03/13 17:28:40,四杯咖啡☕,100.00,¥,非常感谢你的开源与付出,插件超实用,小小支持,继续加油!💪,一世风霜 2026/03/02 09:38:26,四杯咖啡☕,100.00,¥,非常好,开发不易,支持一下。,xuhsu 2026/01/14 15:58:04,任意打赏,88.00,¥,能力有限,不成敬意,wutay 2026/03/02 14:50:25,任意打赏,66.00,¥,感谢大佬!非常好用!,Patrick 2026/03/01 22:56:17,任意打赏,66.00,¥,随喜赞叹!,xday 2026/03/01 22:40:23,任意打赏,66.00,¥,大佬nb,插件很好用ヽ(*≧ω≦)ノ,HanHaocheng 2026/02/16 21:34:33,任意打赏,66.00,¥,新年快乐,Jack 2026/04/02 19:16:44,任意打赏,51.55,¥,很好的插件,小七的小洋 2026/04/21 14:32:40,两杯咖啡☕,50.00,¥,太棒了,宝藏插件啊,加油,安度 2026/04/09 13:45:07,两杯咖啡☕,50.00,¥,电脑同步成功,安卓报错 code=305,message=参数验证失败 Details=context is requ,亲 yexizhu811 2026/04/08 03:18:40,两杯咖啡☕,50.00,¥,感谢作者,这一同步方式解决了多设备配置一致性的麻烦。,彼岸花 2026/04/07 07:52:09,两杯咖啡☕,50.00,¥,太棒了,很需要,感谢大佬。,tom 2026/04/03 10:50:44,两杯咖啡☕,50.00,¥,Good work,David 2026/04/02 03:03:20,两杯咖啡☕,50.00,¥,为牛逼付费!,狗带带子 2026/03/27 10:15:04,两杯咖啡☕,50.00,¥,好人一生平安,卿 2026/03/18 22:57:03,两杯咖啡☕,50.00,¥,感谢telegram上的指导,灰风 2026/03/15 20:09:05,两杯咖啡☕,50.00,¥,感谢🙏,插件好用,红星 RedStar 2026/03/14 23:46:58,两杯咖啡☕,50.00,¥,非常好整套架构,让我进入21世纪,fbeis 2026/03/02 21:00:43,两杯咖啡☕,50.00,¥,谢谢大佬,南科大小魏 2026/02/28 13:56:18,两杯咖啡☕,50.00,¥,太勤劳了,必须支持,xenon 2026/02/24 16:37:58,两杯咖啡☕,50.00,¥,很不错的同步方案,熙熙煦煦 2026/02/16 12:14:32,两杯咖啡☕,50.00,¥,感谢作者,新年快乐!,红殇 2026/02/14 18:01:55,两杯咖啡☕,50.00,¥,感谢开发这么棒的插件,解决了同步问题,Jacky龙 2026/02/04 10:41:49,两杯咖啡☕,50.00,¥,同步功能很好用,希望继续迭代完善,以笔记安全为主,咕咕咕 2026/01/31 16:59:55,两杯咖啡☕,50.00,¥,好好开发,确实解决了 Obsidian 最大的一个痛点!,vulnnull 2026/01/21 09:37:19,两杯咖啡☕,50.00,¥,谢谢你的Obsidian同步很好用👍🏻,Mojo抖音 2026/01/09 16:34:10,两杯咖啡☕,50.00,¥,谢谢男菩萨的 OB 插件造福世人(^🙏^),小小心意不成敬意。,喆 2026/03/04 21:42:57,任意打赏,30.00,¥,插件很好用,感谢开发者,X 2026/02/25 10:55:32,任意打赏,30.00,¥,大佬的项目帮了我大忙!非常感谢!希望大佬继续加油,jeanlaw 2026/03/13 13:43:33,任意打赏,29.00,¥,好作品,加油!,rocku 2026/02/24 18:36:51,任意打赏,25.00,¥,"一直找不到理想的obsidian的同步方案,感谢作者 加油!",淇淇 2026/04/21 13:45:19,一杯咖啡☕,20.00,¥,很好用,加油,蓬歌 2026/04/20 15:37:57,一杯咖啡☕,20.00,¥,给大佬跪了🧎🏻‍♂️,希望后续多多更新,riding-a-colt 2026/04/17 00:15:35,一杯咖啡☕,20.00,¥,好用,支持一下,稻草人 2026/04/15 13:41:41,一杯咖啡☕,20.00,¥,谢谢大佬,hitomi 2026/04/15 11:22:39,一杯咖啡☕,20.00,¥,忘记密码如何找回,woshiug 2026/04/15 06:38:14,一杯咖啡☕,20.00,¥,试试,ke1078 2026/04/12 23:54:45,一杯咖啡☕,20.00,¥,很棒的软件,感谢作者付出和开源分享。,Nikki 2026/04/12 23:40:17,一杯咖啡☕,20.00,¥,感谢🙏🏻,月非明 2026/04/10 23:00:36,一杯咖啡☕,20.00,¥,棒棒哒,真好用,wdysjy 2026/04/04 01:33:37,一杯咖啡☕,20.00,¥,来杯咖啡,辛苦了,hsonghao 2026/04/03 23:42:51,一杯咖啡☕,20.00,¥,牛逼,好用,kakaa 2026/04/03 13:28:02,一杯咖啡☕,20.00,¥,非常好用,感谢,晴天小嘉 2026/04/02 23:08:13,一杯咖啡☕,20.00,¥,感谢大佬,希望能继续坚持,畅 2026/04/02 15:13:46,一杯咖啡☕,20.00,¥,请您喝咖啡,这个项目非常有用,dove 2026/03/30 23:34:12,一杯咖啡☕,20.00,¥,加油⛽️,andie 2026/03/28 12:46:23,一杯咖啡☕,20.00,¥,感谢大大,辛苦啦,zhengbiubiu 2026/03/27 19:45:57,一杯咖啡☕,20.00,¥,fast note sync👍,IsaacSuo 2026/03/26 13:22:50,一杯咖啡☕,20.00,¥,牛哇👍,dawn 2026/03/24 14:45:21,一杯咖啡☕,20.00,¥,感谢,软件很方便。,Bean 2026/03/22 23:08:14,一杯咖啡☕,20.00,¥,respect,拾感 2026/03/19 22:47:54,一杯咖啡☕,20.00,¥,谢谢🙏!很赞!,jediknight 2026/03/19 20:20:17,一杯咖啡☕,20.00,¥,感谢开发者,太好用了,加油!,Fcjd 2026/03/17 01:17:02,一杯咖啡☕,20.00,¥,做的太好了,大道至简,希望继续优化~,southzen 2026/03/16 16:24:11,一杯咖啡☕,20.00,¥,非常好用的喵~谢谢喵~,长筱团子 2026/03/16 09:22:14,一杯咖啡☕,20.00,¥,不成敬意^_^,barry 2026/03/16 01:01:12,一杯咖啡☕,20.00,¥,感谢你对开源世界的贡献,Stone 2026/03/14 17:15:45,一杯咖啡☕,20.00,¥,感谢作者,不成敬意,R M 2026/03/14 00:39:54,一杯咖啡☕,20.00,¥,大佬NB,感谢感谢,T0_欣 2026/03/11 20:05:58,一杯咖啡☕,20.00,¥,感谢大佬做的插件,很好用很方便,Ucat 2026/03/10 01:08:20,一杯咖啡☕,20.00,¥,牛逼,la 2026/03/09 09:59:44,一杯咖啡☕,20.00,¥,好用,支持,耀/ 2026/03/07 23:36:38,一杯咖啡☕,20.00,¥,很好的插件 支持,阿叶 2026/03/06 09:58:58,一杯咖啡☕,20.00,¥,谢谢,Dylan 2026/03/03 00:53:09,一杯咖啡☕,20.00,¥,感谢作者日夜辛苦的写代码,并开源,Alan 2026/03/01 21:10:27,一杯咖啡☕,20.00,¥,希望能更完善,可以在云端查看ob的其他格式的文件,Jack ☑️ 2026/02/28 09:50:58,一杯咖啡☕,20.00,¥,这个插件思路很好,加油,tangdh 2026/02/27 12:48:50,一杯咖啡☕,20.00,¥,很好的同步插件,aban 2026/02/27 11:37:18,一杯咖啡☕,20.00,¥,感谢您的工作,三岁 2026/02/27 11:19:13,一杯咖啡☕,20.00,¥,太强了,行长 2026/02/26 11:16:25,一杯咖啡☕,20.00,¥,这么好的东西应该让更多人知道,加大宣传力度啊!,fausto 2026/02/25 20:17:42,一杯咖啡☕,20.00,¥,非常好用,感谢作者开发这么好的工具!,woloin 2026/02/24 18:29:45,一杯咖啡☕,20.00,¥,感谢作者开发那么好用的插件,让我的obsidian旋转🥰,kimi 2026/02/24 10:01:37,一杯咖啡☕,20.00,¥,插件好用,ccsir 2026/02/23 20:53:08,一杯咖啡☕,20.00,¥,用了一段时间了,真的太棒了。,KevinYAN 2026/02/23 19:32:03,一杯咖啡☕,20.00,¥,马年快乐,繁星影月 2026/02/23 12:30:55,一杯咖啡☕,20.00,¥,谢谢辛苦了,ahto 2026/02/23 06:24:55,一杯咖啡☕,20.00,¥,很有帮助,加油!,大学生 2026/02/15 09:25:17,一杯咖啡☕,20.00,¥,支持,加油~,很实用,强需求的功能。在AI时代大有用处,sfsun67 2026/02/10 23:21:38,一杯咖啡☕,20.00,¥,感谢大佬做的同步插件,非常好用,请大佬先喝一杯咖啡,后续还会再继续打赏的,toby 2026/02/10 11:41:36,一杯咖啡☕,20.00,¥,大佬的同步插件非常棒,我会一直持续支持的,WONG 2026/02/08 22:02:32,一杯咖啡☕,20.00,¥,非常好用,感谢,小迪 2026/02/06 08:42:49,一杯咖啡☕,20.00,¥,加油💪web端的图片编辑功能整一下呗,Max 2026/01/28 10:48:02,一杯咖啡☕,20.00,¥,加油,通 2026/01/26 17:21:27,一杯咖啡☕,20.00,¥,😘,CloseCV 2026/01/16 11:47:13,一杯咖啡☕,20.00,¥,很好用,期待后续的开发与优化。感谢。,苏 2026/01/15 14:51:11,一杯咖啡☕,20.00,¥,非常好用感谢!,灰风 2026/01/09 18:12:17,一杯咖啡☕,20.00,¥,插件思路太对了,xix 2026/01/03 22:44:43,一杯咖啡☕,20.00,¥,希望越来越好👌🏻,姚朝伟 2026/01/03 14:58:43,一杯咖啡☕,20.00,¥,很棒的同步方案,未来可期!非常感谢开源!加油!,roao 2026/03/11 09:58:20,任意打赏,18.00,¥,绵薄之力,以表敬意,下鞅 2026/03/02 21:15:39,任意打赏,10.00,¥,加油大神,路过打酱 2026/02/28 12:27:51,任意打赏,10.00,¥,希望越来越好,白芷 2026/02/27 15:54:55,任意打赏,10.00,¥,谢谢作者开发,感谢开源,祝越来越好。,柴特 2026/02/23 15:34:53,任意打赏,10.00,¥,感谢,比官方同步还好用,Joe M 2026/02/20 10:37:02,任意打赏,10.00,¥,感谢做了这么便捷的同步软件,羽山猫四叶 2026/03/22 15:09:43,任意打赏,9.90,¥,好软件,感谢作者。,Shifuwang 2026/01/28 12:03:03,任意打赏,9.90,¥,牛🐮,华星 2026/04/09 13:51:11,任意打赏,8.88,¥,加油……你这个绝对有发展前途,推荐其他朋友看都觉得很不错。,散装白酒🍶 2026/03/07 20:29:42,任意打赏,8.88,¥,感谢大佬,非常好用,点赞,皮皮 2026/01/28 02:52:15,任意打赏,8.88,¥,感谢分享,obsidian 2026/02/28 19:51:59,任意打赏,8.00,¥,很完善,甚至服务器界面也做得非常好看,yang 2026/03/25 11:52:09,任意打赏,6.66,¥,装好了 无敌 哈哈哈哈哈哈,东 2026/03/23 01:02:18,任意打赏,6.66,¥,一定要坚持开发呀!!!,wishyuwill 2026/03/02 17:10:28,任意打赏,6.66,¥,感谢开发,插件很好用,更新很快[强],马孔多的旅人 2026/02/01 23:44:27,任意打赏,6.66,¥,爱你,爱你 2026/01/09 22:22:25,任意打赏,6.66,¥,老哥写的插件很棒,继续努力吧!,kane 2026/04/20 16:34:57,任意打赏,5.00,¥,这也太好用了,折腾这么久感觉终于毕业了(╥╯﹏╰╥)ง,david 2026/04/15 00:48:38,任意打赏,5.00,¥,不易,ben 2026/04/06 20:57:36,任意打赏,5.00,¥,感谢你的项目帮助到我,obsidian 同步从此变得容易,octobersky 2026/03/01 00:28:19,任意打赏,5.00,¥,开发的太棒了,colorednoise 2026/02/27 15:18:26,任意打赏,5.00,¥,支持,wudibaolong 2026/02/14 02:32:50,任意打赏,5.00,¥,感谢开发这么好用的插件,支持开源精神 2026/02/13 02:13:10,任意打赏,5.00,¥,打赏,xxx 2026/02/11 17:07:02,任意打赏,5.00,¥,加油,Acckion 2026/01/11 14:20:34,任意打赏,5.00,¥,很好用,希望能把 git 做出来,安宁 2026/04/18 16:59:14,任意打赏,3.66,¥,继续开发,作出好产品,jeremy 2026/02/23 17:47:46,任意打赏,3.00,¥,新年快乐,LL 2026/04/11 12:28:18,任意打赏,1.00,¥,试试,ke1078 2026/03/26 15:03:40,任意打赏,1.00,¥,感谢,十分有用,guanyingquan 2026/02/24 20:15:19,任意打赏,1.00,¥,感恩您的 Obs 同步插件非常有帮助!,Jimmy 2026/01/08 15:18:06,任意打赏,1.00,¥,从发现部署到使用,好多年了,从未有过的流畅丝滑的感觉,真的太好了![强][强][强],用户 ================================================ FILE: docs/Support.en.json ================================================ [ { "time": "2026/03/27 00:36:52", "item": "Tip as you like", "amount": "128.00", "unit": "¥", "message": "It’s great, I’ve been using it, and I hope it gets better and better.", "name": "Geeson" }, { "time": "2026/04/18 21:15:46", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Support me [hold fist]", "name": "lien" }, { "time": "2026/03/29 11:20:42", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "support! come on!", "name": "猛将兄" }, { "time": "2026/03/27 15:05:35", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Great job", "name": "Bais" }, { "time": "2026/03/24 09:02:45", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Thanks for your hard work", "name": "cc" }, { "time": "2026/03/22 12:16:07", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Strong support for version iteration", "name": "cw" }, { "time": "2026/03/19 13:41:05", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Go and work overtime to update", "name": "背背背疼" }, { "time": "2026/03/13 17:28:40", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Thank you very much for your open source and efforts. The plug-in is super practical. I appreciate your support. Keep up the good work! 💪", "name": "一世风霜" }, { "time": "2026/03/02 09:38:26", "item": "Four cups of coffee☕", "amount": "100.00", "unit": "¥", "message": "Very good, development is not easy, please support it.", "name": "xuhsu" }, { "time": "2026/01/14 15:58:04", "item": "Tip as you like", "amount": "88.00", "unit": "¥", "message": "Limited ability, no respect", "name": "wutay" }, { "time": "2026/03/02 14:50:25", "item": "Tip as you like", "amount": "66.00", "unit": "¥", "message": "Thanks man! Very easy to use!", "name": "Patrick" }, { "time": "2026/03/01 22:56:17", "item": "Tip as you like", "amount": "66.00", "unit": "¥", "message": "Congratulations!", "name": "xday" }, { "time": "2026/03/01 22:40:23", "item": "Tip as you like", "amount": "66.00", "unit": "¥", "message": "Boss nb, the plug-in is very usefulヽ(*≧ω≦)ノ", "name": "HanHaocheng" }, { "time": "2026/02/16 21:34:33", "item": "Tip as you like", "amount": "66.00", "unit": "¥", "message": "Happy New Year", "name": "Jack" }, { "time": "2026/04/02 19:16:44", "item": "Tip as you like", "amount": "51.55", "unit": "¥", "message": "Very good plugin", "name": "小七的小洋" }, { "time": "2026/04/21 14:32:40", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Great, treasure plug-in, come on", "name": "安度" }, { "time": "2026/04/09 13:45:07", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Computer synchronization is successful, Android error code=305, message=parameter verification failed Details=context is requ", "name": "亲 yexizhu811" }, { "time": "2026/04/08 03:18:40", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thanks to the author, this synchronization method solves the trouble of multi-device configuration consistency.", "name": "彼岸花" }, { "time": "2026/04/07 07:52:09", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Awesome, much needed, thank you sir.", "name": "tom" }, { "time": "2026/04/03 10:50:44", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Good work", "name": "David" }, { "time": "2026/04/02 03:03:20", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Pay for awesomeness!", "name": "狗带带子" }, { "time": "2026/03/27 10:15:04", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Good people have a safe life", "name": "卿" }, { "time": "2026/03/18 22:57:03", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thanks for the guidance on telegram", "name": "灰风" }, { "time": "2026/03/15 20:09:05", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thanks 🙏, the plug-in is easy to use", "name": "红星 RedStar" }, { "time": "2026/03/14 23:46:58", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "The whole structure is very good and brings me into the 21st century.", "name": "fbeis" }, { "time": "2026/03/02 21:00:43", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thank you, boss", "name": "南科大小魏" }, { "time": "2026/02/28 13:56:18", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Too hardworking and must be supported", "name": "xenon" }, { "time": "2026/02/24 16:37:58", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Very good synchronization solution", "name": "熙熙煦煦" }, { "time": "2026/02/16 12:14:32", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thanks to the author and Happy New Year!", "name": "红殇" }, { "time": "2026/02/14 18:01:55", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thanks for developing such a great plug-in, which solves the synchronization problem", "name": "Jacky龙" }, { "time": "2026/02/04 10:41:49", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "The synchronization function is very useful. I hope to continue to iterate and improve it, focusing on note safety.", "name": "咕咕咕" }, { "time": "2026/01/31 16:59:55", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Well developed, it has indeed solved one of Obsidian’s biggest pain points!", "name": "vulnnull" }, { "time": "2026/01/21 09:37:19", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thank you for the Obsidian synchronization which is very useful👍🏻", "name": "Mojo抖音" }, { "time": "2026/01/09 16:34:10", "item": "Two cups of coffee☕", "amount": "50.00", "unit": "¥", "message": "Thank you Male Bodhisattva for your OB plug-in for benefiting the world (^🙏^), being petty is disrespectful.", "name": "喆" }, { "time": "2026/03/04 21:42:57", "item": "Tip as you like", "amount": "30.00", "unit": "¥", "message": "The plug-in is very useful, thank you to the developer", "name": "X" }, { "time": "2026/02/25 10:55:32", "item": "Tip as you like", "amount": "30.00", "unit": "¥", "message": "The boss’s project helped me a lot! Thank you so much! I hope you guys will continue to work hard", "name": "jeanlaw" }, { "time": "2026/03/13 13:43:33", "item": "Tip as you like", "amount": "29.00", "unit": "¥", "message": "Great work, keep it up!", "name": "rocku" }, { "time": "2026/02/24 18:36:51", "item": "Tip as you like", "amount": "25.00", "unit": "¥", "message": "I have never been able to find an ideal obsidian synchronization solution. Thank you to the author. Come on!", "name": "淇淇" }, { "time": "2026/04/21 13:45:19", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very useful, keep going", "name": "蓬歌" }, { "time": "2026/04/20 15:37:57", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Kneel down to the boss 🧎🏻‍♂️, I hope there will be more updates in the future", "name": "riding-a-colt" }, { "time": "2026/04/17 00:15:35", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "It’s easy to use, support it", "name": "稻草人" }, { "time": "2026/04/15 13:41:41", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you, boss", "name": "hitomi" }, { "time": "2026/04/15 11:22:39", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "How to retrieve forgotten password", "name": "woshiug" }, { "time": "2026/04/15 06:38:14", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "try", "name": "ke1078" }, { "time": "2026/04/12 23:54:45", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Great software, thank the author for his contribution and open source sharing.", "name": "Nikki" }, { "time": "2026/04/12 23:40:17", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you🙏🏻", "name": "月非明" }, { "time": "2026/04/10 23:00:36", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Awesome, really useful", "name": "wdysjy" }, { "time": "2026/04/04 01:33:37", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Have a cup of coffee. Thank you for your hard work.", "name": "hsonghao" }, { "time": "2026/04/03 23:42:51", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Awesome, easy to use", "name": "kakaa" }, { "time": "2026/04/03 13:28:02", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very useful, thank you", "name": "晴天小嘉" }, { "time": "2026/04/02 23:08:13", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks man, I hope you can continue to persevere", "name": "畅" }, { "time": "2026/04/02 15:13:46", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Buy you a coffee, this project is very useful", "name": "dove" }, { "time": "2026/03/30 23:34:12", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Come on⛽️", "name": "andie" }, { "time": "2026/03/28 12:46:23", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you very much, thank you for your hard work", "name": "zhengbiubiu" }, { "time": "2026/03/27 19:45:57", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "fast note sync👍", "name": "IsaacSuo" }, { "time": "2026/03/26 13:22:50", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Wow 👍", "name": "dawn" }, { "time": "2026/03/24 14:45:21", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you, the software is very convenient.", "name": "Bean" }, { "time": "2026/03/22 23:08:14", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "respect", "name": "拾感" }, { "time": "2026/03/19 22:47:54", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you 🙏! Great!", "name": "jediknight" }, { "time": "2026/03/19 20:20:17", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks to the developer, it’s so easy to use, come on!", "name": "Fcjd" }, { "time": "2026/03/17 01:17:02", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Great job, simple and simple, I hope to continue to optimize~", "name": "southzen" }, { "time": "2026/03/16 16:24:11", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very useful, meow~Thank you, meow~", "name": "长筱团子" }, { "time": "2026/03/16 09:22:14", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Not respectful^_^", "name": "barry" }, { "time": "2026/03/16 01:01:12", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you for your contributions to the open source world", "name": "Stone" }, { "time": "2026/03/14 17:15:45", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks to the author, no disrespect", "name": "R M" }, { "time": "2026/03/14 00:39:54", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Boss NB, thank you thank you", "name": "T0_欣" }, { "time": "2026/03/11 20:05:58", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks for the plug-in, it’s very useful and convenient.", "name": "Ucat" }, { "time": "2026/03/10 01:08:20", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Awesome", "name": "la" }, { "time": "2026/03/09 09:59:44", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Easy to use, support", "name": "耀/" }, { "time": "2026/03/07 23:36:38", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very good plugin support", "name": "阿叶" }, { "time": "2026/03/06 09:58:58", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks", "name": "Dylan" }, { "time": "2026/03/03 00:53:09", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks to the author for working hard day and night to write the code and open source it", "name": "Alan" }, { "time": "2026/03/01 21:10:27", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "I hope it will be more complete and you can view ob files in other formats on the cloud.", "name": "Jack ☑️" }, { "time": "2026/02/28 09:50:58", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "The idea of ​​this plug-in is very good, keep up the good work", "name": "tangdh" }, { "time": "2026/02/27 12:48:50", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very good sync plugin", "name": "aban" }, { "time": "2026/02/27 11:37:18", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "thank you for your work", "name": "三岁" }, { "time": "2026/02/27 11:19:13", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Too strong", "name": "行长" }, { "time": "2026/02/26 11:16:25", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Such a good thing should be made known to more people and publicity efforts should be increased!", "name": "fausto" }, { "time": "2026/02/25 20:17:42", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very easy to use, thank the author for developing such a good tool!", "name": "woloin" }, { "time": "2026/02/24 18:29:45", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks to the author for developing such a useful plug-in, which makes my obsidian rotate🥰", "name": "kimi" }, { "time": "2026/02/24 10:01:37", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Plug-in is easy to use", "name": "ccsir" }, { "time": "2026/02/23 20:53:08", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Been using it for a while and it's really great.", "name": "KevinYAN" }, { "time": "2026/02/23 19:32:03", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Happy Year of the Horse", "name": "繁星影月" }, { "time": "2026/02/23 12:30:55", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thank you for your hard work", "name": "ahto" }, { "time": "2026/02/23 06:24:55", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very helpful, come on!", "name": "大学生" }, { "time": "2026/02/15 09:25:17", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Support, come on~, it is very practical and a highly demanded function. Very useful in the AI ​​era", "name": "sfsun67" }, { "time": "2026/02/10 23:21:38", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Thanks for the synchronization plug-in made by the boss. It is very easy to use. Please drink a cup of coffee first and I will continue to reward you later.", "name": "toby" }, { "time": "2026/02/10 11:41:36", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "The synchronization plug-in of the boss is very good. I will continue to support it.", "name": "WONG" }, { "time": "2026/02/08 22:02:32", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very useful, thank you", "name": "小迪" }, { "time": "2026/02/06 08:42:49", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Come on💪 improve the image editing function on the web", "name": "Max" }, { "time": "2026/01/28 10:48:02", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "come on", "name": "通" }, { "time": "2026/01/26 17:21:27", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "-", "name": "CloseCV" }, { "time": "2026/01/16 11:47:13", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very useful, looking forward to subsequent development and optimization. grateful.", "name": "苏" }, { "time": "2026/01/15 14:51:11", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Very useful. Thanks!", "name": "灰风" }, { "time": "2026/01/09 18:12:17", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "The plug-in idea is so right", "name": "xix" }, { "time": "2026/01/03 22:44:43", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Hope it gets better and better👌🏻", "name": "姚朝伟" }, { "time": "2026/01/03 14:58:43", "item": "A cup of coffee☕", "amount": "20.00", "unit": "¥", "message": "Great synchronization solution, look forward to the future! Thank you very much for the open source! come on!", "name": "roao" }, { "time": "2026/03/11 09:58:20", "item": "Tip as you like", "amount": "18.00", "unit": "¥", "message": "A small effort to show respect", "name": "下鞅" }, { "time": "2026/03/02 21:15:39", "item": "Tip as you like", "amount": "10.00", "unit": "¥", "message": "Come on, God", "name": "路过打酱" }, { "time": "2026/02/28 12:27:51", "item": "Tip as you like", "amount": "10.00", "unit": "¥", "message": "Hope it gets better and better", "name": "白芷" }, { "time": "2026/02/27 15:54:55", "item": "Tip as you like", "amount": "10.00", "unit": "¥", "message": "Thank you to the author for the development, thank you for the open source, and wish it better and better.", "name": "柴特" }, { "time": "2026/02/23 15:34:53", "item": "Tip as you like", "amount": "10.00", "unit": "¥", "message": "Thanks, it’s easier to use than the official synchronization", "name": "Joe M" }, { "time": "2026/02/20 10:37:02", "item": "Tip as you like", "amount": "10.00", "unit": "¥", "message": "Thanks for making such a convenient synchronization software", "name": "羽山猫四叶" }, { "time": "2026/03/22 15:09:43", "item": "Tip as you like", "amount": "9.90", "unit": "¥", "message": "Great software, thanks to the author.", "name": "Shifuwang" }, { "time": "2026/01/28 12:03:03", "item": "Tip as you like", "amount": "9.90", "unit": "¥", "message": "Cow🐮", "name": "华星" }, { "time": "2026/04/09 13:51:11", "item": "Tip as you like", "amount": "8.88", "unit": "¥", "message": "Come on...you definitely have a bright future. I would recommend it to other friends and think it would be great.", "name": "散装白酒🍶" }, { "time": "2026/03/07 20:29:42", "item": "Tip as you like", "amount": "8.88", "unit": "¥", "message": "Thanks man, it’s very useful, like it", "name": "皮皮" }, { "time": "2026/01/28 02:52:15", "item": "Tip as you like", "amount": "8.88", "unit": "¥", "message": "thanks for sharing", "name": "obsidian" }, { "time": "2026/02/28 19:51:59", "item": "Tip as you like", "amount": "8.00", "unit": "¥", "message": "It's very complete, even the server interface is very nice.", "name": "yang" }, { "time": "2026/03/25 11:52:09", "item": "Tip as you like", "amount": "6.66", "unit": "¥", "message": "Pretended to be invincible hahahahahaha", "name": "东" }, { "time": "2026/03/23 01:02:18", "item": "Tip as you like", "amount": "6.66", "unit": "¥", "message": "You must keep developing! ! !", "name": "wishyuwill" }, { "time": "2026/03/02 17:10:28", "item": "Tip as you like", "amount": "6.66", "unit": "¥", "message": "Thanks for the development, the plug-in is very useful and will be updated quickly [strong]", "name": "马孔多的旅人" }, { "time": "2026/02/01 23:44:27", "item": "Tip as you like", "amount": "6.66", "unit": "¥", "message": "Love you", "name": "爱你" }, { "time": "2026/01/09 22:22:25", "item": "Tip as you like", "amount": "6.66", "unit": "¥", "message": "The plug-in I wrote is great, keep up the good work!", "name": "kane" }, { "time": "2026/04/20 16:34:57", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "This is so useful. I feel like I have finally graduated after struggling for so long (╥╯﹏╰╥)ง", "name": "david" }, { "time": "2026/04/15 00:48:38", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "Not easy", "name": "ben" }, { "time": "2026/04/06 20:57:36", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "Thanks for helping me with your project, obsidian synchronization becomes easy from now on", "name": "octobersky" }, { "time": "2026/03/01 00:28:19", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "Great development", "name": "colorednoise" }, { "time": "2026/02/27 15:18:26", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "support", "name": "wudibaolong" }, { "time": "2026/02/14 02:32:50", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "Thanks for developing such a useful plug-in", "name": "支持开源精神" }, { "time": "2026/02/13 02:13:10", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "reward", "name": "xxx" }, { "time": "2026/02/11 17:07:02", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "come on", "name": "Acckion" }, { "time": "2026/01/11 14:20:34", "item": "Tip as you like", "amount": "5.00", "unit": "¥", "message": "It’s very useful. I hope I can use git to build it.", "name": "安宁" }, { "time": "2026/04/18 16:59:14", "item": "Tip as you like", "amount": "3.66", "unit": "¥", "message": "Continue to develop and make good products", "name": "jeremy" }, { "time": "2026/02/23 17:47:46", "item": "Tip as you like", "amount": "3.00", "unit": "¥", "message": "Happy New Year", "name": "LL" }, { "time": "2026/04/11 12:28:18", "item": "Tip as you like", "amount": "1.00", "unit": "¥", "message": "try", "name": "ke1078" }, { "time": "2026/03/26 15:03:40", "item": "Tip as you like", "amount": "1.00", "unit": "¥", "message": "Thanks, very useful", "name": "guanyingquan" }, { "time": "2026/02/24 20:15:19", "item": "Tip as you like", "amount": "1.00", "unit": "¥", "message": "Thanks for the very helpful Obs sync plugin!", "name": "Jimmy" }, { "time": "2026/01/08 15:18:06", "item": "Tip as you like", "amount": "1.00", "unit": "¥", "message": "From discovery and deployment to use, it has been many years, and it feels smooth and silky like never before. It is really great! [strong][strong][strong]", "name": "用户" } ] ================================================ FILE: docs/Support.en.md ================================================ # Supporters List > Thank you very much for supporting this project! Every donation is the driving force for my continuous maintenance and iteration. ❤️ ### 📜 Acknowledgement List | Time | Item | Amount | Name | Message | | :--- | :--- | :--- | :--- | :--- | | 2026/03/27 00:36:52 | Tip as you like | **¥128.00** | Geeson | It’s great, I’ve been using it, and I hope it gets better and better. | | 2026/04/18 21:15:46 | Four cups of coffee☕ | **¥100.00** | lien | Support me [hold fist] | | 2026/03/29 11:20:42 | Four cups of coffee☕ | **¥100.00** | 猛将兄 | support! come on! | | 2026/03/27 15:05:35 | Four cups of coffee☕ | **¥100.00** | Bais | Great job | | 2026/03/24 09:02:45 | Four cups of coffee☕ | **¥100.00** | cc | Thanks for your hard work | | 2026/03/22 12:16:07 | Four cups of coffee☕ | **¥100.00** | cw | Strong support for version iteration | | 2026/03/19 13:41:05 | Four cups of coffee☕ | **¥100.00** | 背背背疼 | Go and work overtime to update | | 2026/03/13 17:28:40 | Four cups of coffee☕ | **¥100.00** | 一世风霜 | Thank you very much for your open source and efforts. The plug-in is super practical. I appreciate your support. Keep up the good work! 💪 | | 2026/03/02 09:38:26 | Four cups of coffee☕ | **¥100.00** | xuhsu | Very good, development is not easy, please support it. | | 2026/01/14 15:58:04 | Tip as you like | **¥88.00** | wutay | Limited ability, no respect | | 2026/03/02 14:50:25 | Tip as you like | **¥66.00** | Patrick | Thanks man! Very easy to use! | | 2026/03/01 22:56:17 | Tip as you like | **¥66.00** | xday | Congratulations! | | 2026/03/01 22:40:23 | Tip as you like | **¥66.00** | HanHaocheng | Boss nb, the plug-in is very usefulヽ(*≧ω≦)ノ | | 2026/02/16 21:34:33 | Tip as you like | **¥66.00** | Jack | Happy New Year | | 2026/04/02 19:16:44 | Tip as you like | **¥51.55** | 小七的小洋 | Very good plugin | | 2026/04/21 14:32:40 | Two cups of coffee☕ | **¥50.00** | 安度 | Great, treasure plug-in, come on | | 2026/04/09 13:45:07 | Two cups of coffee☕ | **¥50.00** | 亲 yexizhu811 | Computer synchronization is successful, Android error code=305, message=parameter verification failed Details=context is requ | | 2026/04/08 03:18:40 | Two cups of coffee☕ | **¥50.00** | 彼岸花 | Thanks to the author, this synchronization method solves the trouble of multi-device configuration consistency. | | 2026/04/07 07:52:09 | Two cups of coffee☕ | **¥50.00** | tom | Awesome, much needed, thank you sir. | | 2026/04/03 10:50:44 | Two cups of coffee☕ | **¥50.00** | David | Good work | | 2026/04/02 03:03:20 | Two cups of coffee☕ | **¥50.00** | 狗带带子 | Pay for awesomeness! | | 2026/03/27 10:15:04 | Two cups of coffee☕ | **¥50.00** | 卿 | Good people have a safe life | | 2026/03/18 22:57:03 | Two cups of coffee☕ | **¥50.00** | 灰风 | Thanks for the guidance on telegram | | 2026/03/15 20:09:05 | Two cups of coffee☕ | **¥50.00** | 红星 RedStar | Thanks 🙏, the plug-in is easy to use | | 2026/03/14 23:46:58 | Two cups of coffee☕ | **¥50.00** | fbeis | The whole structure is very good and brings me into the 21st century. | | 2026/03/02 21:00:43 | Two cups of coffee☕ | **¥50.00** | 南科大小魏 | Thank you, boss | | 2026/02/28 13:56:18 | Two cups of coffee☕ | **¥50.00** | xenon | Too hardworking and must be supported | | 2026/02/24 16:37:58 | Two cups of coffee☕ | **¥50.00** | 熙熙煦煦 | Very good synchronization solution | | 2026/02/16 12:14:32 | Two cups of coffee☕ | **¥50.00** | 红殇 | Thanks to the author and Happy New Year! | | 2026/02/14 18:01:55 | Two cups of coffee☕ | **¥50.00** | Jacky龙 | Thanks for developing such a great plug-in, which solves the synchronization problem | | 2026/02/04 10:41:49 | Two cups of coffee☕ | **¥50.00** | 咕咕咕 | The synchronization function is very useful. I hope to continue to iterate and improve it, focusing on note safety. | | 2026/01/31 16:59:55 | Two cups of coffee☕ | **¥50.00** | vulnnull | Well developed, it has indeed solved one of Obsidian’s biggest pain points! | | 2026/01/21 09:37:19 | Two cups of coffee☕ | **¥50.00** | Mojo抖音 | Thank you for the Obsidian synchronization which is very useful👍🏻 | | 2026/01/09 16:34:10 | Two cups of coffee☕ | **¥50.00** | 喆 | Thank you Male Bodhisattva for your OB plug-in for benefiting the world (^🙏^), being petty is disrespectful. | | 2026/03/04 21:42:57 | Tip as you like | **¥30.00** | X | The plug-in is very useful, thank you to the developer | | 2026/02/25 10:55:32 | Tip as you like | **¥30.00** | jeanlaw | The boss’s project helped me a lot! Thank you so much! I hope you guys will continue to work hard | | 2026/03/13 13:43:33 | Tip as you like | **¥29.00** | rocku | Great work, keep it up! | | 2026/02/24 18:36:51 | Tip as you like | **¥25.00** | 淇淇 | I have never been able to find an ideal obsidian synchronization solution. Thank you to the author. Come on! | | 2026/04/21 13:45:19 | A cup of coffee☕ | **¥20.00** | 蓬歌 | Very useful, keep going | | 2026/04/20 15:37:57 | A cup of coffee☕ | **¥20.00** | riding-a-colt | Kneel down to the boss 🧎🏻‍♂️, I hope there will be more updates in the future | | 2026/04/17 00:15:35 | A cup of coffee☕ | **¥20.00** | 稻草人 | It’s easy to use, support it | | 2026/04/15 13:41:41 | A cup of coffee☕ | **¥20.00** | hitomi | Thank you, boss | | 2026/04/15 11:22:39 | A cup of coffee☕ | **¥20.00** | woshiug | How to retrieve forgotten password | | 2026/04/15 06:38:14 | A cup of coffee☕ | **¥20.00** | ke1078 | try | | 2026/04/12 23:54:45 | A cup of coffee☕ | **¥20.00** | Nikki | Great software, thank the author for his contribution and open source sharing. | | 2026/04/12 23:40:17 | A cup of coffee☕ | **¥20.00** | 月非明 | Thank you🙏🏻 | | 2026/04/10 23:00:36 | A cup of coffee☕ | **¥20.00** | wdysjy | Awesome, really useful | | 2026/04/04 01:33:37 | A cup of coffee☕ | **¥20.00** | hsonghao | Have a cup of coffee. Thank you for your hard work. | | 2026/04/03 23:42:51 | A cup of coffee☕ | **¥20.00** | kakaa | Awesome, easy to use | | 2026/04/03 13:28:02 | A cup of coffee☕ | **¥20.00** | 晴天小嘉 | Very useful, thank you | | 2026/04/02 23:08:13 | A cup of coffee☕ | **¥20.00** | 畅 | Thanks man, I hope you can continue to persevere | | 2026/04/02 15:13:46 | A cup of coffee☕ | **¥20.00** | dove | Buy you a coffee, this project is very useful | | 2026/03/30 23:34:12 | A cup of coffee☕ | **¥20.00** | andie | Come on⛽️ | | 2026/03/28 12:46:23 | A cup of coffee☕ | **¥20.00** | zhengbiubiu | Thank you very much, thank you for your hard work | | 2026/03/27 19:45:57 | A cup of coffee☕ | **¥20.00** | IsaacSuo | fast note sync👍 | | 2026/03/26 13:22:50 | A cup of coffee☕ | **¥20.00** | dawn | Wow 👍 | | 2026/03/24 14:45:21 | A cup of coffee☕ | **¥20.00** | Bean | Thank you, the software is very convenient. | | 2026/03/22 23:08:14 | A cup of coffee☕ | **¥20.00** | 拾感 | respect | | 2026/03/19 22:47:54 | A cup of coffee☕ | **¥20.00** | jediknight | Thank you 🙏! Great! | | 2026/03/19 20:20:17 | A cup of coffee☕ | **¥20.00** | Fcjd | Thanks to the developer, it’s so easy to use, come on! | | 2026/03/17 01:17:02 | A cup of coffee☕ | **¥20.00** | southzen | Great job, simple and simple, I hope to continue to optimize~ | | 2026/03/16 16:24:11 | A cup of coffee☕ | **¥20.00** | 长筱团子 | Very useful, meow~Thank you, meow~ | | 2026/03/16 09:22:14 | A cup of coffee☕ | **¥20.00** | barry | Not respectful^_^ | | 2026/03/16 01:01:12 | A cup of coffee☕ | **¥20.00** | Stone | Thank you for your contributions to the open source world | | 2026/03/14 17:15:45 | A cup of coffee☕ | **¥20.00** | R M | Thanks to the author, no disrespect | | 2026/03/14 00:39:54 | A cup of coffee☕ | **¥20.00** | T0_欣 | Boss NB, thank you thank you | | 2026/03/11 20:05:58 | A cup of coffee☕ | **¥20.00** | Ucat | Thanks for the plug-in, it’s very useful and convenient. | | 2026/03/10 01:08:20 | A cup of coffee☕ | **¥20.00** | la | Awesome | | 2026/03/09 09:59:44 | A cup of coffee☕ | **¥20.00** | 耀/ | Easy to use, support | | 2026/03/07 23:36:38 | A cup of coffee☕ | **¥20.00** | 阿叶 | Very good plugin support | | 2026/03/06 09:58:58 | A cup of coffee☕ | **¥20.00** | Dylan | Thanks | | 2026/03/03 00:53:09 | A cup of coffee☕ | **¥20.00** | Alan | Thanks to the author for working hard day and night to write the code and open source it | | 2026/03/01 21:10:27 | A cup of coffee☕ | **¥20.00** | Jack ☑️ | I hope it will be more complete and you can view ob files in other formats on the cloud. | | 2026/02/28 09:50:58 | A cup of coffee☕ | **¥20.00** | tangdh | The idea of ​​this plug-in is very good, keep up the good work | | 2026/02/27 12:48:50 | A cup of coffee☕ | **¥20.00** | aban | Very good sync plugin | | 2026/02/27 11:37:18 | A cup of coffee☕ | **¥20.00** | 三岁 | thank you for your work | | 2026/02/27 11:19:13 | A cup of coffee☕ | **¥20.00** | 行长 | Too strong | | 2026/02/26 11:16:25 | A cup of coffee☕ | **¥20.00** | fausto | Such a good thing should be made known to more people and publicity efforts should be increased! | | 2026/02/25 20:17:42 | A cup of coffee☕ | **¥20.00** | woloin | Very easy to use, thank the author for developing such a good tool! | | 2026/02/24 18:29:45 | A cup of coffee☕ | **¥20.00** | kimi | Thanks to the author for developing such a useful plug-in, which makes my obsidian rotate🥰 | | 2026/02/24 10:01:37 | A cup of coffee☕ | **¥20.00** | ccsir | Plug-in is easy to use | | 2026/02/23 20:53:08 | A cup of coffee☕ | **¥20.00** | KevinYAN | Been using it for a while and it's really great. | | 2026/02/23 19:32:03 | A cup of coffee☕ | **¥20.00** | 繁星影月 | Happy Year of the Horse | | 2026/02/23 12:30:55 | A cup of coffee☕ | **¥20.00** | ahto | Thank you for your hard work | | 2026/02/23 06:24:55 | A cup of coffee☕ | **¥20.00** | 大学生 | Very helpful, come on! | | 2026/02/15 09:25:17 | A cup of coffee☕ | **¥20.00** | sfsun67 | Support, come on~, it is very practical and a highly demanded function. Very useful in the AI ​​era | | 2026/02/10 23:21:38 | A cup of coffee☕ | **¥20.00** | toby | Thanks for the synchronization plug-in made by the boss. It is very easy to use. Please drink a cup of coffee first and I will continue to reward you later. | | 2026/02/10 11:41:36 | A cup of coffee☕ | **¥20.00** | WONG | The synchronization plug-in of the boss is very good. I will continue to support it. | | 2026/02/08 22:02:32 | A cup of coffee☕ | **¥20.00** | 小迪 | Very useful, thank you | | 2026/02/06 08:42:49 | A cup of coffee☕ | **¥20.00** | Max | Come on💪 improve the image editing function on the web | | 2026/01/28 10:48:02 | A cup of coffee☕ | **¥20.00** | 通 | come on | | 2026/01/26 17:21:27 | A cup of coffee☕ | **¥20.00** | CloseCV | 😘 | | 2026/01/16 11:47:13 | A cup of coffee☕ | **¥20.00** | 苏 | Very useful, looking forward to subsequent development and optimization. grateful. | | 2026/01/15 14:51:11 | A cup of coffee☕ | **¥20.00** | 灰风 | Very useful. Thanks! | | 2026/01/09 18:12:17 | A cup of coffee☕ | **¥20.00** | xix | The plug-in idea is so right | | 2026/01/03 22:44:43 | A cup of coffee☕ | **¥20.00** | 姚朝伟 | Hope it gets better and better👌🏻 | | 2026/01/03 14:58:43 | A cup of coffee☕ | **¥20.00** | roao | Great synchronization solution, look forward to the future! Thank you very much for the open source! come on! | | 2026/03/11 09:58:20 | Tip as you like | **¥18.00** | 下鞅 | A small effort to show respect | | 2026/03/02 21:15:39 | Tip as you like | **¥10.00** | 路过打酱 | Come on, God | | 2026/02/28 12:27:51 | Tip as you like | **¥10.00** | 白芷 | Hope it gets better and better | | 2026/02/27 15:54:55 | Tip as you like | **¥10.00** | 柴特 | Thank you to the author for the development, thank you for the open source, and wish it better and better. | | 2026/02/23 15:34:53 | Tip as you like | **¥10.00** | Joe M | Thanks, it’s easier to use than the official synchronization | | 2026/02/20 10:37:02 | Tip as you like | **¥10.00** | 羽山猫四叶 | Thanks for making such a convenient synchronization software | | 2026/03/22 15:09:43 | Tip as you like | **¥9.90** | Shifuwang | Great software, thanks to the author. | | 2026/01/28 12:03:03 | Tip as you like | **¥9.90** | 华星 | Cow🐮 | | 2026/04/09 13:51:11 | Tip as you like | **¥8.88** | 散装白酒🍶 | Come on...you definitely have a bright future. I would recommend it to other friends and think it would be great. | | 2026/03/07 20:29:42 | Tip as you like | **¥8.88** | 皮皮 | Thanks man, it’s very useful, like it | | 2026/01/28 02:52:15 | Tip as you like | **¥8.88** | obsidian | thanks for sharing | | 2026/02/28 19:51:59 | Tip as you like | **¥8.00** | yang | It's very complete, even the server interface is very nice. | | 2026/03/25 11:52:09 | Tip as you like | **¥6.66** | 东 | Pretended to be invincible hahahahahaha | | 2026/03/23 01:02:18 | Tip as you like | **¥6.66** | wishyuwill | You must keep developing! ! ! | | 2026/03/02 17:10:28 | Tip as you like | **¥6.66** | 马孔多的旅人 | Thanks for the development, the plug-in is very useful and will be updated quickly [strong] | | 2026/02/01 23:44:27 | Tip as you like | **¥6.66** | 爱你 | Love you | | 2026/01/09 22:22:25 | Tip as you like | **¥6.66** | kane | The plug-in I wrote is great, keep up the good work! | | 2026/04/20 16:34:57 | Tip as you like | **¥5.00** | david | This is so useful. I feel like I have finally graduated after struggling for so long (╥╯﹏╰╥)ง | | 2026/04/15 00:48:38 | Tip as you like | **¥5.00** | ben | Not easy | | 2026/04/06 20:57:36 | Tip as you like | **¥5.00** | octobersky | Thanks for helping me with your project, obsidian synchronization becomes easy from now on | | 2026/03/01 00:28:19 | Tip as you like | **¥5.00** | colorednoise | Great development | | 2026/02/27 15:18:26 | Tip as you like | **¥5.00** | wudibaolong | support | | 2026/02/14 02:32:50 | Tip as you like | **¥5.00** | 支持开源精神 | Thanks for developing such a useful plug-in | | 2026/02/13 02:13:10 | Tip as you like | **¥5.00** | xxx | reward | | 2026/02/11 17:07:02 | Tip as you like | **¥5.00** | Acckion | come on | | 2026/01/11 14:20:34 | Tip as you like | **¥5.00** | 安宁 | It’s very useful. I hope I can use git to build it. | | 2026/04/18 16:59:14 | Tip as you like | **¥3.66** | jeremy | Continue to develop and make good products | | 2026/02/23 17:47:46 | Tip as you like | **¥3.00** | LL | Happy New Year | | 2026/04/11 12:28:18 | Tip as you like | **¥1.00** | ke1078 | try | | 2026/03/26 15:03:40 | Tip as you like | **¥1.00** | guanyingquan | Thanks, very useful | | 2026/02/24 20:15:19 | Tip as you like | **¥1.00** | Jimmy | Thanks for the very helpful Obs sync plugin! | | 2026/01/08 15:18:06 | Tip as you like | **¥1.00** | 用户 | From discovery and deployment to use, it has been many years, and it feels smooth and silky like never before. It is really great! [strong][strong][strong] | --- *Last updated on: Tue, 21 Apr 2026 13:06:19 GMT* ================================================ FILE: docs/Support.ja.json ================================================ [ { "time": "2026/03/27 00:36:52", "item": "チップはお好みで", "amount": "128.00", "unit": "¥", "message": "素晴らしいです、私はそれを使っています、そしてそれがどんどん良くなることを願っています。", "name": "Geeson" }, { "time": "2026/04/18 21:15:46", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "サポートしてください[拳を握ります]", "name": "lien" }, { "time": "2026/03/29 11:20:42", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "サポート!来て!", "name": "猛将兄" }, { "time": "2026/03/27 15:05:35", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "素晴らしい仕事だ", "name": "Bais" }, { "time": "2026/03/24 09:02:45", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "お疲れ様でした", "name": "cc" }, { "time": "2026/03/22 12:16:07", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "バージョンの反復に対する強力なサポート", "name": "cw" }, { "time": "2026/03/19 13:41:05", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "残業して更新してください", "name": "背背背疼" }, { "time": "2026/03/13 17:28:40", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "オープンソースとご尽力に心より感謝いたします。このプラグインは非常に実用的です。ご支援に感謝いたします。これからも頑張ってください! 💪", "name": "一世风霜" }, { "time": "2026/03/02 09:38:26", "item": "コーヒー4杯☕", "amount": "100.00", "unit": "¥", "message": "とても良いです、開発は簡単ではありません、サポートしてください。", "name": "xuhsu" }, { "time": "2026/01/14 15:58:04", "item": "チップはお好みで", "amount": "88.00", "unit": "¥", "message": "限られた能力、敬意なし", "name": "wutay" }, { "time": "2026/03/02 14:50:25", "item": "チップはお好みで", "amount": "66.00", "unit": "¥", "message": "ありがとう!とても使いやすいです!", "name": "Patrick" }, { "time": "2026/03/01 22:56:17", "item": "チップはお好みで", "amount": "66.00", "unit": "¥", "message": "おめでとう!", "name": "xday" }, { "time": "2026/03/01 22:40:23", "item": "チップはお好みで", "amount": "66.00", "unit": "¥", "message": "ボス、プラグインはとても便利ですよヽ(*≧ω≦)ノ", "name": "HanHaocheng" }, { "time": "2026/02/16 21:34:33", "item": "チップはお好みで", "amount": "66.00", "unit": "¥", "message": "あけましておめでとう", "name": "Jack" }, { "time": "2026/04/02 19:16:44", "item": "チップはお好みで", "amount": "51.55", "unit": "¥", "message": "とても良いプラグイン", "name": "小七的小洋" }, { "time": "2026/04/21 14:32:40", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "素晴らしい、宝のプラグイン、さあ", "name": "安度" }, { "time": "2026/04/09 13:45:07", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "コンピュータの同期は成功しました、Android エラー コード = 305、メッセージ = パラメータの検証に失敗しました 詳細 = コンテキストが要求されています", "name": "亲 yexizhu811" }, { "time": "2026/04/08 03:18:40", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "作者のおかげで、この同期方法により、マルチデバイス構成の一貫性の問題が解決されました。", "name": "彼岸花" }, { "time": "2026/04/07 07:52:09", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "素晴らしいですね、本当に必要でした、ありがとうございます。", "name": "tom" }, { "time": "2026/04/03 10:50:44", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "お疲れ様でした", "name": "David" }, { "time": "2026/04/02 03:03:20", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "素晴らしさに対してお金を払いましょう!", "name": "狗带带子" }, { "time": "2026/03/27 10:15:04", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "善良な人は安全な生活を送っている", "name": "卿" }, { "time": "2026/03/18 22:57:03", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "電報のご指導ありがとうございます", "name": "灰风" }, { "time": "2026/03/15 20:09:05", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "ありがとう 🙏、プラグインは使いやすいです", "name": "红星 RedStar" }, { "time": "2026/03/14 23:46:58", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "全体の構成がとても良く、21世紀を感じさせてくれます。", "name": "fbeis" }, { "time": "2026/03/02 21:00:43", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "ありがとう、ボス", "name": "南科大小魏" }, { "time": "2026/02/28 13:56:18", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "働き者すぎるのでサポートが必要", "name": "xenon" }, { "time": "2026/02/24 16:37:58", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "非常に優れた同期ソリューション", "name": "熙熙煦煦" }, { "time": "2026/02/16 12:14:32", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "著者に感謝します、そして明けましておめでとうございます!", "name": "红殇" }, { "time": "2026/02/14 18:01:55", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "同期の問題を解決する素晴らしいプラグインを開発していただきありがとうございます", "name": "Jacky龙" }, { "time": "2026/02/04 10:41:49", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "同期機能はとても便利です。今後もnoteの安全性を重視して繰り返し改善していきたいと思っています。", "name": "咕咕咕" }, { "time": "2026/01/31 16:59:55", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "よく開発されており、Obsidian の最大の問題点の 1 つが実際に解決されました。", "name": "vulnnull" }, { "time": "2026/01/21 09:37:19", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "とても便利なObsidian同期をありがとうございます👍🏻", "name": "Mojo抖音" }, { "time": "2026/01/09 16:34:10", "item": "コーヒー2杯☕", "amount": "50.00", "unit": "¥", "message": "男菩薩様、OBプラグインを世の中に役立たせていただきありがとうございます(^🙏^)、ケチなことは失礼です。", "name": "喆" }, { "time": "2026/03/04 21:42:57", "item": "チップはお好みで", "amount": "30.00", "unit": "¥", "message": "プラグインは非常に便利です、開発者に感謝します", "name": "X" }, { "time": "2026/02/25 10:55:32", "item": "チップはお好みで", "amount": "30.00", "unit": "¥", "message": "上司のプロジェクトはとても役に立ちました!どうもありがとうございます!皆さんもこれからも頑張ってほしいと思います", "name": "jeanlaw" }, { "time": "2026/03/13 13:43:33", "item": "チップはお好みで", "amount": "29.00", "unit": "¥", "message": "素晴らしい仕事です、これからも頑張ってください!", "name": "rocku" }, { "time": "2026/02/24 18:36:51", "item": "チップはお好みで", "amount": "25.00", "unit": "¥", "message": "私は理想的な黒曜石の同期ソリューションを見つけることができませんでした。著者に感謝します。来て!", "name": "淇淇" }, { "time": "2026/04/21 13:45:19", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても便利です、続けてください", "name": "蓬歌" }, { "time": "2026/04/20 15:37:57", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ボスに跪きなさい🧎🏻‍♂️、今後もっとアップデートがあることを願っています", "name": "riding-a-colt" }, { "time": "2026/04/17 00:15:35", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "使いやすいのでサポートしてください", "name": "稻草人" }, { "time": "2026/04/15 13:41:41", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ありがとう、ボス", "name": "hitomi" }, { "time": "2026/04/15 11:22:39", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "忘れたパスワードを回復する方法", "name": "woshiug" }, { "time": "2026/04/15 06:38:14", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "試す", "name": "ke1078" }, { "time": "2026/04/12 23:54:45", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "素晴らしいソフトウェアです。作者の貢献とオープンソースの共有に感謝します。", "name": "Nikki" }, { "time": "2026/04/12 23:40:17", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ありがとうございます🙏🏻", "name": "月非明" }, { "time": "2026/04/10 23:00:36", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "すごい、本当に便利", "name": "wdysjy" }, { "time": "2026/04/04 01:33:37", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "コーヒーを一杯飲んでください。お疲れ様でした。", "name": "hsonghao" }, { "time": "2026/04/03 23:42:51", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "素晴らしい、使いやすい", "name": "kakaa" }, { "time": "2026/04/03 13:28:02", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても便利です、ありがとう", "name": "晴天小嘉" }, { "time": "2026/04/02 23:08:13", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ありがとう、これからも頑張ってほしい", "name": "畅" }, { "time": "2026/04/02 15:13:46", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "コーヒーをおごってください、このプロジェクトはとても役に立ちます", "name": "dove" }, { "time": "2026/03/30 23:34:12", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "さあ⛽️", "name": "andie" }, { "time": "2026/03/28 12:46:23", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "本当にありがとう、お疲れ様でした", "name": "zhengbiubiu" }, { "time": "2026/03/27 19:45:57", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "高速ノート同期👍", "name": "IsaacSuo" }, { "time": "2026/03/26 13:22:50", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "うわー👍", "name": "dawn" }, { "time": "2026/03/24 14:45:21", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ありがとう、このソフトウェアはとても便利です。", "name": "Bean" }, { "time": "2026/03/22 23:08:14", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "尊敬", "name": "拾感" }, { "time": "2026/03/19 22:47:54", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ありがとうございます🙏!素晴らしい!", "name": "jediknight" }, { "time": "2026/03/19 20:20:17", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "開発者のおかげで、とても使いやすくなりました。ぜひ!", "name": "Fcjd" }, { "time": "2026/03/17 01:17:02", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "素晴らしい仕事です。シンプルかつシンプルです。引き続き最適化を続けていきたいと思います~", "name": "southzen" }, { "time": "2026/03/16 16:24:11", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても便利ですにゃ~ありがとうにゃ~", "name": "长筱团子" }, { "time": "2026/03/16 09:22:14", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "敬意がない^_^", "name": "barry" }, { "time": "2026/03/16 01:01:12", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "オープンソースの世界への貢献に感謝します", "name": "Stone" }, { "time": "2026/03/14 17:15:45", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "作者に感謝します、失礼ではありません", "name": "R M" }, { "time": "2026/03/14 00:39:54", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ボスNB、ありがとう、ありがとう", "name": "T0_欣" }, { "time": "2026/03/11 20:05:58", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "プラグインをありがとう。とても便利で便利です。", "name": "Ucat" }, { "time": "2026/03/10 01:08:20", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "素晴らしい", "name": "la" }, { "time": "2026/03/09 09:59:44", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "使いやすさ、サポート", "name": "耀/" }, { "time": "2026/03/07 23:36:38", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "非常に優れたプラグインのサポート", "name": "阿叶" }, { "time": "2026/03/06 09:58:58", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ありがとう", "name": "Dylan" }, { "time": "2026/03/03 00:53:09", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "コードを書いてオープンソースにするために昼も夜も懸命に働いてくれた作者に感謝します", "name": "Alan" }, { "time": "2026/03/01 21:10:27", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "もっと完全になって、クラウド上で他の形式の ob ファイルを表示できるようになることを願っています。", "name": "Jack ☑️" }, { "time": "2026/02/28 09:50:58", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "このプラグインのアイデアはとても良いです、これからも頑張ってください", "name": "tangdh" }, { "time": "2026/02/27 12:48:50", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "非常に優れた同期プラグイン", "name": "aban" }, { "time": "2026/02/27 11:37:18", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "お疲れ様です", "name": "三岁" }, { "time": "2026/02/27 11:19:13", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "強すぎる", "name": "行长" }, { "time": "2026/02/26 11:16:25", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "このような良いことをもっと多くの人に知ってもらい、宣伝活動を強化すべきです!", "name": "fausto" }, { "time": "2026/02/25 20:17:42", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても使いやすいです。このような良いツールを開発してくれた作者に感謝します。", "name": "woloin" }, { "time": "2026/02/24 18:29:45", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "私の黒曜石を回転させる、このような便利なプラグインを開発してくれた作者に感謝します🥰", "name": "kimi" }, { "time": "2026/02/24 10:01:37", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "プラグインは使いやすい", "name": "ccsir" }, { "time": "2026/02/23 20:53:08", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "しばらく使っていますが、本当に素晴らしいです。", "name": "KevinYAN" }, { "time": "2026/02/23 19:32:03", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "午年おめでとう", "name": "繁星影月" }, { "time": "2026/02/23 12:30:55", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "お疲れ様でした", "name": "ahto" }, { "time": "2026/02/23 06:24:55", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても助かりました、ぜひ!", "name": "大学生" }, { "time": "2026/02/15 09:25:17", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "サポート、さあ~、とても実用的で需要の高い機能です。 AI時代にとても役立つ", "name": "sfsun67" }, { "time": "2026/02/10 23:21:38", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "ボスが作成した同期プラグインに感謝します。使い方はとても簡単です。まずはコーヒーを一杯飲んでください。後ほどご褒美をあげます。", "name": "toby" }, { "time": "2026/02/10 11:41:36", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "Bossの同期プラグインは非常に優れています。これからも応援していきます。", "name": "WONG" }, { "time": "2026/02/08 22:02:32", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても便利です、ありがとう", "name": "小迪" }, { "time": "2026/02/06 08:42:49", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "さあ💪ウェブ上の画像編集機能を改善してください", "name": "Max" }, { "time": "2026/01/28 10:48:02", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "来て", "name": "通" }, { "time": "2026/01/26 17:21:27", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "-", "name": "CloseCV" }, { "time": "2026/01/16 11:47:13", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "非常に便利で、今後の開発と最適化が楽しみです。ありがたい。", "name": "苏" }, { "time": "2026/01/15 14:51:11", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "とても便利です。ありがとう!", "name": "灰风" }, { "time": "2026/01/09 18:12:17", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "プラグインのアイデアはとても正しいです", "name": "xix" }, { "time": "2026/01/03 22:44:43", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "どんどん良くなっていきますように👌🏻", "name": "姚朝伟" }, { "time": "2026/01/03 14:58:43", "item": "コーヒー一杯☕", "amount": "20.00", "unit": "¥", "message": "素晴らしい同期ソリューションです。今後に期待してください。オープンソースをありがとうございます!来て!", "name": "roao" }, { "time": "2026/03/11 09:58:20", "item": "チップはお好みで", "amount": "18.00", "unit": "¥", "message": "敬意を示すための小さな努力", "name": "下鞅" }, { "time": "2026/03/02 21:15:39", "item": "チップはお好みで", "amount": "10.00", "unit": "¥", "message": "さあ、神様", "name": "路过打酱" }, { "time": "2026/02/28 12:27:51", "item": "チップはお好みで", "amount": "10.00", "unit": "¥", "message": "どんどん良くなることを願っています", "name": "白芷" }, { "time": "2026/02/27 15:54:55", "item": "チップはお好みで", "amount": "10.00", "unit": "¥", "message": "開発してくれた作者に感謝し、オープンソースに感謝し、それがますます良くなることを願っています。", "name": "柴特" }, { "time": "2026/02/23 15:34:53", "item": "チップはお好みで", "amount": "10.00", "unit": "¥", "message": "ありがとう、公式同期より使いやすい", "name": "Joe M" }, { "time": "2026/02/20 10:37:02", "item": "チップはお好みで", "amount": "10.00", "unit": "¥", "message": "こんなに便利な同期ソフトを作ってくれてありがとう", "name": "羽山猫四叶" }, { "time": "2026/03/22 15:09:43", "item": "チップはお好みで", "amount": "9.90", "unit": "¥", "message": "素晴らしいソフトウェアです、作者に感謝します。", "name": "Shifuwang" }, { "time": "2026/01/28 12:03:03", "item": "チップはお好みで", "amount": "9.90", "unit": "¥", "message": "牛🐮", "name": "华星" }, { "time": "2026/04/09 13:51:11", "item": "チップはお好みで", "amount": "8.88", "unit": "¥", "message": "さあ…あなたには間違いなく明るい未来があります。他の友達にも勧めたいですし、素晴らしいと思います。", "name": "散装白酒🍶" }, { "time": "2026/03/07 20:29:42", "item": "チップはお好みで", "amount": "8.88", "unit": "¥", "message": "ありがとう、とても便利です、いいね", "name": "皮皮" }, { "time": "2026/01/28 02:52:15", "item": "チップはお好みで", "amount": "8.88", "unit": "¥", "message": "共有してくれてありがとう", "name": "obsidian" }, { "time": "2026/02/28 19:51:59", "item": "チップはお好みで", "amount": "8.00", "unit": "¥", "message": "非常に完成度が高く、サーバーインターフェイスも非常に優れています。", "name": "yang" }, { "time": "2026/03/25 11:52:09", "item": "チップはお好みで", "amount": "6.66", "unit": "¥", "message": "無敵のふりをした、はははははは", "name": "东" }, { "time": "2026/03/23 01:02:18", "item": "チップはお好みで", "amount": "6.66", "unit": "¥", "message": "開発を続けなければなりません! ! !", "name": "wishyuwill" }, { "time": "2026/03/02 17:10:28", "item": "チップはお好みで", "amount": "6.66", "unit": "¥", "message": "開発に感謝します。プラグインは非常に便利で、すぐに更新されます [strong]", "name": "马孔多的旅人" }, { "time": "2026/02/01 23:44:27", "item": "チップはお好みで", "amount": "6.66", "unit": "¥", "message": "愛している", "name": "爱你" }, { "time": "2026/01/09 22:22:25", "item": "チップはお好みで", "amount": "6.66", "unit": "¥", "message": "私が書いたプラグインは素晴らしいです、これからも頑張ってください!", "name": "kane" }, { "time": "2026/04/20 16:34:57", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "これはとても便利です。長い間苦労してやっと卒業できた気がします(╥╯﹏╰╥)ง", "name": "david" }, { "time": "2026/04/15 00:48:38", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "簡単ではない", "name": "ben" }, { "time": "2026/04/06 20:57:36", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "プロジェクトにご協力いただきありがとうございます。これからは黒曜石の同期が簡単になります。", "name": "octobersky" }, { "time": "2026/03/01 00:28:19", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "素晴らしい発展", "name": "colorednoise" }, { "time": "2026/02/27 15:18:26", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "サポート", "name": "wudibaolong" }, { "time": "2026/02/14 02:32:50", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "このような便利なプラグインを開発していただきありがとうございます", "name": "支持开源精神" }, { "time": "2026/02/13 02:13:10", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "褒美", "name": "xxx" }, { "time": "2026/02/11 17:07:02", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "来て", "name": "Acckion" }, { "time": "2026/01/11 14:20:34", "item": "チップはお好みで", "amount": "5.00", "unit": "¥", "message": "とても便利です。 git を使用してビルドできれば幸いです。", "name": "安宁" }, { "time": "2026/04/18 16:59:14", "item": "チップはお好みで", "amount": "3.66", "unit": "¥", "message": "これからも良い製品を開発し、作り続けてください", "name": "jeremy" }, { "time": "2026/02/23 17:47:46", "item": "チップはお好みで", "amount": "3.00", "unit": "¥", "message": "あけましておめでとう", "name": "LL" }, { "time": "2026/04/11 12:28:18", "item": "チップはお好みで", "amount": "1.00", "unit": "¥", "message": "試す", "name": "ke1078" }, { "time": "2026/03/26 15:03:40", "item": "チップはお好みで", "amount": "1.00", "unit": "¥", "message": "ありがとう、とても便利です", "name": "guanyingquan" }, { "time": "2026/02/24 20:15:19", "item": "チップはお好みで", "amount": "1.00", "unit": "¥", "message": "非常に役立つ Obs 同期プラグインをありがとう!", "name": "Jimmy" }, { "time": "2026/01/08 15:18:06", "item": "チップはお好みで", "amount": "1.00", "unit": "¥", "message": "発見から展開、使用まで何年も経ちましたが、今までにないほど滑らかで滑らかな感触です。本当に素晴らしいです! [強い][強い][強い]", "name": "用户" } ] ================================================ FILE: docs/Support.ja.md ================================================ # サポーターリスト > このプロジェクトを応援していただき、誠にありがとうございます!皆様からのご支援は、継続的なメンテナンスと開発の原動力となっています。 ❤️ ### 📜 謝辞リスト | 受領时间 | 项目 | 金额 | 昵称 | メッセージ | | :--- | :--- | :--- | :--- | :--- | | 2026/03/27 00:36:52 | チップはお好みで | **¥128.00** | Geeson | 素晴らしいです、私はそれを使っています、そしてそれがどんどん良くなることを願っています。 | | 2026/04/18 21:15:46 | コーヒー4杯☕ | **¥100.00** | lien | サポートしてください[拳を握ります] | | 2026/03/29 11:20:42 | コーヒー4杯☕ | **¥100.00** | 猛将兄 | サポート!来て! | | 2026/03/27 15:05:35 | コーヒー4杯☕ | **¥100.00** | Bais | 素晴らしい仕事だ | | 2026/03/24 09:02:45 | コーヒー4杯☕ | **¥100.00** | cc | お疲れ様でした | | 2026/03/22 12:16:07 | コーヒー4杯☕ | **¥100.00** | cw | バージョンの反復に対する強力なサポート | | 2026/03/19 13:41:05 | コーヒー4杯☕ | **¥100.00** | 背背背疼 | 残業して更新してください | | 2026/03/13 17:28:40 | コーヒー4杯☕ | **¥100.00** | 一世风霜 | オープンソースとご尽力に心より感謝いたします。このプラグインは非常に実用的です。ご支援に感謝いたします。これからも頑張ってください! 💪 | | 2026/03/02 09:38:26 | コーヒー4杯☕ | **¥100.00** | xuhsu | とても良いです、開発は簡単ではありません、サポートしてください。 | | 2026/01/14 15:58:04 | チップはお好みで | **¥88.00** | wutay | 限られた能力、敬意なし | | 2026/03/02 14:50:25 | チップはお好みで | **¥66.00** | Patrick | ありがとう!とても使いやすいです! | | 2026/03/01 22:56:17 | チップはお好みで | **¥66.00** | xday | おめでとう! | | 2026/03/01 22:40:23 | チップはお好みで | **¥66.00** | HanHaocheng | ボス、プラグインはとても便利ですよヽ(*≧ω≦)ノ | | 2026/02/16 21:34:33 | チップはお好みで | **¥66.00** | Jack | あけましておめでとう | | 2026/04/02 19:16:44 | チップはお好みで | **¥51.55** | 小七的小洋 | とても良いプラグイン | | 2026/04/21 14:32:40 | コーヒー2杯☕ | **¥50.00** | 安度 | 素晴らしい、宝のプラグイン、さあ | | 2026/04/09 13:45:07 | コーヒー2杯☕ | **¥50.00** | 亲 yexizhu811 | コンピュータの同期は成功しました、Android エラー コード = 305、メッセージ = パラメータの検証に失敗しました 詳細 = コンテキストが要求されています | | 2026/04/08 03:18:40 | コーヒー2杯☕ | **¥50.00** | 彼岸花 | 作者のおかげで、この同期方法により、マルチデバイス構成の一貫性の問題が解決されました。 | | 2026/04/07 07:52:09 | コーヒー2杯☕ | **¥50.00** | tom | 素晴らしいですね、本当に必要でした、ありがとうございます。 | | 2026/04/03 10:50:44 | コーヒー2杯☕ | **¥50.00** | David | お疲れ様でした | | 2026/04/02 03:03:20 | コーヒー2杯☕ | **¥50.00** | 狗带带子 | 素晴らしさに対してお金を払いましょう! | | 2026/03/27 10:15:04 | コーヒー2杯☕ | **¥50.00** | 卿 | 善良な人は安全な生活を送っている | | 2026/03/18 22:57:03 | コーヒー2杯☕ | **¥50.00** | 灰风 | 電報のご指導ありがとうございます | | 2026/03/15 20:09:05 | コーヒー2杯☕ | **¥50.00** | 红星 RedStar | ありがとう 🙏、プラグインは使いやすいです | | 2026/03/14 23:46:58 | コーヒー2杯☕ | **¥50.00** | fbeis | 全体の構成がとても良く、21世紀を感じさせてくれます。 | | 2026/03/02 21:00:43 | コーヒー2杯☕ | **¥50.00** | 南科大小魏 | ありがとう、ボス | | 2026/02/28 13:56:18 | コーヒー2杯☕ | **¥50.00** | xenon | 働き者すぎるのでサポートが必要 | | 2026/02/24 16:37:58 | コーヒー2杯☕ | **¥50.00** | 熙熙煦煦 | 非常に優れた同期ソリューション | | 2026/02/16 12:14:32 | コーヒー2杯☕ | **¥50.00** | 红殇 | 著者に感謝します、そして明けましておめでとうございます! | | 2026/02/14 18:01:55 | コーヒー2杯☕ | **¥50.00** | Jacky龙 | 同期の問題を解決する素晴らしいプラグインを開発していただきありがとうございます | | 2026/02/04 10:41:49 | コーヒー2杯☕ | **¥50.00** | 咕咕咕 | 同期機能はとても便利です。今後もnoteの安全性を重視して繰り返し改善していきたいと思っています。 | | 2026/01/31 16:59:55 | コーヒー2杯☕ | **¥50.00** | vulnnull | よく開発されており、Obsidian の最大の問題点の 1 つが実際に解決されました。 | | 2026/01/21 09:37:19 | コーヒー2杯☕ | **¥50.00** | Mojo抖音 | とても便利なObsidian同期をありがとうございます👍🏻 | | 2026/01/09 16:34:10 | コーヒー2杯☕ | **¥50.00** | 喆 | 男菩薩様、OBプラグインを世の中に役立たせていただきありがとうございます(^🙏^)、ケチなことは失礼です。 | | 2026/03/04 21:42:57 | チップはお好みで | **¥30.00** | X | プラグインは非常に便利です、開発者に感謝します | | 2026/02/25 10:55:32 | チップはお好みで | **¥30.00** | jeanlaw | 上司のプロジェクトはとても役に立ちました!どうもありがとうございます!皆さんもこれからも頑張ってほしいと思います | | 2026/03/13 13:43:33 | チップはお好みで | **¥29.00** | rocku | 素晴らしい仕事です、これからも頑張ってください! | | 2026/02/24 18:36:51 | チップはお好みで | **¥25.00** | 淇淇 | 私は理想的な黒曜石の同期ソリューションを見つけることができませんでした。著者に感謝します。来て! | | 2026/04/21 13:45:19 | コーヒー一杯☕ | **¥20.00** | 蓬歌 | とても便利です、続けてください | | 2026/04/20 15:37:57 | コーヒー一杯☕ | **¥20.00** | riding-a-colt | ボスに跪きなさい🧎🏻‍♂️、今後もっとアップデートがあることを願っています | | 2026/04/17 00:15:35 | コーヒー一杯☕ | **¥20.00** | 稻草人 | 使いやすいのでサポートしてください | | 2026/04/15 13:41:41 | コーヒー一杯☕ | **¥20.00** | hitomi | ありがとう、ボス | | 2026/04/15 11:22:39 | コーヒー一杯☕ | **¥20.00** | woshiug | 忘れたパスワードを回復する方法 | | 2026/04/15 06:38:14 | コーヒー一杯☕ | **¥20.00** | ke1078 | 試す | | 2026/04/12 23:54:45 | コーヒー一杯☕ | **¥20.00** | Nikki | 素晴らしいソフトウェアです。作者の貢献とオープンソースの共有に感謝します。 | | 2026/04/12 23:40:17 | コーヒー一杯☕ | **¥20.00** | 月非明 | ありがとうございます🙏🏻 | | 2026/04/10 23:00:36 | コーヒー一杯☕ | **¥20.00** | wdysjy | すごい、本当に便利 | | 2026/04/04 01:33:37 | コーヒー一杯☕ | **¥20.00** | hsonghao | コーヒーを一杯飲んでください。お疲れ様でした。 | | 2026/04/03 23:42:51 | コーヒー一杯☕ | **¥20.00** | kakaa | 素晴らしい、使いやすい | | 2026/04/03 13:28:02 | コーヒー一杯☕ | **¥20.00** | 晴天小嘉 | とても便利です、ありがとう | | 2026/04/02 23:08:13 | コーヒー一杯☕ | **¥20.00** | 畅 | ありがとう、これからも頑張ってほしい | | 2026/04/02 15:13:46 | コーヒー一杯☕ | **¥20.00** | dove | コーヒーをおごってください、このプロジェクトはとても役に立ちます | | 2026/03/30 23:34:12 | コーヒー一杯☕ | **¥20.00** | andie | さあ⛽️ | | 2026/03/28 12:46:23 | コーヒー一杯☕ | **¥20.00** | zhengbiubiu | 本当にありがとう、お疲れ様でした | | 2026/03/27 19:45:57 | コーヒー一杯☕ | **¥20.00** | IsaacSuo | 高速ノート同期👍 | | 2026/03/26 13:22:50 | コーヒー一杯☕ | **¥20.00** | dawn | うわー👍 | | 2026/03/24 14:45:21 | コーヒー一杯☕ | **¥20.00** | Bean | ありがとう、このソフトウェアはとても便利です。 | | 2026/03/22 23:08:14 | コーヒー一杯☕ | **¥20.00** | 拾感 | 尊敬 | | 2026/03/19 22:47:54 | コーヒー一杯☕ | **¥20.00** | jediknight | ありがとうございます🙏!素晴らしい! | | 2026/03/19 20:20:17 | コーヒー一杯☕ | **¥20.00** | Fcjd | 開発者のおかげで、とても使いやすくなりました。ぜひ! | | 2026/03/17 01:17:02 | コーヒー一杯☕ | **¥20.00** | southzen | 素晴らしい仕事です。シンプルかつシンプルです。引き続き最適化を続けていきたいと思います~ | | 2026/03/16 16:24:11 | コーヒー一杯☕ | **¥20.00** | 长筱团子 | とても便利ですにゃ~ありがとうにゃ~ | | 2026/03/16 09:22:14 | コーヒー一杯☕ | **¥20.00** | barry | 敬意がない^_^ | | 2026/03/16 01:01:12 | コーヒー一杯☕ | **¥20.00** | Stone | オープンソースの世界への貢献に感謝します | | 2026/03/14 17:15:45 | コーヒー一杯☕ | **¥20.00** | R M | 作者に感謝します、失礼ではありません | | 2026/03/14 00:39:54 | コーヒー一杯☕ | **¥20.00** | T0_欣 | ボスNB、ありがとう、ありがとう | | 2026/03/11 20:05:58 | コーヒー一杯☕ | **¥20.00** | Ucat | プラグインをありがとう。とても便利で便利です。 | | 2026/03/10 01:08:20 | コーヒー一杯☕ | **¥20.00** | la | 素晴らしい | | 2026/03/09 09:59:44 | コーヒー一杯☕ | **¥20.00** | 耀/ | 使いやすさ、サポート | | 2026/03/07 23:36:38 | コーヒー一杯☕ | **¥20.00** | 阿叶 | 非常に優れたプラグインのサポート | | 2026/03/06 09:58:58 | コーヒー一杯☕ | **¥20.00** | Dylan | ありがとう | | 2026/03/03 00:53:09 | コーヒー一杯☕ | **¥20.00** | Alan | コードを書いてオープンソースにするために昼も夜も懸命に働いてくれた作者に感謝します | | 2026/03/01 21:10:27 | コーヒー一杯☕ | **¥20.00** | Jack ☑️ | もっと完全になって、クラウド上で他の形式の ob ファイルを表示できるようになることを願っています。 | | 2026/02/28 09:50:58 | コーヒー一杯☕ | **¥20.00** | tangdh | このプラグインのアイデアはとても良いです、これからも頑張ってください | | 2026/02/27 12:48:50 | コーヒー一杯☕ | **¥20.00** | aban | 非常に優れた同期プラグイン | | 2026/02/27 11:37:18 | コーヒー一杯☕ | **¥20.00** | 三岁 | お疲れ様です | | 2026/02/27 11:19:13 | コーヒー一杯☕ | **¥20.00** | 行长 | 強すぎる | | 2026/02/26 11:16:25 | コーヒー一杯☕ | **¥20.00** | fausto | このような良いことをもっと多くの人に知ってもらい、宣伝活動を強化すべきです! | | 2026/02/25 20:17:42 | コーヒー一杯☕ | **¥20.00** | woloin | とても使いやすいです。このような良いツールを開発してくれた作者に感謝します。 | | 2026/02/24 18:29:45 | コーヒー一杯☕ | **¥20.00** | kimi | 私の黒曜石を回転させる、このような便利なプラグインを開発してくれた作者に感謝します🥰 | | 2026/02/24 10:01:37 | コーヒー一杯☕ | **¥20.00** | ccsir | プラグインは使いやすい | | 2026/02/23 20:53:08 | コーヒー一杯☕ | **¥20.00** | KevinYAN | しばらく使っていますが、本当に素晴らしいです。 | | 2026/02/23 19:32:03 | コーヒー一杯☕ | **¥20.00** | 繁星影月 | 午年おめでとう | | 2026/02/23 12:30:55 | コーヒー一杯☕ | **¥20.00** | ahto | お疲れ様でした | | 2026/02/23 06:24:55 | コーヒー一杯☕ | **¥20.00** | 大学生 | とても助かりました、ぜひ! | | 2026/02/15 09:25:17 | コーヒー一杯☕ | **¥20.00** | sfsun67 | サポート、さあ~、とても実用的で需要の高い機能です。 AI時代にとても役立つ | | 2026/02/10 23:21:38 | コーヒー一杯☕ | **¥20.00** | toby | ボスが作成した同期プラグインに感謝します。使い方はとても簡単です。まずはコーヒーを一杯飲んでください。後ほどご褒美をあげます。 | | 2026/02/10 11:41:36 | コーヒー一杯☕ | **¥20.00** | WONG | Bossの同期プラグインは非常に優れています。これからも応援していきます。 | | 2026/02/08 22:02:32 | コーヒー一杯☕ | **¥20.00** | 小迪 | とても便利です、ありがとう | | 2026/02/06 08:42:49 | コーヒー一杯☕ | **¥20.00** | Max | さあ💪ウェブ上の画像編集機能を改善してください | | 2026/01/28 10:48:02 | コーヒー一杯☕ | **¥20.00** | 通 | 来て | | 2026/01/26 17:21:27 | コーヒー一杯☕ | **¥20.00** | CloseCV | 😘 | | 2026/01/16 11:47:13 | コーヒー一杯☕ | **¥20.00** | 苏 | 非常に便利で、今後の開発と最適化が楽しみです。ありがたい。 | | 2026/01/15 14:51:11 | コーヒー一杯☕ | **¥20.00** | 灰风 | とても便利です。ありがとう! | | 2026/01/09 18:12:17 | コーヒー一杯☕ | **¥20.00** | xix | プラグインのアイデアはとても正しいです | | 2026/01/03 22:44:43 | コーヒー一杯☕ | **¥20.00** | 姚朝伟 | どんどん良くなっていきますように👌🏻 | | 2026/01/03 14:58:43 | コーヒー一杯☕ | **¥20.00** | roao | 素晴らしい同期ソリューションです。今後に期待してください。オープンソースをありがとうございます!来て! | | 2026/03/11 09:58:20 | チップはお好みで | **¥18.00** | 下鞅 | 敬意を示すための小さな努力 | | 2026/03/02 21:15:39 | チップはお好みで | **¥10.00** | 路过打酱 | さあ、神様 | | 2026/02/28 12:27:51 | チップはお好みで | **¥10.00** | 白芷 | どんどん良くなることを願っています | | 2026/02/27 15:54:55 | チップはお好みで | **¥10.00** | 柴特 | 開発してくれた作者に感謝し、オープンソースに感謝し、それがますます良くなることを願っています。 | | 2026/02/23 15:34:53 | チップはお好みで | **¥10.00** | Joe M | ありがとう、公式同期より使いやすい | | 2026/02/20 10:37:02 | チップはお好みで | **¥10.00** | 羽山猫四叶 | こんなに便利な同期ソフトを作ってくれてありがとう | | 2026/03/22 15:09:43 | チップはお好みで | **¥9.90** | Shifuwang | 素晴らしいソフトウェアです、作者に感謝します。 | | 2026/01/28 12:03:03 | チップはお好みで | **¥9.90** | 华星 | 牛🐮 | | 2026/04/09 13:51:11 | チップはお好みで | **¥8.88** | 散装白酒🍶 | さあ…あなたには間違いなく明るい未来があります。他の友達にも勧めたいですし、素晴らしいと思います。 | | 2026/03/07 20:29:42 | チップはお好みで | **¥8.88** | 皮皮 | ありがとう、とても便利です、いいね | | 2026/01/28 02:52:15 | チップはお好みで | **¥8.88** | obsidian | 共有してくれてありがとう | | 2026/02/28 19:51:59 | チップはお好みで | **¥8.00** | yang | 非常に完成度が高く、サーバーインターフェイスも非常に優れています。 | | 2026/03/25 11:52:09 | チップはお好みで | **¥6.66** | 东 | 無敵のふりをした、はははははは | | 2026/03/23 01:02:18 | チップはお好みで | **¥6.66** | wishyuwill | 開発を続けなければなりません! ! ! | | 2026/03/02 17:10:28 | チップはお好みで | **¥6.66** | 马孔多的旅人 | 開発に感謝します。プラグインは非常に便利で、すぐに更新されます [strong] | | 2026/02/01 23:44:27 | チップはお好みで | **¥6.66** | 爱你 | 愛している | | 2026/01/09 22:22:25 | チップはお好みで | **¥6.66** | kane | 私が書いたプラグインは素晴らしいです、これからも頑張ってください! | | 2026/04/20 16:34:57 | チップはお好みで | **¥5.00** | david | これはとても便利です。長い間苦労してやっと卒業できた気がします(╥╯﹏╰╥)ง | | 2026/04/15 00:48:38 | チップはお好みで | **¥5.00** | ben | 簡単ではない | | 2026/04/06 20:57:36 | チップはお好みで | **¥5.00** | octobersky | プロジェクトにご協力いただきありがとうございます。これからは黒曜石の同期が簡単になります。 | | 2026/03/01 00:28:19 | チップはお好みで | **¥5.00** | colorednoise | 素晴らしい発展 | | 2026/02/27 15:18:26 | チップはお好みで | **¥5.00** | wudibaolong | サポート | | 2026/02/14 02:32:50 | チップはお好みで | **¥5.00** | 支持开源精神 | このような便利なプラグインを開発していただきありがとうございます | | 2026/02/13 02:13:10 | チップはお好みで | **¥5.00** | xxx | 褒美 | | 2026/02/11 17:07:02 | チップはお好みで | **¥5.00** | Acckion | 来て | | 2026/01/11 14:20:34 | チップはお好みで | **¥5.00** | 安宁 | とても便利です。 git を使用してビルドできれば幸いです。 | | 2026/04/18 16:59:14 | チップはお好みで | **¥3.66** | jeremy | これからも良い製品を開発し、作り続けてください | | 2026/02/23 17:47:46 | チップはお好みで | **¥3.00** | LL | あけましておめでとう | | 2026/04/11 12:28:18 | チップはお好みで | **¥1.00** | ke1078 | 試す | | 2026/03/26 15:03:40 | チップはお好みで | **¥1.00** | guanyingquan | ありがとう、とても便利です | | 2026/02/24 20:15:19 | チップはお好みで | **¥1.00** | Jimmy | 非常に役立つ Obs 同期プラグインをありがとう! | | 2026/01/08 15:18:06 | チップはお好みで | **¥1.00** | 用户 | 発見から展開、使用まで何年も経ちましたが、今までにないほど滑らかで滑らかな感触です。本当に素晴らしいです! [強い][強い][強い] | --- *最終更新日:2026/4/21 21:07:54* ================================================ FILE: docs/Support.ko.json ================================================ [ { "time": "2026/03/27 00:36:52", "item": "원하는 대로 팁을 주세요", "amount": "128.00", "unit": "¥", "message": "훌륭해요. 계속 사용하고 있는데, 점점 더 좋아지길 바랍니다.", "name": "Geeson" }, { "time": "2026/04/18 21:15:46", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "지지해줘 [주먹잡기]", "name": "lien" }, { "time": "2026/03/29 11:20:42", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "지원하다! 어서 해봐요!", "name": "猛将兄" }, { "time": "2026/03/27 15:05:35", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "잘했어요", "name": "Bais" }, { "time": "2026/03/24 09:02:45", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "당신의 노고에 감사드립니다", "name": "cc" }, { "time": "2026/03/22 12:16:07", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "버전 반복에 대한 강력한 지원", "name": "cw" }, { "time": "2026/03/19 13:41:05", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "가서 야근해서 업데이트해", "name": "背背背疼" }, { "time": "2026/03/13 17:28:40", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "여러분의 오픈소스와 노력에 진심으로 감사드립니다. 플러그인은 매우 실용적입니다. 귀하의 지원에 감사드립니다. 계속 좋은 일을 하세요! 💪", "name": "一世风霜" }, { "time": "2026/03/02 09:38:26", "item": "커피 네 잔 ☺", "amount": "100.00", "unit": "¥", "message": "아주 좋습니다. 개발이 쉽지 않습니다. 지원해 주세요.", "name": "xuhsu" }, { "time": "2026/01/14 15:58:04", "item": "원하는 대로 팁을 주세요", "amount": "88.00", "unit": "¥", "message": "제한된 능력, 존중 없음", "name": "wutay" }, { "time": "2026/03/02 14:50:25", "item": "원하는 대로 팁을 주세요", "amount": "66.00", "unit": "¥", "message": "고마워요! 사용하기 매우 쉽습니다!", "name": "Patrick" }, { "time": "2026/03/01 22:56:17", "item": "원하는 대로 팁을 주세요", "amount": "66.00", "unit": "¥", "message": "축하해요!", "name": "xday" }, { "time": "2026/03/01 22:40:23", "item": "원하는 대로 팁을 주세요", "amount": "66.00", "unit": "¥", "message": "형님, 플러그인이 매우 유용해요ヽ(*≧ΩDF)ノ", "name": "HanHaocheng" }, { "time": "2026/02/16 21:34:33", "item": "원하는 대로 팁을 주세요", "amount": "66.00", "unit": "¥", "message": "새해 복 많이 받으세요", "name": "Jack" }, { "time": "2026/04/02 19:16:44", "item": "원하는 대로 팁을 주세요", "amount": "51.55", "unit": "¥", "message": "아주 좋은 플러그인", "name": "小七的小洋" }, { "time": "2026/04/21 14:32:40", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "훌륭해, 보물 플러그인, 어서", "name": "安度" }, { "time": "2026/04/09 13:45:07", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "컴퓨터 동기화가 성공했습니다. Android 오류 코드=305, 메시지=매개변수 확인 실패 세부정보=컨텍스트가 필요합니다.", "name": "亲 yexizhu811" }, { "time": "2026/04/08 03:18:40", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "저자 덕분에 이 동기화 방법은 다중 장치 구성 일관성 문제를 해결합니다.", "name": "彼岸花" }, { "time": "2026/04/07 07:52:09", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "정말 필요합니다. 감사합니다.", "name": "tom" }, { "time": "2026/04/03 10:50:44", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "잘했어", "name": "David" }, { "time": "2026/04/02 03:03:20", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "굉장함을 위해 지불하십시오!", "name": "狗带带子" }, { "time": "2026/03/27 10:15:04", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "좋은 사람들은 안전한 삶을 살고 있습니다", "name": "卿" }, { "time": "2026/03/18 22:57:03", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "텔레그램으로 안내해주셔서 감사합니다", "name": "灰风" }, { "time": "2026/03/15 20:09:05", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "감사합니다 🙏 플러그인은 사용하기 쉽습니다", "name": "红星 RedStar" }, { "time": "2026/03/14 23:46:58", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "전체적인 구조가 매우 좋고 나를 21세기로 데려다준다.", "name": "fbeis" }, { "time": "2026/03/02 21:00:43", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "감사합니다, 사장님", "name": "南科大小魏" }, { "time": "2026/02/28 13:56:18", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "너무 열심히 일하고 지원해야합니다", "name": "xenon" }, { "time": "2026/02/24 16:37:58", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "매우 우수한 동기화 솔루션", "name": "熙熙煦煦" }, { "time": "2026/02/16 12:14:32", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "작가님께 감사드리며, 새해 복 많이 받으세요!", "name": "红殇" }, { "time": "2026/02/14 18:01:55", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "동기화 문제를 해결하는 훌륭한 플러그인을 개발해 주셔서 감사합니다.", "name": "Jacky龙" }, { "time": "2026/02/04 10:41:49", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "동기화 기능은 매우 유용합니다. 노트 안전성에 중점을 두고 계속해서 반복하고 개선해 나가고 싶습니다.", "name": "咕咕咕" }, { "time": "2026/01/31 16:59:55", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "잘 개발되어 실제로 Obsidian의 가장 큰 문제점 중 하나를 해결했습니다!", "name": "vulnnull" }, { "time": "2026/01/21 09:37:19", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "매우 유용한 Obsidian 동기화에 감사드립니다👍🏻", "name": "Mojo抖音" }, { "time": "2026/01/09 16:34:10", "item": "커피 두 잔 ☺", "amount": "50.00", "unit": "¥", "message": "세상을 이롭게 하는 OB 플러그인을 주신 남보살님께 감사드립니다(^🙏^), 사소한 것은 무례한 일입니다.", "name": "喆" }, { "time": "2026/03/04 21:42:57", "item": "원하는 대로 팁을 주세요", "amount": "30.00", "unit": "¥", "message": "플러그인이 매우 유용합니다. 개발자님께 감사드립니다.", "name": "X" }, { "time": "2026/02/25 10:55:32", "item": "원하는 대로 팁을 주세요", "amount": "30.00", "unit": "¥", "message": "사장님의 프로젝트가 저에게 많은 도움이 되었어요! 매우 감사합니다! 앞으로도 열심히 해주시기 바랍니다", "name": "jeanlaw" }, { "time": "2026/03/13 13:43:33", "item": "원하는 대로 팁을 주세요", "amount": "29.00", "unit": "¥", "message": "잘했어요. 계속해서 노력하세요!", "name": "rocku" }, { "time": "2026/02/24 18:36:51", "item": "원하는 대로 팁을 주세요", "amount": "25.00", "unit": "¥", "message": "나는 이상적인 흑요석 동기화 솔루션을 찾지 못했습니다. 저자에게 감사드립니다. 어서 해봐요!", "name": "淇淇" }, { "time": "2026/04/21 13:45:19", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "매우 유용합니다. 계속하세요.", "name": "蓬歌" }, { "time": "2026/04/20 15:37:57", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "사장님께 무릎꿇어🧎🏻‍♂️ 앞으로 더 좋은 소식이 있었으면 좋겠습니다", "name": "riding-a-colt" }, { "time": "2026/04/17 00:15:35", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "사용하기 쉽고 지원하세요", "name": "稻草人" }, { "time": "2026/04/15 13:41:41", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "감사합니다, 사장님", "name": "hitomi" }, { "time": "2026/04/15 11:22:39", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "잊어버린 비밀번호를 찾는 방법", "name": "woshiug" }, { "time": "2026/04/15 06:38:14", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "노력하다", "name": "ke1078" }, { "time": "2026/04/12 23:54:45", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "훌륭한 소프트웨어입니다. 작성자의 기여와 오픈 소스 공유에 감사드립니다.", "name": "Nikki" }, { "time": "2026/04/12 23:40:17", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "감사합니다🙏🏻", "name": "月非明" }, { "time": "2026/04/10 23:00:36", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "대단해요 정말 유용해요", "name": "wdysjy" }, { "time": "2026/04/04 01:33:37", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "커피 한잔하세요. 여러분의 노고에 감사드립니다.", "name": "hsonghao" }, { "time": "2026/04/03 23:42:51", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "훌륭하고 사용하기 쉽습니다.", "name": "kakaa" }, { "time": "2026/04/03 13:28:02", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "매우 유용합니다. 감사합니다.", "name": "晴天小嘉" }, { "time": "2026/04/02 23:08:13", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "고마워요, 계속해서 인내할 수 있기를 바라요", "name": "畅" }, { "time": "2026/04/02 15:13:46", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "커피 사주세요. 이 프로젝트는 매우 유용합니다.", "name": "dove" }, { "time": "2026/03/30 23:34:12", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "어서⛽️", "name": "andie" }, { "time": "2026/03/28 12:46:23", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "정말 감사합니다, 열심히 일해주셔서 감사합니다", "name": "zhengbiubiu" }, { "time": "2026/03/27 19:45:57", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "빠른 노트 동기화👍", "name": "IsaacSuo" }, { "time": "2026/03/26 13:22:50", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "와 👍", "name": "dawn" }, { "time": "2026/03/24 14:45:21", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "감사합니다. 소프트웨어가 매우 편리합니다.", "name": "Bean" }, { "time": "2026/03/22 23:08:14", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "존경", "name": "拾感" }, { "time": "2026/03/19 22:47:54", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "감사합니다 🙏! 엄청난!", "name": "jediknight" }, { "time": "2026/03/19 20:20:17", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "개발자님 덕분에 사용하기 너무 쉽습니다.", "name": "Fcjd" }, { "time": "2026/03/17 01:17:02", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "훌륭하고 간단하고 간단합니다. 계속해서 최적화하기를 바랍니다 ~", "name": "southzen" }, { "time": "2026/03/16 16:24:11", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "아주 유용해요, 야옹~고마워요, 야옹~", "name": "长筱团子" }, { "time": "2026/03/16 09:22:14", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "무례해요^_^", "name": "barry" }, { "time": "2026/03/16 01:01:12", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "오픈 소스 세계에 기여해 주셔서 감사합니다", "name": "Stone" }, { "time": "2026/03/14 17:15:45", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "작가님 덕분에 무례함은 없어요", "name": "R M" }, { "time": "2026/03/14 00:39:54", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "NB보스님, 감사합니다. 감사합니다.", "name": "T0_欣" }, { "time": "2026/03/11 20:05:58", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "플러그인을 주셔서 감사합니다. 매우 유용하고 편리합니다.", "name": "Ucat" }, { "time": "2026/03/10 01:08:20", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "엄청난", "name": "la" }, { "time": "2026/03/09 09:59:44", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "사용하기 쉽고 지원", "name": "耀/" }, { "time": "2026/03/07 23:36:38", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "아주 좋은 플러그인 지원", "name": "阿叶" }, { "time": "2026/03/06 09:58:58", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "감사해요", "name": "Dylan" }, { "time": "2026/03/03 00:53:09", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "코드를 작성하고 소스를 공개하기 위해 밤낮으로 열심히 일한 작성자에게 감사드립니다.", "name": "Alan" }, { "time": "2026/03/01 21:10:27", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "좀 더 완벽해지고 클라우드에서 다른 형식의 ob 파일을 볼 수 있기를 바랍니다.", "name": "Jack ☑️" }, { "time": "2026/02/28 09:50:58", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "이 플러그인의 아이디어는 매우 좋습니다. 계속해서 좋은 작업을 해주세요.", "name": "tangdh" }, { "time": "2026/02/27 12:48:50", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "아주 좋은 동기화 플러그인", "name": "aban" }, { "time": "2026/02/27 11:37:18", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "당신의 일에 감사드립니다", "name": "三岁" }, { "time": "2026/02/27 11:19:13", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "너무 강하다", "name": "行长" }, { "time": "2026/02/26 11:16:25", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "이런 좋은 사실을 더 많은 사람들에게 알리고 홍보에 더욱 힘써야겠습니다!", "name": "fausto" }, { "time": "2026/02/25 20:17:42", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "사용하기 매우 쉽습니다. 이렇게 좋은 도구를 개발해 주신 작성자에게 감사드립니다!", "name": "woloin" }, { "time": "2026/02/24 18:29:45", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "흑요석을 회전하게 해주는 유용한 플러그인을 개발해주신 작성자님께 감사드립니다🥰", "name": "kimi" }, { "time": "2026/02/24 10:01:37", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "플러그인은 사용하기 쉽습니다", "name": "ccsir" }, { "time": "2026/02/23 20:53:08", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "한동안 사용해봤는데 정말 좋습니다.", "name": "KevinYAN" }, { "time": "2026/02/23 19:32:03", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "말의 해를 맞아", "name": "繁星影月" }, { "time": "2026/02/23 12:30:55", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "당신의 노고에 감사드립니다", "name": "ahto" }, { "time": "2026/02/23 06:24:55", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "매우 도움이 됩니다. 어서요!", "name": "大学生" }, { "time": "2026/02/15 09:25:17", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "지원하세요~ 매우 실용적이고 수요가 많은 기능입니다. AI 시대에 매우 유용", "name": "sfsun67" }, { "time": "2026/02/10 23:21:38", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "사장님이 만들어주신 동기화 플러그인 감사합니다. 사용하기가 매우 쉽습니다. 먼저 커피 한잔 드시고 나중에 계속 보답해드리겠습니다.", "name": "toby" }, { "time": "2026/02/10 11:41:36", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "보스의 동기화 플러그인은 매우 좋습니다. 나는 그것을 계속 지원할 것입니다.", "name": "WONG" }, { "time": "2026/02/08 22:02:32", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "매우 유용합니다. 감사합니다.", "name": "小迪" }, { "time": "2026/02/06 08:42:49", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "어서💪 웹에서 이미지 편집 기능을 개선해보세요", "name": "Max" }, { "time": "2026/01/28 10:48:02", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "어서 해봐요", "name": "通" }, { "time": "2026/01/26 17:21:27", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "-", "name": "CloseCV" }, { "time": "2026/01/16 11:47:13", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "매우 유용하며 후속 개발 및 최적화를 기대합니다. 고마워하는.", "name": "苏" }, { "time": "2026/01/15 14:51:11", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "매우 유용합니다. 감사해요!", "name": "灰风" }, { "time": "2026/01/09 18:12:17", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "플러그인 아이디어가 너무 옳습니다", "name": "xix" }, { "time": "2026/01/03 22:44:43", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "점점 더 좋아지길 바랍니다👌🏻", "name": "姚朝伟" }, { "time": "2026/01/03 14:58:43", "item": "커피한잔☺", "amount": "20.00", "unit": "¥", "message": "훌륭한 동기화 솔루션, 미래를 기대합니다! 오픈소스 정말 감사드립니다! 어서 해봐요!", "name": "roao" }, { "time": "2026/03/11 09:58:20", "item": "원하는 대로 팁을 주세요", "amount": "18.00", "unit": "¥", "message": "존경심을 표현하기 위한 작은 노력", "name": "下鞅" }, { "time": "2026/03/02 21:15:39", "item": "원하는 대로 팁을 주세요", "amount": "10.00", "unit": "¥", "message": "어서, 하느님", "name": "路过打酱" }, { "time": "2026/02/28 12:27:51", "item": "원하는 대로 팁을 주세요", "amount": "10.00", "unit": "¥", "message": "점점 좋아지길 바라요", "name": "白芷" }, { "time": "2026/02/27 15:54:55", "item": "원하는 대로 팁을 주세요", "amount": "10.00", "unit": "¥", "message": "개발을 해주신 작성자에게 감사드리며, 오픈 소스에 감사드리며, 더욱 더 좋아지기를 바랍니다.", "name": "柴特" }, { "time": "2026/02/23 15:34:53", "item": "원하는 대로 팁을 주세요", "amount": "10.00", "unit": "¥", "message": "감사합니다. 공식 동기화보다 사용하기가 더 쉽습니다.", "name": "Joe M" }, { "time": "2026/02/20 10:37:02", "item": "원하는 대로 팁을 주세요", "amount": "10.00", "unit": "¥", "message": "이렇게 편리한 동기화 소프트웨어를 만들어주셔서 감사합니다", "name": "羽山猫四叶" }, { "time": "2026/03/22 15:09:43", "item": "원하는 대로 팁을 주세요", "amount": "9.90", "unit": "¥", "message": "작성자에게 감사드립니다. 훌륭한 소프트웨어입니다.", "name": "Shifuwang" }, { "time": "2026/01/28 12:03:03", "item": "원하는 대로 팁을 주세요", "amount": "9.90", "unit": "¥", "message": "소🐮", "name": "华星" }, { "time": "2026/04/09 13:51:11", "item": "원하는 대로 팁을 주세요", "amount": "8.88", "unit": "¥", "message": "어서...당신의 미래는 분명 밝습니다. 다른 친구들에게도 추천하고 좋을 것 같아요.", "name": "散装白酒🍶" }, { "time": "2026/03/07 20:29:42", "item": "원하는 대로 팁을 주세요", "amount": "8.88", "unit": "¥", "message": "고마워요, 정말 유용해요. 좋아요", "name": "皮皮" }, { "time": "2026/01/28 02:52:15", "item": "원하는 대로 팁을 주세요", "amount": "8.88", "unit": "¥", "message": "공유해주셔서 감사합니다", "name": "obsidian" }, { "time": "2026/02/28 19:51:59", "item": "원하는 대로 팁을 주세요", "amount": "8.00", "unit": "¥", "message": "매우 완벽하고 서버 인터페이스도 매우 훌륭합니다.", "name": "yang" }, { "time": "2026/03/25 11:52:09", "item": "원하는 대로 팁을 주세요", "amount": "6.66", "unit": "¥", "message": "무적인 척 하하하하하하", "name": "东" }, { "time": "2026/03/23 01:02:18", "item": "원하는 대로 팁을 주세요", "amount": "6.66", "unit": "¥", "message": "계속 발전해야 해요! ! !", "name": "wishyuwill" }, { "time": "2026/03/02 17:10:28", "item": "원하는 대로 팁을 주세요", "amount": "6.66", "unit": "¥", "message": "개발해주셔서 감사합니다. 플러그인은 매우 유용하며 빠르게 업데이트될 예정입니다. [strong]", "name": "马孔多的旅人" }, { "time": "2026/02/01 23:44:27", "item": "원하는 대로 팁을 주세요", "amount": "6.66", "unit": "¥", "message": "사랑해요", "name": "爱你" }, { "time": "2026/01/09 22:22:25", "item": "원하는 대로 팁을 주세요", "amount": "6.66", "unit": "¥", "message": "제가 작성한 플러그인은 훌륭합니다. 계속해서 좋은 일을 해주세요!", "name": "kane" }, { "time": "2026/04/20 16:34:57", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "이것은 매우 유용합니다. 오랜 시간 고생 끝에 드디어 졸업한 기분이에요 (╥╯﹏╰╥)§", "name": "david" }, { "time": "2026/04/15 00:48:38", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "쉽지 않다", "name": "ben" }, { "time": "2026/04/06 20:57:36", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "프로젝트에 도움을 주셔서 감사합니다. 이제부터 흑요석 동기화가 쉬워집니다.", "name": "octobersky" }, { "time": "2026/03/01 00:28:19", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "대단한 발전", "name": "colorednoise" }, { "time": "2026/02/27 15:18:26", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "지원하다", "name": "wudibaolong" }, { "time": "2026/02/14 02:32:50", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "유용한 플러그인을 개발해주셔서 감사합니다", "name": "支持开源精神" }, { "time": "2026/02/13 02:13:10", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "보상", "name": "xxx" }, { "time": "2026/02/11 17:07:02", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "어서 해봐요", "name": "Acckion" }, { "time": "2026/01/11 14:20:34", "item": "원하는 대로 팁을 주세요", "amount": "5.00", "unit": "¥", "message": "매우 유용합니다. git을 사용하여 빌드할 수 있기를 바랍니다.", "name": "安宁" }, { "time": "2026/04/18 16:59:14", "item": "원하는 대로 팁을 주세요", "amount": "3.66", "unit": "¥", "message": "앞으로도 좋은 제품을 개발하고 만들어주세요", "name": "jeremy" }, { "time": "2026/02/23 17:47:46", "item": "원하는 대로 팁을 주세요", "amount": "3.00", "unit": "¥", "message": "새해 복 많이 받으세요", "name": "LL" }, { "time": "2026/04/11 12:28:18", "item": "원하는 대로 팁을 주세요", "amount": "1.00", "unit": "¥", "message": "노력하다", "name": "ke1078" }, { "time": "2026/03/26 15:03:40", "item": "원하는 대로 팁을 주세요", "amount": "1.00", "unit": "¥", "message": "고마워요, 매우 유용해요", "name": "guanyingquan" }, { "time": "2026/02/24 20:15:19", "item": "원하는 대로 팁을 주세요", "amount": "1.00", "unit": "¥", "message": "매우 유용한 Obs 동기화 플러그인에 감사드립니다!", "name": "Jimmy" }, { "time": "2026/01/08 15:18:06", "item": "원하는 대로 팁을 주세요", "amount": "1.00", "unit": "¥", "message": "발견과 배포부터 사용까지 수년이 걸렸으며 이전과는 비교할 수 없을 정도로 부드럽고 매끄러운 느낌을 줍니다. 정말 훌륭해요! [강함][강함][강함]", "name": "用户" } ] ================================================ FILE: docs/Support.ko.md ================================================ # 후원자 명단 > 이 프로젝트를 지원해 주셔서 정말 감사합니다! 여러분의 모든 후원은 지속적인 유지보수와 개발의 원동력이 됩니다. ❤️ ### 📜 감사 명단 | 수령 시간 | 항목 | 금액 | 닉네임 | 메시지 | | :--- | :--- | :--- | :--- | :--- | | 2026/03/27 00:36:52 | 원하는 대로 팁을 주세요 | **¥128.00** | Geeson | 훌륭해요. 계속 사용하고 있는데, 점점 더 좋아지길 바랍니다. | | 2026/04/18 21:15:46 | 커피 네 잔 ☺ | **¥100.00** | lien | 지지해줘 [주먹잡기] | | 2026/03/29 11:20:42 | 커피 네 잔 ☺ | **¥100.00** | 猛将兄 | 지원하다! 어서 해봐요! | | 2026/03/27 15:05:35 | 커피 네 잔 ☺ | **¥100.00** | Bais | 잘했어요 | | 2026/03/24 09:02:45 | 커피 네 잔 ☺ | **¥100.00** | cc | 당신의 노고에 감사드립니다 | | 2026/03/22 12:16:07 | 커피 네 잔 ☺ | **¥100.00** | cw | 버전 반복에 대한 강력한 지원 | | 2026/03/19 13:41:05 | 커피 네 잔 ☺ | **¥100.00** | 背背背疼 | 가서 야근해서 업데이트해 | | 2026/03/13 17:28:40 | 커피 네 잔 ☺ | **¥100.00** | 一世风霜 | 여러분의 오픈소스와 노력에 진심으로 감사드립니다. 플러그인은 매우 실용적입니다. 귀하의 지원에 감사드립니다. 계속 좋은 일을 하세요! 💪 | | 2026/03/02 09:38:26 | 커피 네 잔 ☺ | **¥100.00** | xuhsu | 아주 좋습니다. 개발이 쉽지 않습니다. 지원해 주세요. | | 2026/01/14 15:58:04 | 원하는 대로 팁을 주세요 | **¥88.00** | wutay | 제한된 능력, 존중 없음 | | 2026/03/02 14:50:25 | 원하는 대로 팁을 주세요 | **¥66.00** | Patrick | 고마워요! 사용하기 매우 쉽습니다! | | 2026/03/01 22:56:17 | 원하는 대로 팁을 주세요 | **¥66.00** | xday | 축하해요! | | 2026/03/01 22:40:23 | 원하는 대로 팁을 주세요 | **¥66.00** | HanHaocheng | 형님, 플러그인이 매우 유용해요ヽ(*≧ΩDF)ノ | | 2026/02/16 21:34:33 | 원하는 대로 팁을 주세요 | **¥66.00** | Jack | 새해 복 많이 받으세요 | | 2026/04/02 19:16:44 | 원하는 대로 팁을 주세요 | **¥51.55** | 小七的小洋 | 아주 좋은 플러그인 | | 2026/04/21 14:32:40 | 커피 두 잔 ☺ | **¥50.00** | 安度 | 훌륭해, 보물 플러그인, 어서 | | 2026/04/09 13:45:07 | 커피 두 잔 ☺ | **¥50.00** | 亲 yexizhu811 | 컴퓨터 동기화가 성공했습니다. Android 오류 코드=305, 메시지=매개변수 확인 실패 세부정보=컨텍스트가 필요합니다. | | 2026/04/08 03:18:40 | 커피 두 잔 ☺ | **¥50.00** | 彼岸花 | 저자 덕분에 이 동기화 방법은 다중 장치 구성 일관성 문제를 해결합니다. | | 2026/04/07 07:52:09 | 커피 두 잔 ☺ | **¥50.00** | tom | 정말 필요합니다. 감사합니다. | | 2026/04/03 10:50:44 | 커피 두 잔 ☺ | **¥50.00** | David | 잘했어 | | 2026/04/02 03:03:20 | 커피 두 잔 ☺ | **¥50.00** | 狗带带子 | 굉장함을 위해 지불하십시오! | | 2026/03/27 10:15:04 | 커피 두 잔 ☺ | **¥50.00** | 卿 | 좋은 사람들은 안전한 삶을 살고 있습니다 | | 2026/03/18 22:57:03 | 커피 두 잔 ☺ | **¥50.00** | 灰风 | 텔레그램으로 안내해주셔서 감사합니다 | | 2026/03/15 20:09:05 | 커피 두 잔 ☺ | **¥50.00** | 红星 RedStar | 감사합니다 🙏 플러그인은 사용하기 쉽습니다 | | 2026/03/14 23:46:58 | 커피 두 잔 ☺ | **¥50.00** | fbeis | 전체적인 구조가 매우 좋고 나를 21세기로 데려다준다. | | 2026/03/02 21:00:43 | 커피 두 잔 ☺ | **¥50.00** | 南科大小魏 | 감사합니다, 사장님 | | 2026/02/28 13:56:18 | 커피 두 잔 ☺ | **¥50.00** | xenon | 너무 열심히 일하고 지원해야합니다 | | 2026/02/24 16:37:58 | 커피 두 잔 ☺ | **¥50.00** | 熙熙煦煦 | 매우 우수한 동기화 솔루션 | | 2026/02/16 12:14:32 | 커피 두 잔 ☺ | **¥50.00** | 红殇 | 작가님께 감사드리며, 새해 복 많이 받으세요! | | 2026/02/14 18:01:55 | 커피 두 잔 ☺ | **¥50.00** | Jacky龙 | 동기화 문제를 해결하는 훌륭한 플러그인을 개발해 주셔서 감사합니다. | | 2026/02/04 10:41:49 | 커피 두 잔 ☺ | **¥50.00** | 咕咕咕 | 동기화 기능은 매우 유용합니다. 노트 안전성에 중점을 두고 계속해서 반복하고 개선해 나가고 싶습니다. | | 2026/01/31 16:59:55 | 커피 두 잔 ☺ | **¥50.00** | vulnnull | 잘 개발되어 실제로 Obsidian의 가장 큰 문제점 중 하나를 해결했습니다! | | 2026/01/21 09:37:19 | 커피 두 잔 ☺ | **¥50.00** | Mojo抖音 | 매우 유용한 Obsidian 동기화에 감사드립니다👍🏻 | | 2026/01/09 16:34:10 | 커피 두 잔 ☺ | **¥50.00** | 喆 | 세상을 이롭게 하는 OB 플러그인을 주신 남보살님께 감사드립니다(^🙏^), 사소한 것은 무례한 일입니다. | | 2026/03/04 21:42:57 | 원하는 대로 팁을 주세요 | **¥30.00** | X | 플러그인이 매우 유용합니다. 개발자님께 감사드립니다. | | 2026/02/25 10:55:32 | 원하는 대로 팁을 주세요 | **¥30.00** | jeanlaw | 사장님의 프로젝트가 저에게 많은 도움이 되었어요! 매우 감사합니다! 앞으로도 열심히 해주시기 바랍니다 | | 2026/03/13 13:43:33 | 원하는 대로 팁을 주세요 | **¥29.00** | rocku | 잘했어요. 계속해서 노력하세요! | | 2026/02/24 18:36:51 | 원하는 대로 팁을 주세요 | **¥25.00** | 淇淇 | 나는 이상적인 흑요석 동기화 솔루션을 찾지 못했습니다. 저자에게 감사드립니다. 어서 해봐요! | | 2026/04/21 13:45:19 | 커피한잔☺ | **¥20.00** | 蓬歌 | 매우 유용합니다. 계속하세요. | | 2026/04/20 15:37:57 | 커피한잔☺ | **¥20.00** | riding-a-colt | 사장님께 무릎꿇어🧎🏻‍♂️ 앞으로 더 좋은 소식이 있었으면 좋겠습니다 | | 2026/04/17 00:15:35 | 커피한잔☺ | **¥20.00** | 稻草人 | 사용하기 쉽고 지원하세요 | | 2026/04/15 13:41:41 | 커피한잔☺ | **¥20.00** | hitomi | 감사합니다, 사장님 | | 2026/04/15 11:22:39 | 커피한잔☺ | **¥20.00** | woshiug | 잊어버린 비밀번호를 찾는 방법 | | 2026/04/15 06:38:14 | 커피한잔☺ | **¥20.00** | ke1078 | 노력하다 | | 2026/04/12 23:54:45 | 커피한잔☺ | **¥20.00** | Nikki | 훌륭한 소프트웨어입니다. 작성자의 기여와 오픈 소스 공유에 감사드립니다. | | 2026/04/12 23:40:17 | 커피한잔☺ | **¥20.00** | 月非明 | 감사합니다🙏🏻 | | 2026/04/10 23:00:36 | 커피한잔☺ | **¥20.00** | wdysjy | 대단해요 정말 유용해요 | | 2026/04/04 01:33:37 | 커피한잔☺ | **¥20.00** | hsonghao | 커피 한잔하세요. 여러분의 노고에 감사드립니다. | | 2026/04/03 23:42:51 | 커피한잔☺ | **¥20.00** | kakaa | 훌륭하고 사용하기 쉽습니다. | | 2026/04/03 13:28:02 | 커피한잔☺ | **¥20.00** | 晴天小嘉 | 매우 유용합니다. 감사합니다. | | 2026/04/02 23:08:13 | 커피한잔☺ | **¥20.00** | 畅 | 고마워요, 계속해서 인내할 수 있기를 바라요 | | 2026/04/02 15:13:46 | 커피한잔☺ | **¥20.00** | dove | 커피 사주세요. 이 프로젝트는 매우 유용합니다. | | 2026/03/30 23:34:12 | 커피한잔☺ | **¥20.00** | andie | 어서⛽️ | | 2026/03/28 12:46:23 | 커피한잔☺ | **¥20.00** | zhengbiubiu | 정말 감사합니다, 열심히 일해주셔서 감사합니다 | | 2026/03/27 19:45:57 | 커피한잔☺ | **¥20.00** | IsaacSuo | 빠른 노트 동기화👍 | | 2026/03/26 13:22:50 | 커피한잔☺ | **¥20.00** | dawn | 와 👍 | | 2026/03/24 14:45:21 | 커피한잔☺ | **¥20.00** | Bean | 감사합니다. 소프트웨어가 매우 편리합니다. | | 2026/03/22 23:08:14 | 커피한잔☺ | **¥20.00** | 拾感 | 존경 | | 2026/03/19 22:47:54 | 커피한잔☺ | **¥20.00** | jediknight | 감사합니다 🙏! 엄청난! | | 2026/03/19 20:20:17 | 커피한잔☺ | **¥20.00** | Fcjd | 개발자님 덕분에 사용하기 너무 쉽습니다. | | 2026/03/17 01:17:02 | 커피한잔☺ | **¥20.00** | southzen | 훌륭하고 간단하고 간단합니다. 계속해서 최적화하기를 바랍니다 ~ | | 2026/03/16 16:24:11 | 커피한잔☺ | **¥20.00** | 长筱团子 | 아주 유용해요, 야옹~고마워요, 야옹~ | | 2026/03/16 09:22:14 | 커피한잔☺ | **¥20.00** | barry | 무례해요^_^ | | 2026/03/16 01:01:12 | 커피한잔☺ | **¥20.00** | Stone | 오픈 소스 세계에 기여해 주셔서 감사합니다 | | 2026/03/14 17:15:45 | 커피한잔☺ | **¥20.00** | R M | 작가님 덕분에 무례함은 없어요 | | 2026/03/14 00:39:54 | 커피한잔☺ | **¥20.00** | T0_欣 | NB보스님, 감사합니다. 감사합니다. | | 2026/03/11 20:05:58 | 커피한잔☺ | **¥20.00** | Ucat | 플러그인을 주셔서 감사합니다. 매우 유용하고 편리합니다. | | 2026/03/10 01:08:20 | 커피한잔☺ | **¥20.00** | la | 엄청난 | | 2026/03/09 09:59:44 | 커피한잔☺ | **¥20.00** | 耀/ | 사용하기 쉽고 지원 | | 2026/03/07 23:36:38 | 커피한잔☺ | **¥20.00** | 阿叶 | 아주 좋은 플러그인 지원 | | 2026/03/06 09:58:58 | 커피한잔☺ | **¥20.00** | Dylan | 감사해요 | | 2026/03/03 00:53:09 | 커피한잔☺ | **¥20.00** | Alan | 코드를 작성하고 소스를 공개하기 위해 밤낮으로 열심히 일한 작성자에게 감사드립니다. | | 2026/03/01 21:10:27 | 커피한잔☺ | **¥20.00** | Jack ☑️ | 좀 더 완벽해지고 클라우드에서 다른 형식의 ob 파일을 볼 수 있기를 바랍니다. | | 2026/02/28 09:50:58 | 커피한잔☺ | **¥20.00** | tangdh | 이 플러그인의 아이디어는 매우 좋습니다. 계속해서 좋은 작업을 해주세요. | | 2026/02/27 12:48:50 | 커피한잔☺ | **¥20.00** | aban | 아주 좋은 동기화 플러그인 | | 2026/02/27 11:37:18 | 커피한잔☺ | **¥20.00** | 三岁 | 당신의 일에 감사드립니다 | | 2026/02/27 11:19:13 | 커피한잔☺ | **¥20.00** | 行长 | 너무 강하다 | | 2026/02/26 11:16:25 | 커피한잔☺ | **¥20.00** | fausto | 이런 좋은 사실을 더 많은 사람들에게 알리고 홍보에 더욱 힘써야겠습니다! | | 2026/02/25 20:17:42 | 커피한잔☺ | **¥20.00** | woloin | 사용하기 매우 쉽습니다. 이렇게 좋은 도구를 개발해 주신 작성자에게 감사드립니다! | | 2026/02/24 18:29:45 | 커피한잔☺ | **¥20.00** | kimi | 흑요석을 회전하게 해주는 유용한 플러그인을 개발해주신 작성자님께 감사드립니다🥰 | | 2026/02/24 10:01:37 | 커피한잔☺ | **¥20.00** | ccsir | 플러그인은 사용하기 쉽습니다 | | 2026/02/23 20:53:08 | 커피한잔☺ | **¥20.00** | KevinYAN | 한동안 사용해봤는데 정말 좋습니다. | | 2026/02/23 19:32:03 | 커피한잔☺ | **¥20.00** | 繁星影月 | 말의 해를 맞아 | | 2026/02/23 12:30:55 | 커피한잔☺ | **¥20.00** | ahto | 당신의 노고에 감사드립니다 | | 2026/02/23 06:24:55 | 커피한잔☺ | **¥20.00** | 大学生 | 매우 도움이 됩니다. 어서요! | | 2026/02/15 09:25:17 | 커피한잔☺ | **¥20.00** | sfsun67 | 지원하세요~ 매우 실용적이고 수요가 많은 기능입니다. AI 시대에 매우 유용 | | 2026/02/10 23:21:38 | 커피한잔☺ | **¥20.00** | toby | 사장님이 만들어주신 동기화 플러그인 감사합니다. 사용하기가 매우 쉽습니다. 먼저 커피 한잔 드시고 나중에 계속 보답해드리겠습니다. | | 2026/02/10 11:41:36 | 커피한잔☺ | **¥20.00** | WONG | 보스의 동기화 플러그인은 매우 좋습니다. 나는 그것을 계속 지원할 것입니다. | | 2026/02/08 22:02:32 | 커피한잔☺ | **¥20.00** | 小迪 | 매우 유용합니다. 감사합니다. | | 2026/02/06 08:42:49 | 커피한잔☺ | **¥20.00** | Max | 어서💪 웹에서 이미지 편집 기능을 개선해보세요 | | 2026/01/28 10:48:02 | 커피한잔☺ | **¥20.00** | 通 | 어서 해봐요 | | 2026/01/26 17:21:27 | 커피한잔☺ | **¥20.00** | CloseCV | 😘 | | 2026/01/16 11:47:13 | 커피한잔☺ | **¥20.00** | 苏 | 매우 유용하며 후속 개발 및 최적화를 기대합니다. 고마워하는. | | 2026/01/15 14:51:11 | 커피한잔☺ | **¥20.00** | 灰风 | 매우 유용합니다. 감사해요! | | 2026/01/09 18:12:17 | 커피한잔☺ | **¥20.00** | xix | 플러그인 아이디어가 너무 옳습니다 | | 2026/01/03 22:44:43 | 커피한잔☺ | **¥20.00** | 姚朝伟 | 점점 더 좋아지길 바랍니다👌🏻 | | 2026/01/03 14:58:43 | 커피한잔☺ | **¥20.00** | roao | 훌륭한 동기화 솔루션, 미래를 기대합니다! 오픈소스 정말 감사드립니다! 어서 해봐요! | | 2026/03/11 09:58:20 | 원하는 대로 팁을 주세요 | **¥18.00** | 下鞅 | 존경심을 표현하기 위한 작은 노력 | | 2026/03/02 21:15:39 | 원하는 대로 팁을 주세요 | **¥10.00** | 路过打酱 | 어서, 하느님 | | 2026/02/28 12:27:51 | 원하는 대로 팁을 주세요 | **¥10.00** | 白芷 | 점점 좋아지길 바라요 | | 2026/02/27 15:54:55 | 원하는 대로 팁을 주세요 | **¥10.00** | 柴特 | 개발을 해주신 작성자에게 감사드리며, 오픈 소스에 감사드리며, 더욱 더 좋아지기를 바랍니다. | | 2026/02/23 15:34:53 | 원하는 대로 팁을 주세요 | **¥10.00** | Joe M | 감사합니다. 공식 동기화보다 사용하기가 더 쉽습니다. | | 2026/02/20 10:37:02 | 원하는 대로 팁을 주세요 | **¥10.00** | 羽山猫四叶 | 이렇게 편리한 동기화 소프트웨어를 만들어주셔서 감사합니다 | | 2026/03/22 15:09:43 | 원하는 대로 팁을 주세요 | **¥9.90** | Shifuwang | 작성자에게 감사드립니다. 훌륭한 소프트웨어입니다. | | 2026/01/28 12:03:03 | 원하는 대로 팁을 주세요 | **¥9.90** | 华星 | 소🐮 | | 2026/04/09 13:51:11 | 원하는 대로 팁을 주세요 | **¥8.88** | 散装白酒🍶 | 어서...당신의 미래는 분명 밝습니다. 다른 친구들에게도 추천하고 좋을 것 같아요. | | 2026/03/07 20:29:42 | 원하는 대로 팁을 주세요 | **¥8.88** | 皮皮 | 고마워요, 정말 유용해요. 좋아요 | | 2026/01/28 02:52:15 | 원하는 대로 팁을 주세요 | **¥8.88** | obsidian | 공유해주셔서 감사합니다 | | 2026/02/28 19:51:59 | 원하는 대로 팁을 주세요 | **¥8.00** | yang | 매우 완벽하고 서버 인터페이스도 매우 훌륭합니다. | | 2026/03/25 11:52:09 | 원하는 대로 팁을 주세요 | **¥6.66** | 东 | 무적인 척 하하하하하하 | | 2026/03/23 01:02:18 | 원하는 대로 팁을 주세요 | **¥6.66** | wishyuwill | 계속 발전해야 해요! ! ! | | 2026/03/02 17:10:28 | 원하는 대로 팁을 주세요 | **¥6.66** | 马孔多的旅人 | 개발해주셔서 감사합니다. 플러그인은 매우 유용하며 빠르게 업데이트될 예정입니다. [strong] | | 2026/02/01 23:44:27 | 원하는 대로 팁을 주세요 | **¥6.66** | 爱你 | 사랑해요 | | 2026/01/09 22:22:25 | 원하는 대로 팁을 주세요 | **¥6.66** | kane | 제가 작성한 플러그인은 훌륭합니다. 계속해서 좋은 일을 해주세요! | | 2026/04/20 16:34:57 | 원하는 대로 팁을 주세요 | **¥5.00** | david | 이것은 매우 유용합니다. 오랜 시간 고생 끝에 드디어 졸업한 기분이에요 (╥╯﹏╰╥)§ | | 2026/04/15 00:48:38 | 원하는 대로 팁을 주세요 | **¥5.00** | ben | 쉽지 않다 | | 2026/04/06 20:57:36 | 원하는 대로 팁을 주세요 | **¥5.00** | octobersky | 프로젝트에 도움을 주셔서 감사합니다. 이제부터 흑요석 동기화가 쉬워집니다. | | 2026/03/01 00:28:19 | 원하는 대로 팁을 주세요 | **¥5.00** | colorednoise | 대단한 발전 | | 2026/02/27 15:18:26 | 원하는 대로 팁을 주세요 | **¥5.00** | wudibaolong | 지원하다 | | 2026/02/14 02:32:50 | 원하는 대로 팁을 주세요 | **¥5.00** | 支持开源精神 | 유용한 플러그인을 개발해주셔서 감사합니다 | | 2026/02/13 02:13:10 | 원하는 대로 팁을 주세요 | **¥5.00** | xxx | 보상 | | 2026/02/11 17:07:02 | 원하는 대로 팁을 주세요 | **¥5.00** | Acckion | 어서 해봐요 | | 2026/01/11 14:20:34 | 원하는 대로 팁을 주세요 | **¥5.00** | 安宁 | 매우 유용합니다. git을 사용하여 빌드할 수 있기를 바랍니다. | | 2026/04/18 16:59:14 | 원하는 대로 팁을 주세요 | **¥3.66** | jeremy | 앞으로도 좋은 제품을 개발하고 만들어주세요 | | 2026/02/23 17:47:46 | 원하는 대로 팁을 주세요 | **¥3.00** | LL | 새해 복 많이 받으세요 | | 2026/04/11 12:28:18 | 원하는 대로 팁을 주세요 | **¥1.00** | ke1078 | 노력하다 | | 2026/03/26 15:03:40 | 원하는 대로 팁을 주세요 | **¥1.00** | guanyingquan | 고마워요, 매우 유용해요 | | 2026/02/24 20:15:19 | 원하는 대로 팁을 주세요 | **¥1.00** | Jimmy | 매우 유용한 Obs 동기화 플러그인에 감사드립니다! | | 2026/01/08 15:18:06 | 원하는 대로 팁을 주세요 | **¥1.00** | 用户 | 발견과 배포부터 사용까지 수년이 걸렸으며 이전과는 비교할 수 없을 정도로 부드럽고 매끄러운 느낌을 줍니다. 정말 훌륭해요! [강함][강함][강함] | --- *마지막 업데이트:2026/4/21 21:09:29* ================================================ FILE: docs/Support.zh-CN.json ================================================ [ { "time": "2026/03/27 00:36:52", "item": "任意打赏", "amount": "128.00", "unit": "¥", "message": "特别棒,一直在用,希望越做越好。", "name": "Geeson" }, { "time": "2026/04/18 21:15:46", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "支持一下[抱拳]", "name": "lien" }, { "time": "2026/03/29 11:20:42", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "支持!加油!", "name": "猛将兄" }, { "time": "2026/03/27 15:05:35", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "做得太棒了", "name": "Bais" }, { "time": "2026/03/24 09:02:45", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "辛苦了", "name": "cc" }, { "time": "2026/03/22 12:16:07", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "强烈支持版本迭代", "name": "cw" }, { "time": "2026/03/19 13:41:05", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "快去加班更新", "name": "背背背疼" }, { "time": "2026/03/13 17:28:40", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "非常感谢你的开源与付出,插件超实用,小小支持,继续加油!💪", "name": "一世风霜" }, { "time": "2026/03/02 09:38:26", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "非常好,开发不易,支持一下。", "name": "xuhsu" }, { "time": "2026/01/14 15:58:04", "item": "任意打赏", "amount": "88.00", "unit": "¥", "message": "能力有限,不成敬意", "name": "wutay" }, { "time": "2026/03/02 14:50:25", "item": "任意打赏", "amount": "66.00", "unit": "¥", "message": "感谢大佬!非常好用!", "name": "Patrick" }, { "time": "2026/03/01 22:56:17", "item": "任意打赏", "amount": "66.00", "unit": "¥", "message": "随喜赞叹!", "name": "xday" }, { "time": "2026/03/01 22:40:23", "item": "任意打赏", "amount": "66.00", "unit": "¥", "message": "大佬nb,插件很好用ヽ(*≧ω≦)ノ", "name": "HanHaocheng" }, { "time": "2026/02/16 21:34:33", "item": "任意打赏", "amount": "66.00", "unit": "¥", "message": "新年快乐", "name": "Jack" }, { "time": "2026/04/02 19:16:44", "item": "任意打赏", "amount": "51.55", "unit": "¥", "message": "很好的插件", "name": "小七的小洋" }, { "time": "2026/04/21 14:32:40", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "太棒了,宝藏插件啊,加油", "name": "安度" }, { "time": "2026/04/09 13:45:07", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "电脑同步成功,安卓报错 code=305,message=参数验证失败 Details=context is requ", "name": "亲 yexizhu811" }, { "time": "2026/04/08 03:18:40", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感谢作者,这一同步方式解决了多设备配置一致性的麻烦。", "name": "彼岸花" }, { "time": "2026/04/07 07:52:09", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "太棒了,很需要,感谢大佬。", "name": "tom" }, { "time": "2026/04/03 10:50:44", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "Good work", "name": "David" }, { "time": "2026/04/02 03:03:20", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "为牛逼付费!", "name": "狗带带子" }, { "time": "2026/03/27 10:15:04", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "好人一生平安", "name": "卿" }, { "time": "2026/03/18 22:57:03", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感谢telegram上的指导", "name": "灰风" }, { "time": "2026/03/15 20:09:05", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感谢🙏,插件好用", "name": "红星 RedStar" }, { "time": "2026/03/14 23:46:58", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "非常好整套架构,让我进入21世纪", "name": "fbeis" }, { "time": "2026/03/02 21:00:43", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "谢谢大佬", "name": "南科大小魏" }, { "time": "2026/02/28 13:56:18", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "太勤劳了,必须支持", "name": "xenon" }, { "time": "2026/02/24 16:37:58", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "很不错的同步方案", "name": "熙熙煦煦" }, { "time": "2026/02/16 12:14:32", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感谢作者,新年快乐!", "name": "红殇" }, { "time": "2026/02/14 18:01:55", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感谢开发这么棒的插件,解决了同步问题", "name": "Jacky龙" }, { "time": "2026/02/04 10:41:49", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "同步功能很好用,希望继续迭代完善,以笔记安全为主", "name": "咕咕咕" }, { "time": "2026/01/31 16:59:55", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "好好开发,确实解决了 Obsidian 最大的一个痛点!", "name": "vulnnull" }, { "time": "2026/01/21 09:37:19", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "谢谢你的Obsidian同步很好用👍🏻", "name": "Mojo抖音" }, { "time": "2026/01/09 16:34:10", "item": "两杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "谢谢男菩萨的 OB 插件造福世人(^🙏^),小小心意不成敬意。", "name": "喆" }, { "time": "2026/03/04 21:42:57", "item": "任意打赏", "amount": "30.00", "unit": "¥", "message": "插件很好用,感谢开发者", "name": "X" }, { "time": "2026/02/25 10:55:32", "item": "任意打赏", "amount": "30.00", "unit": "¥", "message": "大佬的项目帮了我大忙!非常感谢!希望大佬继续加油", "name": "jeanlaw" }, { "time": "2026/03/13 13:43:33", "item": "任意打赏", "amount": "29.00", "unit": "¥", "message": "好作品,加油!", "name": "rocku" }, { "time": "2026/02/24 18:36:51", "item": "任意打赏", "amount": "25.00", "unit": "¥", "message": "一直找不到理想的obsidian的同步方案,感谢作者 加油!", "name": "淇淇" }, { "time": "2026/04/21 13:45:19", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好用,加油", "name": "蓬歌" }, { "time": "2026/04/20 15:37:57", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "给大佬跪了🧎🏻‍♂️,希望后续多多更新", "name": "riding-a-colt" }, { "time": "2026/04/17 00:15:35", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "好用,支持一下", "name": "稻草人" }, { "time": "2026/04/15 13:41:41", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "谢谢大佬", "name": "hitomi" }, { "time": "2026/04/15 11:22:39", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "忘记密码如何找回", "name": "woshiug" }, { "time": "2026/04/15 06:38:14", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "试试", "name": "ke1078" }, { "time": "2026/04/12 23:54:45", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很棒的软件,感谢作者付出和开源分享。", "name": "Nikki" }, { "time": "2026/04/12 23:40:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢🙏🏻", "name": "月非明" }, { "time": "2026/04/10 23:00:36", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "棒棒哒,真好用", "name": "wdysjy" }, { "time": "2026/04/04 01:33:37", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "来杯咖啡,辛苦了", "name": "hsonghao" }, { "time": "2026/04/03 23:42:51", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "牛逼,好用", "name": "kakaa" }, { "time": "2026/04/03 13:28:02", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用,感谢", "name": "晴天小嘉" }, { "time": "2026/04/02 23:08:13", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢大佬,希望能继续坚持", "name": "畅" }, { "time": "2026/04/02 15:13:46", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "请您喝咖啡,这个项目非常有用", "name": "dove" }, { "time": "2026/03/30 23:34:12", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "加油⛽️", "name": "andie" }, { "time": "2026/03/28 12:46:23", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢大大,辛苦啦", "name": "zhengbiubiu" }, { "time": "2026/03/27 19:45:57", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "fast note sync👍", "name": "IsaacSuo" }, { "time": "2026/03/26 13:22:50", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "牛哇👍", "name": "dawn" }, { "time": "2026/03/24 14:45:21", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢,软件很方便。", "name": "Bean" }, { "time": "2026/03/22 23:08:14", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "respect", "name": "拾感" }, { "time": "2026/03/19 22:47:54", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "谢谢🙏!很赞!", "name": "jediknight" }, { "time": "2026/03/19 20:20:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢开发者,太好用了,加油!", "name": "Fcjd" }, { "time": "2026/03/17 01:17:02", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "做的太好了,大道至简,希望继续优化~", "name": "southzen" }, { "time": "2026/03/16 16:24:11", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用的喵~谢谢喵~", "name": "长筱团子" }, { "time": "2026/03/16 09:22:14", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "不成敬意^_^", "name": "barry" }, { "time": "2026/03/16 01:01:12", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢你对开源世界的贡献", "name": "Stone" }, { "time": "2026/03/14 17:15:45", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢作者,不成敬意", "name": "R M" }, { "time": "2026/03/14 00:39:54", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "大佬NB,感谢感谢", "name": "T0_欣" }, { "time": "2026/03/11 20:05:58", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢大佬做的插件,很好用很方便", "name": "Ucat" }, { "time": "2026/03/10 01:08:20", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "牛逼", "name": "la" }, { "time": "2026/03/09 09:59:44", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "好用,支持", "name": "耀/" }, { "time": "2026/03/07 23:36:38", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好的插件 支持", "name": "阿叶" }, { "time": "2026/03/06 09:58:58", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "谢谢", "name": "Dylan" }, { "time": "2026/03/03 00:53:09", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢作者日夜辛苦的写代码,并开源", "name": "Alan" }, { "time": "2026/03/01 21:10:27", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "希望能更完善,可以在云端查看ob的其他格式的文件", "name": "Jack ☑️" }, { "time": "2026/02/28 09:50:58", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "这个插件思路很好,加油", "name": "tangdh" }, { "time": "2026/02/27 12:48:50", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好的同步插件", "name": "aban" }, { "time": "2026/02/27 11:37:18", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢您的工作", "name": "三岁" }, { "time": "2026/02/27 11:19:13", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "太强了", "name": "行长" }, { "time": "2026/02/26 11:16:25", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "这么好的东西应该让更多人知道,加大宣传力度啊!", "name": "fausto" }, { "time": "2026/02/25 20:17:42", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用,感谢作者开发这么好的工具!", "name": "woloin" }, { "time": "2026/02/24 18:29:45", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢作者开发那么好用的插件,让我的obsidian旋转🥰", "name": "kimi" }, { "time": "2026/02/24 10:01:37", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "插件好用", "name": "ccsir" }, { "time": "2026/02/23 20:53:08", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "用了一段时间了,真的太棒了。", "name": "KevinYAN" }, { "time": "2026/02/23 19:32:03", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "马年快乐", "name": "繁星影月" }, { "time": "2026/02/23 12:30:55", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "谢谢辛苦了", "name": "ahto" }, { "time": "2026/02/23 06:24:55", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很有帮助,加油!", "name": "大学生" }, { "time": "2026/02/15 09:25:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "支持,加油~,很实用,强需求的功能。在AI时代大有用处", "name": "sfsun67" }, { "time": "2026/02/10 23:21:38", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感谢大佬做的同步插件,非常好用,请大佬先喝一杯咖啡,后续还会再继续打赏的", "name": "toby" }, { "time": "2026/02/10 11:41:36", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "大佬的同步插件非常棒,我会一直持续支持的", "name": "WONG" }, { "time": "2026/02/08 22:02:32", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用,感谢", "name": "小迪" }, { "time": "2026/02/06 08:42:49", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "加油💪web端的图片编辑功能整一下呗", "name": "Max" }, { "time": "2026/01/28 10:48:02", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "加油", "name": "通" }, { "time": "2026/01/26 17:21:27", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "😘", "name": "CloseCV" }, { "time": "2026/01/16 11:47:13", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好用,期待后续的开发与优化。感谢。", "name": "苏" }, { "time": "2026/01/15 14:51:11", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用感谢!", "name": "灰风" }, { "time": "2026/01/09 18:12:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "插件思路太对了", "name": "xix" }, { "time": "2026/01/03 22:44:43", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "希望越来越好👌🏻", "name": "姚朝伟" }, { "time": "2026/01/03 14:58:43", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很棒的同步方案,未来可期!非常感谢开源!加油!", "name": "roao" }, { "time": "2026/03/11 09:58:20", "item": "任意打赏", "amount": "18.00", "unit": "¥", "message": "绵薄之力,以表敬意", "name": "下鞅" }, { "time": "2026/03/02 21:15:39", "item": "任意打赏", "amount": "10.00", "unit": "¥", "message": "加油大神", "name": "路过打酱" }, { "time": "2026/02/28 12:27:51", "item": "任意打赏", "amount": "10.00", "unit": "¥", "message": "希望越来越好", "name": "白芷" }, { "time": "2026/02/27 15:54:55", "item": "任意打赏", "amount": "10.00", "unit": "¥", "message": "谢谢作者开发,感谢开源,祝越来越好。", "name": "柴特" }, { "time": "2026/02/23 15:34:53", "item": "任意打赏", "amount": "10.00", "unit": "¥", "message": "感谢,比官方同步还好用", "name": "Joe M" }, { "time": "2026/02/20 10:37:02", "item": "任意打赏", "amount": "10.00", "unit": "¥", "message": "感谢做了这么便捷的同步软件", "name": "羽山猫四叶" }, { "time": "2026/03/22 15:09:43", "item": "任意打赏", "amount": "9.90", "unit": "¥", "message": "好软件,感谢作者。", "name": "Shifuwang" }, { "time": "2026/01/28 12:03:03", "item": "任意打赏", "amount": "9.90", "unit": "¥", "message": "牛🐮", "name": "华星" }, { "time": "2026/04/09 13:51:11", "item": "任意打赏", "amount": "8.88", "unit": "¥", "message": "加油……你这个绝对有发展前途,推荐其他朋友看都觉得很不错。", "name": "散装白酒🍶" }, { "time": "2026/03/07 20:29:42", "item": "任意打赏", "amount": "8.88", "unit": "¥", "message": "感谢大佬,非常好用,点赞", "name": "皮皮" }, { "time": "2026/01/28 02:52:15", "item": "任意打赏", "amount": "8.88", "unit": "¥", "message": "感谢分享", "name": "obsidian" }, { "time": "2026/02/28 19:51:59", "item": "任意打赏", "amount": "8.00", "unit": "¥", "message": "很完善,甚至服务器界面也做得非常好看", "name": "yang" }, { "time": "2026/03/25 11:52:09", "item": "任意打赏", "amount": "6.66", "unit": "¥", "message": "装好了 无敌 哈哈哈哈哈哈", "name": "东" }, { "time": "2026/03/23 01:02:18", "item": "任意打赏", "amount": "6.66", "unit": "¥", "message": "一定要坚持开发呀!!!", "name": "wishyuwill" }, { "time": "2026/03/02 17:10:28", "item": "任意打赏", "amount": "6.66", "unit": "¥", "message": "感谢开发,插件很好用,更新很快[强]", "name": "马孔多的旅人" }, { "time": "2026/02/01 23:44:27", "item": "任意打赏", "amount": "6.66", "unit": "¥", "message": "爱你", "name": "爱你" }, { "time": "2026/01/09 22:22:25", "item": "任意打赏", "amount": "6.66", "unit": "¥", "message": "老哥写的插件很棒,继续努力吧!", "name": "kane" }, { "time": "2026/04/20 16:34:57", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "这也太好用了,折腾这么久感觉终于毕业了(╥╯﹏╰╥)ง", "name": "david" }, { "time": "2026/04/15 00:48:38", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "不易", "name": "ben" }, { "time": "2026/04/06 20:57:36", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "感谢你的项目帮助到我,obsidian 同步从此变得容易", "name": "octobersky" }, { "time": "2026/03/01 00:28:19", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "开发的太棒了", "name": "colorednoise" }, { "time": "2026/02/27 15:18:26", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "支持", "name": "wudibaolong" }, { "time": "2026/02/14 02:32:50", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "感谢开发这么好用的插件", "name": "支持开源精神" }, { "time": "2026/02/13 02:13:10", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "打赏", "name": "xxx" }, { "time": "2026/02/11 17:07:02", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "加油", "name": "Acckion" }, { "time": "2026/01/11 14:20:34", "item": "任意打赏", "amount": "5.00", "unit": "¥", "message": "很好用,希望能把 git 做出来", "name": "安宁" }, { "time": "2026/04/18 16:59:14", "item": "任意打赏", "amount": "3.66", "unit": "¥", "message": "继续开发,作出好产品", "name": "jeremy" }, { "time": "2026/02/23 17:47:46", "item": "任意打赏", "amount": "3.00", "unit": "¥", "message": "新年快乐", "name": "LL" }, { "time": "2026/04/11 12:28:18", "item": "任意打赏", "amount": "1.00", "unit": "¥", "message": "试试", "name": "ke1078" }, { "time": "2026/03/26 15:03:40", "item": "任意打赏", "amount": "1.00", "unit": "¥", "message": "感谢,十分有用", "name": "guanyingquan" }, { "time": "2026/02/24 20:15:19", "item": "任意打赏", "amount": "1.00", "unit": "¥", "message": "感恩您的 Obs 同步插件非常有帮助!", "name": "Jimmy" }, { "time": "2026/01/08 15:18:06", "item": "任意打赏", "amount": "1.00", "unit": "¥", "message": "从发现部署到使用,好多年了,从未有过的流畅丝滑的感觉,真的太好了![强][强][强]", "name": "用户" } ] ================================================ FILE: docs/Support.zh-CN.md ================================================ # 支持者名单 (Thanks to Supporters) > 非常感谢大家对本项目的支持!每一份打赏都是我持续维护和迭代的动力。 ❤️ ### 📜 致谢列表 | 收款时间 | 收款项 | 金额 | 昵称 | 留言 | | :--- | :--- | :--- | :--- | :--- | | 2026/03/27 00:36:52 | 任意打赏 | **¥128.00** | Geeson | 特别棒,一直在用,希望越做越好。 | | 2026/04/18 21:15:46 | 四杯咖啡☕ | **¥100.00** | lien | 支持一下[抱拳] | | 2026/03/29 11:20:42 | 四杯咖啡☕ | **¥100.00** | 猛将兄 | 支持!加油! | | 2026/03/27 15:05:35 | 四杯咖啡☕ | **¥100.00** | Bais | 做得太棒了 | | 2026/03/24 09:02:45 | 四杯咖啡☕ | **¥100.00** | cc | 辛苦了 | | 2026/03/22 12:16:07 | 四杯咖啡☕ | **¥100.00** | cw | 强烈支持版本迭代 | | 2026/03/19 13:41:05 | 四杯咖啡☕ | **¥100.00** | 背背背疼 | 快去加班更新 | | 2026/03/13 17:28:40 | 四杯咖啡☕ | **¥100.00** | 一世风霜 | 非常感谢你的开源与付出,插件超实用,小小支持,继续加油!💪 | | 2026/03/02 09:38:26 | 四杯咖啡☕ | **¥100.00** | xuhsu | 非常好,开发不易,支持一下。 | | 2026/01/14 15:58:04 | 任意打赏 | **¥88.00** | wutay | 能力有限,不成敬意 | | 2026/03/02 14:50:25 | 任意打赏 | **¥66.00** | Patrick | 感谢大佬!非常好用! | | 2026/03/01 22:56:17 | 任意打赏 | **¥66.00** | xday | 随喜赞叹! | | 2026/03/01 22:40:23 | 任意打赏 | **¥66.00** | HanHaocheng | 大佬nb,插件很好用ヽ(*≧ω≦)ノ | | 2026/02/16 21:34:33 | 任意打赏 | **¥66.00** | Jack | 新年快乐 | | 2026/04/02 19:16:44 | 任意打赏 | **¥51.55** | 小七的小洋 | 很好的插件 | | 2026/04/21 14:32:40 | 两杯咖啡☕ | **¥50.00** | 安度 | 太棒了,宝藏插件啊,加油 | | 2026/04/09 13:45:07 | 两杯咖啡☕ | **¥50.00** | 亲 yexizhu811 | 电脑同步成功,安卓报错 code=305,message=参数验证失败 Details=context is requ | | 2026/04/08 03:18:40 | 两杯咖啡☕ | **¥50.00** | 彼岸花 | 感谢作者,这一同步方式解决了多设备配置一致性的麻烦。 | | 2026/04/07 07:52:09 | 两杯咖啡☕ | **¥50.00** | tom | 太棒了,很需要,感谢大佬。 | | 2026/04/03 10:50:44 | 两杯咖啡☕ | **¥50.00** | David | Good work | | 2026/04/02 03:03:20 | 两杯咖啡☕ | **¥50.00** | 狗带带子 | 为牛逼付费! | | 2026/03/27 10:15:04 | 两杯咖啡☕ | **¥50.00** | 卿 | 好人一生平安 | | 2026/03/18 22:57:03 | 两杯咖啡☕ | **¥50.00** | 灰风 | 感谢telegram上的指导 | | 2026/03/15 20:09:05 | 两杯咖啡☕ | **¥50.00** | 红星 RedStar | 感谢🙏,插件好用 | | 2026/03/14 23:46:58 | 两杯咖啡☕ | **¥50.00** | fbeis | 非常好整套架构,让我进入21世纪 | | 2026/03/02 21:00:43 | 两杯咖啡☕ | **¥50.00** | 南科大小魏 | 谢谢大佬 | | 2026/02/28 13:56:18 | 两杯咖啡☕ | **¥50.00** | xenon | 太勤劳了,必须支持 | | 2026/02/24 16:37:58 | 两杯咖啡☕ | **¥50.00** | 熙熙煦煦 | 很不错的同步方案 | | 2026/02/16 12:14:32 | 两杯咖啡☕ | **¥50.00** | 红殇 | 感谢作者,新年快乐! | | 2026/02/14 18:01:55 | 两杯咖啡☕ | **¥50.00** | Jacky龙 | 感谢开发这么棒的插件,解决了同步问题 | | 2026/02/04 10:41:49 | 两杯咖啡☕ | **¥50.00** | 咕咕咕 | 同步功能很好用,希望继续迭代完善,以笔记安全为主 | | 2026/01/31 16:59:55 | 两杯咖啡☕ | **¥50.00** | vulnnull | 好好开发,确实解决了 Obsidian 最大的一个痛点! | | 2026/01/21 09:37:19 | 两杯咖啡☕ | **¥50.00** | Mojo抖音 | 谢谢你的Obsidian同步很好用👍🏻 | | 2026/01/09 16:34:10 | 两杯咖啡☕ | **¥50.00** | 喆 | 谢谢男菩萨的 OB 插件造福世人(^🙏^),小小心意不成敬意。 | | 2026/03/04 21:42:57 | 任意打赏 | **¥30.00** | X | 插件很好用,感谢开发者 | | 2026/02/25 10:55:32 | 任意打赏 | **¥30.00** | jeanlaw | 大佬的项目帮了我大忙!非常感谢!希望大佬继续加油 | | 2026/03/13 13:43:33 | 任意打赏 | **¥29.00** | rocku | 好作品,加油! | | 2026/02/24 18:36:51 | 任意打赏 | **¥25.00** | 淇淇 | 一直找不到理想的obsidian的同步方案,感谢作者 加油! | | 2026/04/21 13:45:19 | 一杯咖啡☕ | **¥20.00** | 蓬歌 | 很好用,加油 | | 2026/04/20 15:37:57 | 一杯咖啡☕ | **¥20.00** | riding-a-colt | 给大佬跪了🧎🏻‍♂️,希望后续多多更新 | | 2026/04/17 00:15:35 | 一杯咖啡☕ | **¥20.00** | 稻草人 | 好用,支持一下 | | 2026/04/15 13:41:41 | 一杯咖啡☕ | **¥20.00** | hitomi | 谢谢大佬 | | 2026/04/15 11:22:39 | 一杯咖啡☕ | **¥20.00** | woshiug | 忘记密码如何找回 | | 2026/04/15 06:38:14 | 一杯咖啡☕ | **¥20.00** | ke1078 | 试试 | | 2026/04/12 23:54:45 | 一杯咖啡☕ | **¥20.00** | Nikki | 很棒的软件,感谢作者付出和开源分享。 | | 2026/04/12 23:40:17 | 一杯咖啡☕ | **¥20.00** | 月非明 | 感谢🙏🏻 | | 2026/04/10 23:00:36 | 一杯咖啡☕ | **¥20.00** | wdysjy | 棒棒哒,真好用 | | 2026/04/04 01:33:37 | 一杯咖啡☕ | **¥20.00** | hsonghao | 来杯咖啡,辛苦了 | | 2026/04/03 23:42:51 | 一杯咖啡☕ | **¥20.00** | kakaa | 牛逼,好用 | | 2026/04/03 13:28:02 | 一杯咖啡☕ | **¥20.00** | 晴天小嘉 | 非常好用,感谢 | | 2026/04/02 23:08:13 | 一杯咖啡☕ | **¥20.00** | 畅 | 感谢大佬,希望能继续坚持 | | 2026/04/02 15:13:46 | 一杯咖啡☕ | **¥20.00** | dove | 请您喝咖啡,这个项目非常有用 | | 2026/03/30 23:34:12 | 一杯咖啡☕ | **¥20.00** | andie | 加油⛽️ | | 2026/03/28 12:46:23 | 一杯咖啡☕ | **¥20.00** | zhengbiubiu | 感谢大大,辛苦啦 | | 2026/03/27 19:45:57 | 一杯咖啡☕ | **¥20.00** | IsaacSuo | fast note sync👍 | | 2026/03/26 13:22:50 | 一杯咖啡☕ | **¥20.00** | dawn | 牛哇👍 | | 2026/03/24 14:45:21 | 一杯咖啡☕ | **¥20.00** | Bean | 感谢,软件很方便。 | | 2026/03/22 23:08:14 | 一杯咖啡☕ | **¥20.00** | 拾感 | respect | | 2026/03/19 22:47:54 | 一杯咖啡☕ | **¥20.00** | jediknight | 谢谢🙏!很赞! | | 2026/03/19 20:20:17 | 一杯咖啡☕ | **¥20.00** | Fcjd | 感谢开发者,太好用了,加油! | | 2026/03/17 01:17:02 | 一杯咖啡☕ | **¥20.00** | southzen | 做的太好了,大道至简,希望继续优化~ | | 2026/03/16 16:24:11 | 一杯咖啡☕ | **¥20.00** | 长筱团子 | 非常好用的喵~谢谢喵~ | | 2026/03/16 09:22:14 | 一杯咖啡☕ | **¥20.00** | barry | 不成敬意^_^ | | 2026/03/16 01:01:12 | 一杯咖啡☕ | **¥20.00** | Stone | 感谢你对开源世界的贡献 | | 2026/03/14 17:15:45 | 一杯咖啡☕ | **¥20.00** | R M | 感谢作者,不成敬意 | | 2026/03/14 00:39:54 | 一杯咖啡☕ | **¥20.00** | T0_欣 | 大佬NB,感谢感谢 | | 2026/03/11 20:05:58 | 一杯咖啡☕ | **¥20.00** | Ucat | 感谢大佬做的插件,很好用很方便 | | 2026/03/10 01:08:20 | 一杯咖啡☕ | **¥20.00** | la | 牛逼 | | 2026/03/09 09:59:44 | 一杯咖啡☕ | **¥20.00** | 耀/ | 好用,支持 | | 2026/03/07 23:36:38 | 一杯咖啡☕ | **¥20.00** | 阿叶 | 很好的插件 支持 | | 2026/03/06 09:58:58 | 一杯咖啡☕ | **¥20.00** | Dylan | 谢谢 | | 2026/03/03 00:53:09 | 一杯咖啡☕ | **¥20.00** | Alan | 感谢作者日夜辛苦的写代码,并开源 | | 2026/03/01 21:10:27 | 一杯咖啡☕ | **¥20.00** | Jack ☑️ | 希望能更完善,可以在云端查看ob的其他格式的文件 | | 2026/02/28 09:50:58 | 一杯咖啡☕ | **¥20.00** | tangdh | 这个插件思路很好,加油 | | 2026/02/27 12:48:50 | 一杯咖啡☕ | **¥20.00** | aban | 很好的同步插件 | | 2026/02/27 11:37:18 | 一杯咖啡☕ | **¥20.00** | 三岁 | 感谢您的工作 | | 2026/02/27 11:19:13 | 一杯咖啡☕ | **¥20.00** | 行长 | 太强了 | | 2026/02/26 11:16:25 | 一杯咖啡☕ | **¥20.00** | fausto | 这么好的东西应该让更多人知道,加大宣传力度啊! | | 2026/02/25 20:17:42 | 一杯咖啡☕ | **¥20.00** | woloin | 非常好用,感谢作者开发这么好的工具! | | 2026/02/24 18:29:45 | 一杯咖啡☕ | **¥20.00** | kimi | 感谢作者开发那么好用的插件,让我的obsidian旋转🥰 | | 2026/02/24 10:01:37 | 一杯咖啡☕ | **¥20.00** | ccsir | 插件好用 | | 2026/02/23 20:53:08 | 一杯咖啡☕ | **¥20.00** | KevinYAN | 用了一段时间了,真的太棒了。 | | 2026/02/23 19:32:03 | 一杯咖啡☕ | **¥20.00** | 繁星影月 | 马年快乐 | | 2026/02/23 12:30:55 | 一杯咖啡☕ | **¥20.00** | ahto | 谢谢辛苦了 | | 2026/02/23 06:24:55 | 一杯咖啡☕ | **¥20.00** | 大学生 | 很有帮助,加油! | | 2026/02/15 09:25:17 | 一杯咖啡☕ | **¥20.00** | sfsun67 | 支持,加油~,很实用,强需求的功能。在AI时代大有用处 | | 2026/02/10 23:21:38 | 一杯咖啡☕ | **¥20.00** | toby | 感谢大佬做的同步插件,非常好用,请大佬先喝一杯咖啡,后续还会再继续打赏的 | | 2026/02/10 11:41:36 | 一杯咖啡☕ | **¥20.00** | WONG | 大佬的同步插件非常棒,我会一直持续支持的 | | 2026/02/08 22:02:32 | 一杯咖啡☕ | **¥20.00** | 小迪 | 非常好用,感谢 | | 2026/02/06 08:42:49 | 一杯咖啡☕ | **¥20.00** | Max | 加油💪web端的图片编辑功能整一下呗 | | 2026/01/28 10:48:02 | 一杯咖啡☕ | **¥20.00** | 通 | 加油 | | 2026/01/26 17:21:27 | 一杯咖啡☕ | **¥20.00** | CloseCV | 😘 | | 2026/01/16 11:47:13 | 一杯咖啡☕ | **¥20.00** | 苏 | 很好用,期待后续的开发与优化。感谢。 | | 2026/01/15 14:51:11 | 一杯咖啡☕ | **¥20.00** | 灰风 | 非常好用感谢! | | 2026/01/09 18:12:17 | 一杯咖啡☕ | **¥20.00** | xix | 插件思路太对了 | | 2026/01/03 22:44:43 | 一杯咖啡☕ | **¥20.00** | 姚朝伟 | 希望越来越好👌🏻 | | 2026/01/03 14:58:43 | 一杯咖啡☕ | **¥20.00** | roao | 很棒的同步方案,未来可期!非常感谢开源!加油! | | 2026/03/11 09:58:20 | 任意打赏 | **¥18.00** | 下鞅 | 绵薄之力,以表敬意 | | 2026/03/02 21:15:39 | 任意打赏 | **¥10.00** | 路过打酱 | 加油大神 | | 2026/02/28 12:27:51 | 任意打赏 | **¥10.00** | 白芷 | 希望越来越好 | | 2026/02/27 15:54:55 | 任意打赏 | **¥10.00** | 柴特 | 谢谢作者开发,感谢开源,祝越来越好。 | | 2026/02/23 15:34:53 | 任意打赏 | **¥10.00** | Joe M | 感谢,比官方同步还好用 | | 2026/02/20 10:37:02 | 任意打赏 | **¥10.00** | 羽山猫四叶 | 感谢做了这么便捷的同步软件 | | 2026/03/22 15:09:43 | 任意打赏 | **¥9.90** | Shifuwang | 好软件,感谢作者。 | | 2026/01/28 12:03:03 | 任意打赏 | **¥9.90** | 华星 | 牛🐮 | | 2026/04/09 13:51:11 | 任意打赏 | **¥8.88** | 散装白酒🍶 | 加油……你这个绝对有发展前途,推荐其他朋友看都觉得很不错。 | | 2026/03/07 20:29:42 | 任意打赏 | **¥8.88** | 皮皮 | 感谢大佬,非常好用,点赞 | | 2026/01/28 02:52:15 | 任意打赏 | **¥8.88** | obsidian | 感谢分享 | | 2026/02/28 19:51:59 | 任意打赏 | **¥8.00** | yang | 很完善,甚至服务器界面也做得非常好看 | | 2026/03/25 11:52:09 | 任意打赏 | **¥6.66** | 东 | 装好了 无敌 哈哈哈哈哈哈 | | 2026/03/23 01:02:18 | 任意打赏 | **¥6.66** | wishyuwill | 一定要坚持开发呀!!! | | 2026/03/02 17:10:28 | 任意打赏 | **¥6.66** | 马孔多的旅人 | 感谢开发,插件很好用,更新很快[强] | | 2026/02/01 23:44:27 | 任意打赏 | **¥6.66** | 爱你 | 爱你 | | 2026/01/09 22:22:25 | 任意打赏 | **¥6.66** | kane | 老哥写的插件很棒,继续努力吧! | | 2026/04/20 16:34:57 | 任意打赏 | **¥5.00** | david | 这也太好用了,折腾这么久感觉终于毕业了(╥╯﹏╰╥)ง | | 2026/04/15 00:48:38 | 任意打赏 | **¥5.00** | ben | 不易 | | 2026/04/06 20:57:36 | 任意打赏 | **¥5.00** | octobersky | 感谢你的项目帮助到我,obsidian 同步从此变得容易 | | 2026/03/01 00:28:19 | 任意打赏 | **¥5.00** | colorednoise | 开发的太棒了 | | 2026/02/27 15:18:26 | 任意打赏 | **¥5.00** | wudibaolong | 支持 | | 2026/02/14 02:32:50 | 任意打赏 | **¥5.00** | 支持开源精神 | 感谢开发这么好用的插件 | | 2026/02/13 02:13:10 | 任意打赏 | **¥5.00** | xxx | 打赏 | | 2026/02/11 17:07:02 | 任意打赏 | **¥5.00** | Acckion | 加油 | | 2026/01/11 14:20:34 | 任意打赏 | **¥5.00** | 安宁 | 很好用,希望能把 git 做出来 | | 2026/04/18 16:59:14 | 任意打赏 | **¥3.66** | jeremy | 继续开发,作出好产品 | | 2026/02/23 17:47:46 | 任意打赏 | **¥3.00** | LL | 新年快乐 | | 2026/04/11 12:28:18 | 任意打赏 | **¥1.00** | ke1078 | 试试 | | 2026/03/26 15:03:40 | 任意打赏 | **¥1.00** | guanyingquan | 感谢,十分有用 | | 2026/02/24 20:15:19 | 任意打赏 | **¥1.00** | Jimmy | 感恩您的 Obs 同步插件非常有帮助! | | 2026/01/08 15:18:06 | 任意打赏 | **¥1.00** | 用户 | 从发现部署到使用,好多年了,从未有过的流畅丝滑的感觉,真的太好了![强][强][强] | --- *本数据最后更新于:2026/4/21 21:03:09* ================================================ FILE: docs/Support.zh-TW.json ================================================ [ { "time": "2026/03/27 00:36:52", "item": "任一打賞", "amount": "128.00", "unit": "¥", "message": "特別棒,一直在用,希望越做越好。", "name": "Geeson" }, { "time": "2026/04/18 21:15:46", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "支持一下[抱拳]", "name": "lien" }, { "time": "2026/03/29 11:20:42", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "支持!加油!", "name": "猛将兄" }, { "time": "2026/03/27 15:05:35", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "做得太棒了", "name": "Bais" }, { "time": "2026/03/24 09:02:45", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "辛苦了", "name": "cc" }, { "time": "2026/03/22 12:16:07", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "強烈支持版本迭代", "name": "cw" }, { "time": "2026/03/19 13:41:05", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "快去加班更新", "name": "背背背疼" }, { "time": "2026/03/13 17:28:40", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "非常感謝你的開源與付出,插件超實用,小小支持,繼續加油! 💪", "name": "一世风霜" }, { "time": "2026/03/02 09:38:26", "item": "四杯咖啡☕", "amount": "100.00", "unit": "¥", "message": "非常好,開發不易,支援一下。", "name": "xuhsu" }, { "time": "2026/01/14 15:58:04", "item": "任一打賞", "amount": "88.00", "unit": "¥", "message": "能力有限,不成敬意", "name": "wutay" }, { "time": "2026/03/02 14:50:25", "item": "任一打賞", "amount": "66.00", "unit": "¥", "message": "感謝大佬!非常好用!", "name": "Patrick" }, { "time": "2026/03/01 22:56:17", "item": "任一打賞", "amount": "66.00", "unit": "¥", "message": "隨喜讚歎!", "name": "xday" }, { "time": "2026/03/01 22:40:23", "item": "任一打賞", "amount": "66.00", "unit": "¥", "message": "大佬nb,插件很好用ヽ(*≧ω≦)ノ", "name": "HanHaocheng" }, { "time": "2026/02/16 21:34:33", "item": "任一打賞", "amount": "66.00", "unit": "¥", "message": "新年快樂", "name": "Jack" }, { "time": "2026/04/02 19:16:44", "item": "任一打賞", "amount": "51.55", "unit": "¥", "message": "很好的插件", "name": "小七的小洋" }, { "time": "2026/04/21 14:32:40", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "太棒了,寶藏插件啊,加油", "name": "安度" }, { "time": "2026/04/09 13:45:07", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "電腦同步成功,安卓報錯 code=305,message=參數驗證失敗 Details=context is requ", "name": "亲 yexizhu811" }, { "time": "2026/04/08 03:18:40", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感謝作者,這同步方式解決了多設備配置一致性的麻煩。", "name": "彼岸花" }, { "time": "2026/04/07 07:52:09", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "太棒了,很需要,感謝大佬。", "name": "tom" }, { "time": "2026/04/03 10:50:44", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "幹得好", "name": "David" }, { "time": "2026/04/02 03:03:20", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "為牛逼付費!", "name": "狗带带子" }, { "time": "2026/03/27 10:15:04", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "好人一生平安", "name": "卿" }, { "time": "2026/03/18 22:57:03", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感謝telegram上的指導", "name": "灰风" }, { "time": "2026/03/15 20:09:05", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感謝🙏,插件好用", "name": "红星 RedStar" }, { "time": "2026/03/14 23:46:58", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "非常好整套架構,讓我進入21世紀", "name": "fbeis" }, { "time": "2026/03/02 21:00:43", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "謝謝大佬", "name": "南科大小魏" }, { "time": "2026/02/28 13:56:18", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "太勤勞了,必須支持", "name": "xenon" }, { "time": "2026/02/24 16:37:58", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "很不錯的同步方案", "name": "熙熙煦煦" }, { "time": "2026/02/16 12:14:32", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感謝作者,新年快樂!", "name": "红殇" }, { "time": "2026/02/14 18:01:55", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "感謝開發這麼棒的插件,解決了同步問題", "name": "Jacky龙" }, { "time": "2026/02/04 10:41:49", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "同步功能很好用,希望繼續迭代完善,以筆記安全為主", "name": "咕咕咕" }, { "time": "2026/01/31 16:59:55", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "好好開發,確實解決了 Obsidian 最大的一個痛點!", "name": "vulnnull" }, { "time": "2026/01/21 09:37:19", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "謝謝你的Obsidian同步很好用👍🏻", "name": "Mojo抖音" }, { "time": "2026/01/09 16:34:10", "item": "兩杯咖啡☕", "amount": "50.00", "unit": "¥", "message": "謝謝男菩薩的 OB 插件造福世人(^🙏^),小小心意不成敬意。", "name": "喆" }, { "time": "2026/03/04 21:42:57", "item": "任一打賞", "amount": "30.00", "unit": "¥", "message": "插件很好用,感謝開發者", "name": "X" }, { "time": "2026/02/25 10:55:32", "item": "任一打賞", "amount": "30.00", "unit": "¥", "message": "大佬的專案幫了我大忙!非常感謝!希望大佬繼續加油", "name": "jeanlaw" }, { "time": "2026/03/13 13:43:33", "item": "任一打賞", "amount": "29.00", "unit": "¥", "message": "好作品,加油!", "name": "rocku" }, { "time": "2026/02/24 18:36:51", "item": "任一打賞", "amount": "25.00", "unit": "¥", "message": "一直找不到理想的obsidian的同步方案,感謝作者 加油!", "name": "淇淇" }, { "time": "2026/04/21 13:45:19", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好用,加油", "name": "蓬歌" }, { "time": "2026/04/20 15:37:57", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "給大佬跪了🧎🏻‍♂️,希望後續多多更新", "name": "riding-a-colt" }, { "time": "2026/04/17 00:15:35", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "好用,支持一下", "name": "稻草人" }, { "time": "2026/04/15 13:41:41", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "謝謝大佬", "name": "hitomi" }, { "time": "2026/04/15 11:22:39", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "忘記密碼如何找回", "name": "woshiug" }, { "time": "2026/04/15 06:38:14", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "試試", "name": "ke1078" }, { "time": "2026/04/12 23:54:45", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很棒的軟體,感謝作者付出和開源分享。", "name": "Nikki" }, { "time": "2026/04/12 23:40:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝🙏🏻", "name": "月非明" }, { "time": "2026/04/10 23:00:36", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "棒棒噠,真好用", "name": "wdysjy" }, { "time": "2026/04/04 01:33:37", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "來杯咖啡,辛苦了", "name": "hsonghao" }, { "time": "2026/04/03 23:42:51", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "牛逼,好用", "name": "kakaa" }, { "time": "2026/04/03 13:28:02", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用,感謝", "name": "晴天小嘉" }, { "time": "2026/04/02 23:08:13", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝大佬,希望能持續堅持", "name": "畅" }, { "time": "2026/04/02 15:13:46", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "請您喝咖啡,這個項目非常有用", "name": "dove" }, { "time": "2026/03/30 23:34:12", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "加油⛽️", "name": "andie" }, { "time": "2026/03/28 12:46:23", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝大,辛苦啦", "name": "zhengbiubiu" }, { "time": "2026/03/27 19:45:57", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "快速筆記同步👍", "name": "IsaacSuo" }, { "time": "2026/03/26 13:22:50", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "牛哇👍", "name": "dawn" }, { "time": "2026/03/24 14:45:21", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝,軟體很方便。", "name": "Bean" }, { "time": "2026/03/22 23:08:14", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "尊重", "name": "拾感" }, { "time": "2026/03/19 22:47:54", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "謝謝🙏!很讚!", "name": "jediknight" }, { "time": "2026/03/19 20:20:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝開發者,太好用了,加油!", "name": "Fcjd" }, { "time": "2026/03/17 01:17:02", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "做的太好了,大道至簡,希望繼續優化~", "name": "southzen" }, { "time": "2026/03/16 16:24:11", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用的喵~謝謝喵~", "name": "长筱团子" }, { "time": "2026/03/16 09:22:14", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "不成敬^_^", "name": "barry" }, { "time": "2026/03/16 01:01:12", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝你對開源世界的貢獻", "name": "Stone" }, { "time": "2026/03/14 17:15:45", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝作者,不成敬意", "name": "R M" }, { "time": "2026/03/14 00:39:54", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "大佬NB,感謝感謝", "name": "T0_欣" }, { "time": "2026/03/11 20:05:58", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝大佬做的插件,很好用很方便", "name": "Ucat" }, { "time": "2026/03/10 01:08:20", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "牛逼", "name": "la" }, { "time": "2026/03/09 09:59:44", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "好用,支持", "name": "耀/" }, { "time": "2026/03/07 23:36:38", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好的插件 支持", "name": "阿叶" }, { "time": "2026/03/06 09:58:58", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "謝謝", "name": "Dylan" }, { "time": "2026/03/03 00:53:09", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝作者日夜辛苦的寫程式碼,並開源", "name": "Alan" }, { "time": "2026/03/01 21:10:27", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "希望能更完善,可以在雲端查看ob的其他格式的文件", "name": "Jack ☑️" }, { "time": "2026/02/28 09:50:58", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "這個插件思路很好,加油", "name": "tangdh" }, { "time": "2026/02/27 12:48:50", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好的同步插件", "name": "aban" }, { "time": "2026/02/27 11:37:18", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝您的工作", "name": "三岁" }, { "time": "2026/02/27 11:19:13", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "太強了", "name": "行长" }, { "time": "2026/02/26 11:16:25", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "這麼好的東西應該​​會讓更多人知道,加大宣傳力道啊!", "name": "fausto" }, { "time": "2026/02/25 20:17:42", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用,感謝作者開發這麼好的工具!", "name": "woloin" }, { "time": "2026/02/24 18:29:45", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝作者開發那麼好用的插件,讓我的obsidian旋轉🥰", "name": "kimi" }, { "time": "2026/02/24 10:01:37", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "插件好用", "name": "ccsir" }, { "time": "2026/02/23 20:53:08", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "用了一段時間了,真的太棒了。", "name": "KevinYAN" }, { "time": "2026/02/23 19:32:03", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "馬年快樂", "name": "繁星影月" }, { "time": "2026/02/23 12:30:55", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "謝謝辛苦了", "name": "ahto" }, { "time": "2026/02/23 06:24:55", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很有幫助,加油!", "name": "大学生" }, { "time": "2026/02/15 09:25:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "支持,加油~,很實用,強需求的功能。在AI時代大有用處", "name": "sfsun67" }, { "time": "2026/02/10 23:21:38", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "感謝大佬做的同步插件,非常好用,請大佬先喝一杯咖啡,後續還會再繼續打賞的", "name": "toby" }, { "time": "2026/02/10 11:41:36", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "大佬的同步插件非常棒,我會一直持續支持的", "name": "WONG" }, { "time": "2026/02/08 22:02:32", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用,感謝", "name": "小迪" }, { "time": "2026/02/06 08:42:49", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "加油💪web端的圖片編輯功能整一下唄", "name": "Max" }, { "time": "2026/01/28 10:48:02", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "加油", "name": "通" }, { "time": "2026/01/26 17:21:27", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "-", "name": "CloseCV" }, { "time": "2026/01/16 11:47:13", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很好用,期待後續的開發與優化。感謝。", "name": "苏" }, { "time": "2026/01/15 14:51:11", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "非常好用感謝!", "name": "灰风" }, { "time": "2026/01/09 18:12:17", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "插件思路太對了", "name": "xix" }, { "time": "2026/01/03 22:44:43", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "希望越來越好👌🏻", "name": "姚朝伟" }, { "time": "2026/01/03 14:58:43", "item": "一杯咖啡☕", "amount": "20.00", "unit": "¥", "message": "很棒的同步方案,未來可期!非常感謝開源!加油!", "name": "roao" }, { "time": "2026/03/11 09:58:20", "item": "任一打賞", "amount": "18.00", "unit": "¥", "message": "綿薄之力,以表敬意", "name": "下鞅" }, { "time": "2026/03/02 21:15:39", "item": "任一打賞", "amount": "10.00", "unit": "¥", "message": "加油大神", "name": "路过打酱" }, { "time": "2026/02/28 12:27:51", "item": "任一打賞", "amount": "10.00", "unit": "¥", "message": "希望越來越好", "name": "白芷" }, { "time": "2026/02/27 15:54:55", "item": "任一打賞", "amount": "10.00", "unit": "¥", "message": "謝謝作者開發,謝謝開源,祝越來越好。", "name": "柴特" }, { "time": "2026/02/23 15:34:53", "item": "任一打賞", "amount": "10.00", "unit": "¥", "message": "感謝,比官方同步還好用", "name": "Joe M" }, { "time": "2026/02/20 10:37:02", "item": "任一打賞", "amount": "10.00", "unit": "¥", "message": "感謝做了這麼便捷的同步軟體", "name": "羽山猫四叶" }, { "time": "2026/03/22 15:09:43", "item": "任一打賞", "amount": "9.90", "unit": "¥", "message": "好軟體,感謝作者。", "name": "Shifuwang" }, { "time": "2026/01/28 12:03:03", "item": "任一打賞", "amount": "9.90", "unit": "¥", "message": "牛🐮", "name": "华星" }, { "time": "2026/04/09 13:51:11", "item": "任一打賞", "amount": "8.88", "unit": "¥", "message": "加油……你這個絕對有發展前途,推薦其他朋友看都覺得很不錯。", "name": "散装白酒🍶" }, { "time": "2026/03/07 20:29:42", "item": "任一打賞", "amount": "8.88", "unit": "¥", "message": "感謝大佬,非常好用,按讚", "name": "皮皮" }, { "time": "2026/01/28 02:52:15", "item": "任一打賞", "amount": "8.88", "unit": "¥", "message": "感謝分享", "name": "obsidian" }, { "time": "2026/02/28 19:51:59", "item": "任一打賞", "amount": "8.00", "unit": "¥", "message": "很完善,連伺服器介面也做得非常好看", "name": "yang" }, { "time": "2026/03/25 11:52:09", "item": "任一打賞", "amount": "6.66", "unit": "¥", "message": "裝好了 無敵 哈哈哈哈哈哈", "name": "东" }, { "time": "2026/03/23 01:02:18", "item": "任一打賞", "amount": "6.66", "unit": "¥", "message": "一定要堅持開發呀! ! !", "name": "wishyuwill" }, { "time": "2026/03/02 17:10:28", "item": "任一打賞", "amount": "6.66", "unit": "¥", "message": "感謝開發,插件很好用,更新很快[強]", "name": "马孔多的旅人" }, { "time": "2026/02/01 23:44:27", "item": "任一打賞", "amount": "6.66", "unit": "¥", "message": "愛你", "name": "爱你" }, { "time": "2026/01/09 22:22:25", "item": "任一打賞", "amount": "6.66", "unit": "¥", "message": "老哥寫的插件很棒,繼續努力!", "name": "kane" }, { "time": "2026/04/20 16:34:57", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "這也太好用了,折騰這麼久感覺終於畢業了(╥╯﹏╰╥)ง", "name": "david" }, { "time": "2026/04/15 00:48:38", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "不易", "name": "ben" }, { "time": "2026/04/06 20:57:36", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "感謝你的專案幫助到我,obsidian 同步從此變得容易", "name": "octobersky" }, { "time": "2026/03/01 00:28:19", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "開發的太棒了", "name": "colorednoise" }, { "time": "2026/02/27 15:18:26", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "支援", "name": "wudibaolong" }, { "time": "2026/02/14 02:32:50", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "感謝開發這麼好用的插件", "name": "支持开源精神" }, { "time": "2026/02/13 02:13:10", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "打賞", "name": "xxx" }, { "time": "2026/02/11 17:07:02", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "加油", "name": "Acckion" }, { "time": "2026/01/11 14:20:34", "item": "任一打賞", "amount": "5.00", "unit": "¥", "message": "很好用,希望能把 git 做出來", "name": "安宁" }, { "time": "2026/04/18 16:59:14", "item": "任一打賞", "amount": "3.66", "unit": "¥", "message": "繼續開發,作出好產品", "name": "jeremy" }, { "time": "2026/02/23 17:47:46", "item": "任一打賞", "amount": "3.00", "unit": "¥", "message": "新年快樂", "name": "LL" }, { "time": "2026/04/11 12:28:18", "item": "任一打賞", "amount": "1.00", "unit": "¥", "message": "試試", "name": "ke1078" }, { "time": "2026/03/26 15:03:40", "item": "任一打賞", "amount": "1.00", "unit": "¥", "message": "感謝,十分有用", "name": "guanyingquan" }, { "time": "2026/02/24 20:15:19", "item": "任一打賞", "amount": "1.00", "unit": "¥", "message": "感恩您的 Obs 同步外掛非常有幫助!", "name": "Jimmy" }, { "time": "2026/01/08 15:18:06", "item": "任一打賞", "amount": "1.00", "unit": "¥", "message": "從發現部署到使用,好多年了,從未有過的流暢絲滑的感覺,真的太好了! [強][強][強]", "name": "用户" } ] ================================================ FILE: docs/Support.zh-TW.md ================================================ # 支持者名單 (Thanks to Supporters) > 非常感謝大家對本項目的支持!每一份打賞都是我持續維護和迭代的動力。 ❤️ ### 📜 致謝列表 | 收款時間 | 收款項 | 金額 | 昵稱 | 留言 | 備註 | | :--- | :--- | :--- | :--- | :--- | :--- | | | | **¥128.00** | | 特別棒,一直在用,希望越做越好。 | | | | | **¥100.00** | | 支持一下[抱拳] | | | | | **¥100.00** | | 支持!加油! | | | | | **¥100.00** | | 做得太棒了 | | | | | **¥100.00** | | 辛苦了 | | | | | **¥100.00** | | 強烈支持版本迭代 | | | | | **¥100.00** | | 快去加班更新 | | | | | **¥100.00** | | 非常感謝你的開源與付出,插件超實用,小小支持,繼續加油! 💪 | | | | | **¥100.00** | | 非常好,開發不易,支援一下。 | | | | | **¥88.00** | | 能力有限,不成敬意 | | | | | **¥66.00** | | 感謝大佬!非常好用! | | | | | **¥66.00** | | 隨喜讚歎! | | | | | **¥66.00** | | 大佬nb,插件很好用ヽ(*≧ω≦)ノ | | | | | **¥66.00** | | 新年快樂 | | | | | **¥51.55** | | 很好的插件 | | | | | **¥50.00** | | 太棒了,寶藏插件啊,加油 | | | | | **¥50.00** | | 電腦同步成功,安卓報錯 code=305,message=參數驗證失敗 Details=context is requ | | | | | **¥50.00** | | 感謝作者,這同步方式解決了多設備配置一致性的麻煩。 | | | | | **¥50.00** | | 太棒了,很需要,感謝大佬。 | | | | | **¥50.00** | | 幹得好 | | | | | **¥50.00** | | 為牛逼付費! | | | | | **¥50.00** | | 好人一生平安 | | | | | **¥50.00** | | 感謝telegram上的指導 | | | | | **¥50.00** | | 感謝🙏,插件好用 | | | | | **¥50.00** | | 非常好整套架構,讓我進入21世紀 | | | | | **¥50.00** | | 謝謝大佬 | | | | | **¥50.00** | | 太勤勞了,必須支持 | | | | | **¥50.00** | | 很不錯的同步方案 | | | | | **¥50.00** | | 感謝作者,新年快樂! | | | | | **¥50.00** | | 感謝開發這麼棒的插件,解決了同步問題 | | | | | **¥50.00** | | 同步功能很好用,希望繼續迭代完善,以筆記安全為主 | | | | | **¥50.00** | | 好好開發,確實解決了 Obsidian 最大的一個痛點! | | | | | **¥50.00** | | 謝謝你的Obsidian同步很好用👍🏻 | | | | | **¥50.00** | | 謝謝男菩薩的 OB 插件造福世人(^🙏^),小小心意不成敬意。 | | | | | **¥30.00** | | 插件很好用,感謝開發者 | | | | | **¥30.00** | | 大佬的專案幫了我大忙!非常感謝!希望大佬繼續加油 | | | | | **¥29.00** | | 好作品,加油! | | | | | **¥25.00** | | 一直找不到理想的obsidian的同步方案,感謝作者 加油! | | | | | **¥20.00** | | 很好用,加油 | | | | | **¥20.00** | | 給大佬跪了🧎🏻‍♂️,希望後續多多更新 | | | | | **¥20.00** | | 好用,支持一下 | | | | | **¥20.00** | | 謝謝大佬 | | | | | **¥20.00** | | 忘記密碼如何找回 | | | | | **¥20.00** | | 試試 | | | | | **¥20.00** | | 很棒的軟體,感謝作者付出和開源分享。 | | | | | **¥20.00** | | 感謝🙏🏻 | | | | | **¥20.00** | | 棒棒噠,真好用 | | | | | **¥20.00** | | 來杯咖啡,辛苦了 | | | | | **¥20.00** | | 牛逼,好用 | | | | | **¥20.00** | | 非常好用,感謝 | | | | | **¥20.00** | | 感謝大佬,希望能持續堅持 | | | | | **¥20.00** | | 請您喝咖啡,這個項目非常有用 | | | | | **¥20.00** | | 加油⛽️ | | | | | **¥20.00** | | 感謝大,辛苦啦 | | | | | **¥20.00** | | 快速筆記同步👍 | | | | | **¥20.00** | | 牛哇👍 | | | | | **¥20.00** | | 感謝,軟體很方便。 | | | | | **¥20.00** | | 尊重 | | | | | **¥20.00** | | 謝謝🙏!很讚! | | | | | **¥20.00** | | 感謝開發者,太好用了,加油! | | | | | **¥20.00** | | 做的太好了,大道至簡,希望繼續優化~ | | | | | **¥20.00** | | 非常好用的喵~謝謝喵~ | | | | | **¥20.00** | | 不成敬^_^ | | | | | **¥20.00** | | 感謝你對開源世界的貢獻 | | | | | **¥20.00** | | 感謝作者,不成敬意 | | | | | **¥20.00** | | 大佬NB,感謝感謝 | | | | | **¥20.00** | | 感謝大佬做的插件,很好用很方便 | | | | | **¥20.00** | | 牛逼 | | | | | **¥20.00** | | 好用,支持 | | | | | **¥20.00** | | 很好的插件 支持 | | | | | **¥20.00** | | 謝謝 | | | | | **¥20.00** | | 感謝作者日夜辛苦的寫程式碼,並開源 | | | | | **¥20.00** | | 希望能更完善,可以在雲端查看ob的其他格式的文件 | | | | | **¥20.00** | | 這個插件思路很好,加油 | | | | | **¥20.00** | | 很好的同步插件 | | | | | **¥20.00** | | 感謝您的工作 | | | | | **¥20.00** | | 太強了 | | | | | **¥20.00** | | 這麼好的東西應該​​會讓更多人知道,加大宣傳力道啊! | | | | | **¥20.00** | | 非常好用,感謝作者開發這麼好的工具! | | | | | **¥20.00** | | 感謝作者開發那麼好用的插件,讓我的obsidian旋轉🥰 | | | | | **¥20.00** | | 插件好用 | | | | | **¥20.00** | | 用了一段時間了,真的太棒了。 | | | | | **¥20.00** | | 馬年快樂 | | | | | **¥20.00** | | 謝謝辛苦了 | | | | | **¥20.00** | | 很有幫助,加油! | | | | | **¥20.00** | | 支持,加油~,很實用,強需求的功能。在AI時代大有用處 | | | | | **¥20.00** | | 感謝大佬做的同步插件,非常好用,請大佬先喝一杯咖啡,後續還會再繼續打賞的 | | | | | **¥20.00** | | 大佬的同步插件非常棒,我會一直持續支持的 | | | | | **¥20.00** | | 非常好用,感謝 | | | | | **¥20.00** | | 加油💪web端的圖片編輯功能整一下唄 | | | | | **¥20.00** | | 加油 | | | | | **¥20.00** | | 😘 | | | | | **¥20.00** | | 很好用,期待後續的開發與優化。感謝。 | | | | | **¥20.00** | | 非常好用感謝! | | | | | **¥20.00** | | 插件思路太對了 | | | | | **¥20.00** | | 希望越來越好👌🏻 | | | | | **¥20.00** | | 很棒的同步方案,未來可期!非常感謝開源!加油! | | | | | **¥18.00** | | 綿薄之力,以表敬意 | | | | | **¥10.00** | | 加油大神 | | | | | **¥10.00** | | 希望越來越好 | | | | | **¥10.00** | | 謝謝作者開發,謝謝開源,祝越來越好。 | | | | | **¥10.00** | | 感謝,比官方同步還好用 | | | | | **¥10.00** | | 感謝做了這麼便捷的同步軟體 | | | | | **¥9.90** | | 好軟體,感謝作者。 | | | | | **¥9.90** | | 牛🐮 | | | | | **¥8.88** | | 加油……你這個絕對有發展前途,推薦其他朋友看都覺得很不錯。 | | | | | **¥8.88** | | 感謝大佬,非常好用,按讚 | | | | | **¥8.88** | | 感謝分享 | | | | | **¥8.00** | | 很完善,連伺服器介面也做得非常好看 | | | | | **¥6.66** | | 裝好了 無敵 哈哈哈哈哈哈 | | | | | **¥6.66** | | 一定要堅持開發呀! ! ! | | | | | **¥6.66** | | 感謝開發,插件很好用,更新很快[強] | | | | | **¥6.66** | | 愛你 | | | | | **¥6.66** | | 老哥寫的插件很棒,繼續努力! | | | | | **¥5.00** | | 這也太好用了,折騰這麼久感覺終於畢業了(╥╯﹏╰╥)ง | | | | | **¥5.00** | | 不易 | | | | | **¥5.00** | | 感謝你的專案幫助到我,obsidian 同步從此變得容易 | | | | | **¥5.00** | | 開發的太棒了 | | | | | **¥5.00** | | 支援 | | | | | **¥5.00** | | 感謝開發這麼好用的插件 | | | | | **¥5.00** | | 打賞 | | | | | **¥5.00** | | 加油 | | | | | **¥5.00** | | 很好用,希望能把 git 做出來 | | | | | **¥3.66** | | 繼續開發,作出好產品 | | | | | **¥3.00** | | 新年快樂 | | | | | **¥1.00** | | 試試 | | | | | **¥1.00** | | 感謝,十分有用 | | | | | **¥1.00** | | 感恩您的 Obs 同步外掛非常有幫助! | | | | | **¥1.00** | | 從發現部署到使用,好多年了,從未有過的流暢絲滑的感覺,真的太好了! [強][強][強] | | --- *本數據最後更新於:2026/4/21 21:04:46* ================================================ FILE: docs/SyncProtocol.md ================================================ # WebSocket 同步协议前端对接说明 (版本 1.1) 本协议描述了最新调整后的同步流程,前端在对接 `NoteSync`, `FolderSync`, `SettingSync` 和 `FileSync` 时需遵循以下规范。 ## 1. 核心变更概览 - **Request**: 所有同步请求需携带 `context` 字符串。 - **Response**: 同步结果不再合并返回,改为 **先返回统计结束消息 (End),后发送逐条详情消息**。 - **Context**: 所有下发的同步响应都将原样透传请求中的 `context`。 ## 2. 交互流程示例 以 **笔记同步 (NoteSync)** 为例: ### Step 1: 前端发起同步请求 前端需生成一个唯一的 `context`(如随机 UUID 或时间戳),用于标识本次同步任务。 **Action**: `NoteSync` **Data**: ```json { "context": "sync_task_001", "vault": "MyNotes", "lastTime": 1708800000000, "notes": [...] } ``` ### Step 2: 服务端返回统计消息 (End) 服务端在扫描完变更后,会立刻发送一个 End 确认消息。该消息**不再包含明细列表**,仅用于告知统计数据。 **ActionType**: `NoteSyncEnd` **Response**: ```json { "code": 200, "status": true, "message": "success", "vault": "MyNotes", "context": "sync_task_001", "data": { "lastTime": 1708900000000, "needUploadCount": 2, "needModifyCount": 1, "needSyncMtimeCount": 0, "needDeleteCount": 1 } } ``` ### Step 3: 服务端逐条推送明细消息 随后,服务端会将具体的变更动作通过独立的 WebSocket 消息下发。 - **明细消息 1 (修改笔记)** **ActionType**: `NoteSyncModify` **Response**: `{ "context": "sync_task_001", "data": { "path": "test.md", "content": "..." }, ... }` - **明细消息 2 (删除笔记)** **ActionType**: `NoteSyncDelete` **Response**: `{ "context": "sync_task_001", "data": { "path": "old.md" }, ... }` ## 3. 响应消息结构 (Res) 所有 WebSocket 响应均遵循以下标准结构: | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `code` | int | 业务状态码 (200 为成功) | | `status` | bool | 成功状态 | | `message` | string | 状态描述 | | `data` | any | 业务数据载体 | | `vault` | string | 保险库名称 (透传) | | `context` | string | **任务上下文标识 (透传)** | ## 4. 前端集成建议 1. **并行处理**: 由于统计消息 (End) 提前到达,前端可以先更新同步进度 UI,随后监听后续的明细推送并动态更新本地缓存。 2. **任务匹配**: 在 WebSocket 的全局消息监听器中,建议通过响应体中的 `context` 字段来匹配本次同步请求的回调逻辑或状态。 3. **计数校验**: 前端可以通过 `SyncEnd` 消息中的 `needXXXCount` 来验证后续是否收到了足额的详情推送。 ## 5. 受影响的接口 Action | 模块 | 同步请求 Action | 统计结束消息 Type | 明细推送消息 Type | | :--- | :--- | :--- | :--- | | **笔记** | `NoteSync` | `NoteSyncEnd` | `NoteSyncModify`, `NoteSyncDelete`, `NoteSyncMtime`, `NoteSyncNeedPush` | | **文件夹** | `FolderSync` | `FolderSyncEnd` | `FolderSyncModify`, `FolderSyncDelete` | | **设置** | `SettingSync` | `SettingSyncEnd` | `SettingSyncModify`, `SettingSyncDelete`, `SettingSyncMtime`, `SettingSyncNeedUpload` | | **文件/附件** | `FileSync` | `FileSyncEnd` | `FileSyncUpdate`, `FileSyncDelete`, `FileSyncMtime`, `FileUpload` | --- *注:请确保前端代码能够兼容处理同一 `context` 下接连收到的多条消息。* ================================================ FILE: docs/admin_config_api.md ================================================ # 管理员配置接口文档 (`/api/admin/config`) 本文档描述了管理员用于获取和更新系统配置的 API 接口。配置参数同步自 `config/config.yaml`。 --- ## 1. 概述 该接口允许具有管理员权限的用户查看和修改系统的核心配置。配置修改后会即时生效并持久化到服务器的配置文件中。 - **基础路径**: `/api/admin/config` - **认证方式**: 需要 `Authorization` 请求头传递 `Token`。 - **权限要求**: 仅限管理员(`AdminUID` 匹配的用户)访问。如果 `adminUid` 设置为 `0`,则默认不限制管理员访问权限(慎用)。 --- ## 2. 获取配置 (`GET`) 获取当前的系统配置信息。 ### 请求头 | 参数名 | 类型 | 是否必选 | 说明 | |:----------------|:---------|:---------|:---------------------| | `Authorization` | `string` | 是 | 用户登录令牌 (Token) | ### 响应示例 **状态码**: `200 OK` ```json { "code": 1, "status": true, "message": "成功", "data": { "fontSet": "local", "registerIsEnable": true, "fileChunkSize": "512KB", "softDeleteRetentionTime": "7d", "uploadSessionTimeout": "1d", "adminUid": 0 } } ``` --- ## 3. 更新配置 (`POST`) 更新系统配置信息。 ### 请求头 | 参数名 | 类型 | 是否必选 | 说明 | |:----------------|:---------|:---------|:---------------------| | `Authorization` | `string` | 是 | 用户登录令牌 (Token) | | `Content-Type` | `string` | 是 | `application/json` | ### 请求参数 (JSON) | 参数名 | 类型 | 示例值 | 说明 | |:--------------------------|:----------|:----------|:-------------------| | `fontSet` | `string` | `"local"` | 界面字体设置 | | `registerIsEnable` | `boolean` | `true` | 是否开启用户注册 | | `fileChunkSize` | `string` | `"1MB"` | 文件同步分块大小 | | `softDeleteRetentionTime` | `string` | `"30d"` | 软删除笔记保留时长 | | `uploadSessionTimeout` | `string` | `"24h"` | 上传会话过期时间 | | `adminUid` | `integer` | `1` | 管理员 UID | --- ## 4. 参数详情与默认值 | 字段 | 类型 | 默认值 | 详细描述 | |:--------------------------|:----------|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------| | `fontSet` | `string` | `"local"` | **字体设置**:
• 留空:不设置特定字体。
• `local`:使用系统本地字体。
• 字体链接:从网络加载特定字体。 | | `registerIsEnable` | `boolean` | `true` | **注册开关**:控制是否允许新用户注册。若关闭,注册 API 将返回错误。 | | `fileChunkSize` | `string` | `"512KB"` | **分块大小**:文件上传和下载时的分块大小。支持单位:`B`, `KB`, `MB`, `GB`(如 `1MB`, `1024`)。`0` 表示默认 512KB。 | | `softDeleteRetentionTime` | `string` | `"7d"` | **软删除保留时长**:笔记删除后进入回收站的保留时间(如 `30d`, `12h`)。超过此时间将被物理删除。建议设置足够长以确保离线设备同步。`0` 表示不自动清理。 | | `uploadSessionTimeout` | `string` | `"1d"` | **上传会话超时**:文件分块上传会话的有效期(如 `5m`, `1d`)。`0` 表示永不超时。 | | `adminUid` | `integer` | `0` | **管理员 UID**:指定特定的用户 UID 作为管理员。`0` 表示不启用特定的管理员权限校验逻辑(建议在初始化后设置为实际的管理员 UID)。 | --- ## 5. 常见错误 | 状态码 | 业务代码 (code) | 说明 | |:-------|:----------------|:------------------------------------------------------------------| | `401` | `508` | 登录状态失效,请重新登录。 | | `200` | `445` | **此操作需要管理员权限**:当前登录 UID 与配置的 `adminUid` 不匹配。 | | `200` | `505` | **参数验证失败**:请求数据格式不正确,或时间/容量格式无法解析。 | ================================================ FILE: docs/docs.go ================================================ // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": { "name": "Haierkeys", "url": "https://github.com/haierkeys", "email": "haierkeys@gmail.com" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/api/admin/check": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Check if the current logged-in user has system admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Check admin permission", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminCheckResponse" } } } ] } } } } }, "/api/admin/cloudflared_tunnel_download": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Trigger the download of cloudflared binary for the current platform", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Download cloudflared binary", "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get full system configuration information, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get full admin config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify full system configuration information, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update admin config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/cloudflare": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get Cloudflare tunnel configuration, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get Cloudflare config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminCloudflareConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify Cloudflare tunnel configuration, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update Cloudflare config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminCloudflareConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminCloudflareConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/ngrok": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get Ngrok tunnel configuration, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get Ngrok config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminNgrokConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify Ngrok tunnel configuration, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update Ngrok config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminNgrokConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminNgrokConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/user_database": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get user database configuration information, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get user database config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify user database configuration information, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update user database config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/user_database/test": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Test if the provided database configuration can connect successfully, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Test user database connection", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Connection failed", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/gc": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Manually run Go runtime GC and release memory to OS, requires admin privileges", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Trigger manual GC", "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/restart": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Gracefully restart the server", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Trigger server restart", "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/system/info": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get server runtime, CPU, memory, host and process info, requires admin privileges", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get system stats", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminSystemInfo" } } } ] } } } } }, "/api/admin/upgrade": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Download latest version and restart server", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Trigger server upgrade", "parameters": [ { "type": "string", "description": "Version to upgrade (e.g. 2.0.10 or latest)", "name": "version", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/ws_clients": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get a list of all current WebSocket connections, requires admin privileges", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get connected WebSocket clients", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/app.WSClientInfo" } } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/config": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Update backup configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Backup Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.BackupConfigRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.BackupConfigDTO" } } } ] } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Delete backup configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "ID // ID", "name": "id", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/configs": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Get backup configurations", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.BackupConfigDTO" } } } } ] } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/execute": { "post": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Trigger a backup manually", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Backup Execute Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.BackupExecuteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/historys": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Get backup history list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "Config ID // 配置 ID", "name": "configId", "in": "query", "required": true }, { "type": "integer", "example": 1, "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "example": 10, "description": "Page size // 每页大小", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.BackupHistoryDTO" } } } } ] } } } ] } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/file": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get raw binary data of an attachment by path, supports strong cache control", "produces": [ "application/octet-stream" ], "tags": [ "File" ], "summary": "Get attachment content", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "Image.png", "description": "File path // 文件路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "type": "file" } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently delete a specific attachment record and its physical file", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Delete attachment", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Image.png", "description": "File path // 文件路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query", "required": true }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/file/info": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get attachment metadata (FileDTO) by path", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Get attachment info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "Image.png", "description": "File path // 文件路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/file/recycle-clear": { "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently clear selected files from recycle bin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Clear recycle bin", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Clear Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FileRecycleClearRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/file/rename": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Rename an attachment to a new path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Rename attachment", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Rename Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FileRenameRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/file/restore": { "put": { "security": [ { "UserAuthToken": [] } ], "description": "Restore deleted attachment from trash", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Restore attachment", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Restore Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FileRestoreRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/files": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get attachment list for current user with pagination, search, filter, and sort support", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Get file list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "vacation", "description": "Search keyword // 搜索关键词", "name": "keyword", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.FileDTO" } } } } ] } } } ] } } } } }, "/api/folder": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get folder info for current user by path or pathHash", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Get folder info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects/Work", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FolderDTO" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Create a new folder or restore a deleted one by path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Create folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Create Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FolderCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FolderDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Soft delete a folder by path or pathHash", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Delete folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Delete Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FolderDeleteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/folder/files": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "List non-deleted files in a specific folder with pagination and sorting", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "List files in folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.FileDTO" } } } } ] } } } ] } } } } }, "/api/folder/notes": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "List non-deleted notes in a specific folder with pagination and sorting", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "List notes in folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteDTO" } } } } ] } } } ] } } } } }, "/api/folder/tree": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get the complete folder tree structure for a vault", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Get folder tree", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "example": 3, "description": "Tree depth // 树深度", "name": "depth", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FolderTreeResponse" } } } ] } } } } }, "/api/folders": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get folder list for current user by parent path or pathHash", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Get folder list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.FolderDTO" } } } } ] } } } } }, "/api/git-sync/config": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Update git sync configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Git Sync Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncConfigRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.GitSyncConfigDTO" } } } ] } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Delete git sync configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Git Sync ID", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncDeleteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/config/clean": { "delete": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Clean local git workspace", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Clean Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncCleanRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/config/execute": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Trigger a manual git sync", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Execute Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncExecuteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/configs": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Get git sync configurations", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.GitSyncConfigDTO" } } } } ] } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/histories": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Get git sync histories", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "name": "configId", "in": "query" }, { "type": "integer", "name": "page", "in": "query" }, { "type": "integer", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.GitSyncHistoryDTO" } } } } ] } } } ] } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/validate": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Validate git sync parameters", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Validation Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncValidateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/health": { "get": { "description": "Check service health status, including database connection", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Health check", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/api_router.HealthResponse" } } } } }, "/api/note": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get specific note content and metadata by path or path hash", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get note details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteWithFileLinksResponse" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Handle note creation, modification, or renaming (identified by path change)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Create or update note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Note Content", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteModifyOrCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Move note to trash", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Delete note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/append": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Append content to the end of a note", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Append content to note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Append Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteAppendRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/backlinks": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all other notes that link to the specified note", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get backlinks", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteLinkItem" } } } } ] } } } } }, "/api/note/frontmatter": { "patch": { "security": [ { "UserAuthToken": [] } ], "description": "Update or delete note frontmatter fields", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Modify note frontmatter", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Frontmatter Modification Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NotePatchFrontmatterRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/histories": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all history records for a specific note with pagination", "produces": [ "application/json" ], "tags": [ "Note History" ], "summary": "Get note history list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteHistoryDTO" } } } } ] } } } ] } } } } }, "/api/note/history": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get specific note history content by history record ID", "produces": [ "application/json" ], "tags": [ "Note History" ], "summary": "Get note history details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "format": "int64", "description": "History Record ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteHistoryDTO" } } } ] } } } } }, "/api/note/history/restore": { "put": { "security": [ { "UserAuthToken": [] } ], "description": "Restore note content to a specific history version", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note History" ], "summary": "Restore note from history", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Restore Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteHistoryRestoreRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/move": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Move a note to a new path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Move note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Move Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteMoveRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/outlinks": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get other notes that the specified note links to", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get outgoing links", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteLinkItem" } } } } ] } } } } }, "/api/note/prepend": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Insert content at the beginning of a note (after frontmatter)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Prepend content to note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Prepend Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NotePrependRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/recycle-clear": { "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently clear selected notes from recycle bin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Clear recycle bin", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Clear Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteRecycleClearRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/note/rename": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Rename a note to a new path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Rename note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Rename Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteRenameRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/replace": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Perform find and replace operation in a note, supporting regular expressions", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Find and replace in note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Find and Replace Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteReplaceRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteReplaceResponse" } } } ] } } } } }, "/api/note/restore": { "put": { "security": [ { "UserAuthToken": [] } ], "description": "Restore deleted note from trash", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Restore note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Restore Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteRestoreRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/notes": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get note list for current user with pagination", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get note list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "todo", "description": "Search keyword // 搜索关键词", "name": "keyword", "in": "query" }, { "type": "string", "example": "note1.md,note2.md", "description": "Comma-separated exact path list for share filter // 逗号分隔的精确路径列表,用于分享筛选", "name": "paths", "in": "query" }, { "type": "boolean", "example": true, "description": "Whether to search content // 是否搜索内容", "name": "searchContent", "in": "query" }, { "type": "string", "example": "content", "description": "Search mode (path, content) // 搜索模式(路径、内容)", "name": "searchMode", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteNoContentDTO" } } } } ] } } } ] } } } } }, "/api/notes/share-paths": { "get": { "security": [ { "UserAuthToken": [] } ], "tags": [ "Share" ], "summary": "Get active shared note paths", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "description": "Vault name", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "string" } } } } ] } } } } }, "/api/setting": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get setting info for current user by path or pathHash", "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Get setting info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "User/Theme", "description": "Setting path // 配置路径", "name": "path", "in": "query" }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.SettingDTO" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Create a new setting or update an existing one", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Create or update setting", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Create/Update Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SettingModifyOrCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.SettingDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Soft delete a setting by path or pathHash", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Delete setting", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Delete Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SettingDeleteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/setting/rename": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Rename a setting and update its path and pathHash", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Rename setting", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Rename Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SettingRenameRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.SettingDTO" } } } ] } } } } }, "/api/settings": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get setting list for current user with pagination and keyword filtering", "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Get setting list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "User/", "description": "Keyword // 关键词", "name": "keyword", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.SettingDTO" } } } } ] } } } ] } } } } }, "/api/share": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get share token and info by vault and path", "tags": [ "Share" ], "summary": "Query share by path", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Resource path // 资源路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Resource path Hash // 资源路径哈希", "name": "pathHash", "in": "query", "required": true }, { "type": "string", "example": "defaultVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.ShareCreateResponse" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Create a share token for a specific note or attachment, automatically resolve attachment references and authorize", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Create resource share", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Share Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.ShareCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.ShareCreateResponse" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Cancel a share by ID or path parameters", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Cancel share", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Cancel Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.ShareCancelRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/share/file": { "get": { "security": [ { "ShareAuthToken": [] } ], "description": "Get raw binary data of a specific attachment via share token", "produces": [ "application/octet-stream" ], "tags": [ "Share" ], "summary": "Get shared attachment content", "parameters": [ { "type": "string", "description": "Auth Token", "name": "Share-Token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "Resource ID // 资源 ID", "name": "id", "in": "query", "required": true }, { "type": "string", "example": "123456", "description": "Share password // 分享密码", "name": "password", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "type": "file" } } } } }, "/api/share/note": { "get": { "security": [ { "ShareAuthToken": [] } ], "description": "Get specific note content (restricted read-only access) via share token", "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Get shared note details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "Share-Token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "Resource ID // 资源 ID", "name": "id", "in": "query", "required": true }, { "type": "string", "example": "123456", "description": "Share password // 分享密码", "name": "password", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/share/password": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Set or update password for a share record", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Update share password", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Update Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SharePasswordUpdateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/share/short_link": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Call sink.cool API to generate a short link for a given share record", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Create short link for share", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Short Link Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.ShareShortLinkCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/api/shares": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all active and inactive shares of the user, supports sorting and pagination", "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "List shares", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "description": "Sort field: created_at, updated_at, expires_at (default: created_at)", "name": "sort_by", "in": "query" }, { "type": "string", "description": "Sort direction: asc or desc (default: desc)", "name": "sort_order", "in": "query" }, { "type": "integer", "description": "Page number", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.ShareListItem" } } } } ] } } } ] } } } } }, "/api/storage": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Get storage configuration list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.StorageDTO" } } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Create or update storage configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Storage Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.StoragePostRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.StorageDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Delete storage configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "format": "int64", "description": "Storage ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/storage/enabled_types": { "get": { "description": "Get list of enabled storage types. Possible values: localfs, oss, s3, r2, minio, webdav", "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Get enabled storage types", "responses": { "200": { "description": "Success. Data contains: localfs, oss, s3, r2, minio, webdav", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "string" } } } } ] } } } } }, "/api/storage/validate": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Validate storage connection", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Storage Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.StoragePostRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/support": { "get": { "description": "Get support records for the specified language with pagination and sorting", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get support records", "parameters": [ { "type": "string", "description": "Language code (default: en)", "name": "lang", "in": "query" }, { "type": "string", "description": "Sort by field (amount, time, name, item)", "name": "sortBy", "in": "query" }, { "type": "string", "description": "Sort order (asc, desc)", "name": "sortOrder", "in": "query" }, { "type": "integer", "description": "Page number", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/app.ListRes" } } } ] } } } } }, "/api/sync-logs": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get sync log list for current user with optional type/action filters and pagination", "produces": [ "application/json" ], "tags": [ "Sync Log" ], "summary": "Get sync log list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "modify", "description": "Action type // 操作类型", "name": "action", "in": "query" }, { "type": "string", "example": "note", "description": "Resource type: note / file / setting / folder // 资源类型", "name": "type", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name (optional filter) // 保险库名称(可选过滤)", "name": "vault", "in": "query" }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.SyncLogDTO" } } } } ] } } } ] } } } } }, "/api/user/change_password": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Handle password change request for current user, validate old password and update new password.\n处理当前用户的修改密码请求,验证旧密码并更新新密码。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "Change user password", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Change Password Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.UserChangePasswordRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Parameters / Old Password Incorrect", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/user/info": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Handle request to get current user info.\n处理获取当前用户信息的请求。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "Get user info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.UserDTO" } } } ] } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/user/login": { "post": { "description": "Handle user login HTTP request, validate parameters and return auth token.\n处理用户登录 HTTP 请求,验证参数并返回认证 Token。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "User login", "parameters": [ { "description": "Login Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.UserLoginRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.UserDTO" } } } ] } }, "400": { "description": "Invalid Parameters / Invalid Credentials", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/user/register": { "post": { "description": "Handle user registration HTTP request, validate parameters and call UserService. Registration may be disabled in server settings.\n处理用户注册 HTTP 请求,验证参数并调用 UserService。注册功能可能在服务器设置中被禁用。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "User registration", "parameters": [ { "description": "Register Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.UserCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.UserDTO" } } } ] } }, "400": { "description": "Invalid Parameters / Registration Disabled / User Already Exists", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/vault": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all note vaults for current user", "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Get vault list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.VaultDTO" } } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Be used to create a new vault or update an existing vault configuration based on the ID in the request parameters", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Create or update vault", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Vault Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.VaultPostRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.VaultDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently delete a specific note vault and all associated notes and attachments", "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Delete vault", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "minimum": 1, "type": "integer", "example": 1, "description": "Vault ID // 保险库 ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/vault/get": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get specific vault configuration details by vault ID", "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Get vault details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "format": "int64", "description": "Vault ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.VaultDTO" } } } ] } } } } }, "/api/version": { "get": { "description": "Get current server software version, Git tag, and build time", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get server version info", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.VersionDTO" } } } ] } } } } }, "/api/webgui/config": { "get": { "description": "Get non-sensitive configuration required for frontend display, such as font settings, registration status, etc.", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get WebGUI basic config", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminWebGUIConfig" } } } ] } } } } } }, "definitions": { "api_router.HealthResponse": { "type": "object", "properties": { "database": { "description": "\"connected\" or \"error\" // \"connected\" 或 \"error\"", "type": "string" }, "status": { "description": "\"healthy\" or \"unhealthy\" // \"healthy\" 或 \"unhealthy\"", "type": "string" }, "uptime": { "description": "Uptime (seconds) // 运行时间(秒)", "type": "number" }, "version": { "description": "Service version number // 服务版本号", "type": "string" } } }, "app.ListRes": { "type": "object", "properties": { "list": { "description": "Data list // 数据清单" }, "pager": { "description": "Pagination info // 翻页信息", "allOf": [ { "$ref": "#/definitions/app.Pager" } ] } } }, "app.Pager": { "type": "object", "properties": { "page": { "description": "Page number // 页码", "type": "integer" }, "pageSize": { "description": "Page size // 每页数量", "type": "integer" }, "totalRows": { "description": "Total rows // 总行数", "type": "integer" } } }, "app.Res": { "type": "object", "properties": { "code": { "type": "integer" }, "context": {}, "data": {}, "details": {}, "message": {}, "status": { "type": "boolean" }, "vault": {} } }, "app.WSClientInfo": { "type": "object", "properties": { "clientName": { "type": "string" }, "clientType": { "type": "string" }, "clientVersion": { "type": "string" }, "nickname": { "type": "string" }, "platformInfo": { "type": "object", "additionalProperties": { "type": "boolean" } }, "remoteAddr": { "type": "string" }, "startTime": { "type": "string" }, "traceId": { "type": "string" }, "uid": { "type": "string" } } }, "diffmatchpatch.Diff": { "type": "object", "properties": { "text": { "type": "string" }, "type": { "$ref": "#/definitions/diffmatchpatch.Operation" } } }, "diffmatchpatch.Operation": { "type": "integer", "format": "int32", "enum": [ -1, 1, 0 ], "x-enum-varnames": [ "DiffDelete", "DiffInsert", "DiffEqual" ] }, "dto.AdminCPUInfo": { "type": "object", "properties": { "loadAvg": { "description": "Load average // 平均负载", "allOf": [ { "$ref": "#/definitions/dto.AdminLoadInfo" } ] }, "logicalCores": { "description": "Logical cores // 逻辑核心数", "type": "integer" }, "modelName": { "description": "Model name // 型号", "type": "string" }, "percent": { "description": "Usage percentage per core // 每个核心的使用率", "type": "array", "items": { "type": "number" } }, "physicalCores": { "description": "Physical cores // 物理核心数", "type": "integer" } } }, "dto.AdminCheckResponse": { "type": "object", "properties": { "isAdmin": { "description": "Whether have admin privileges // 是否具有管理员权限", "type": "boolean" } } }, "dto.AdminCloudflareConfig": { "type": "object", "properties": { "enabled": { "description": "Whether to enable cloudflare tunnel // 是否启用 cloudflare 隧道", "type": "boolean" }, "logEnabled": { "description": "Whether to enable cloudflare tunnel logging // 是否开启 cloudflare 隧道日志", "type": "boolean" }, "token": { "description": "cloudflare tunnel token // cloudflare 隧道令牌", "type": "string" } } }, "dto.AdminConfig": { "type": "object", "properties": { "adminUid": { "description": "Admin UID // 管理员 UID", "type": "integer" }, "authTokenKey": { "description": "Auth token key // 认证 Token 密钥", "type": "string" }, "defaultApiFolder": { "description": "Default API folder // 默认 API 目录", "type": "string" }, "fileChunkSize": { "description": "File chunk size // 文件分块大小", "type": "string" }, "fontSet": { "description": "Font set // 字体设置", "type": "string" }, "historyKeepVersions": { "description": "History versions to keep // 历史版本保留数", "type": "integer" }, "historySaveDelay": { "description": "History save delay // 历史保存延迟", "type": "string" }, "pullSource": { "description": "Data pull source: auto | github | cnb // 数据拉取源:auto | github | cnb", "type": "string" }, "registerIsEnable": { "description": "Registration enablement // 是否开启注册", "type": "boolean" }, "shareTokenExpiry": { "description": "Share token expiry // 分享 Token 有效期", "type": "string" }, "shareTokenKey": { "description": "Share token key // 分享 Token 密钥", "type": "string" }, "softDeleteRetentionTime": { "description": "Soft delete retention time // 软删除保留时间", "type": "string" }, "tokenExpiry": { "description": "Token expiry // Token 有效期", "type": "string" }, "uploadSessionTimeout": { "description": "Upload session timeout // 上传会话超时时间", "type": "string" } } }, "dto.AdminHostInfo": { "type": "object", "properties": { "arch": { "description": "Architecture // 架构", "type": "string" }, "currentTime": { "description": "Current system time // 当前系统时间", "type": "string" }, "hostname": { "description": "Hostname // 主机名", "type": "string" }, "kernelVersion": { "description": "Kernel version // 内核版本", "type": "string" }, "os": { "description": "Operating system // 操作系统", "type": "string" }, "osPretty": { "description": "Detailed OS name // 详细操作系统名称", "type": "string" }, "platform": { "description": "Platform name // 平台", "type": "string" }, "timezone": { "description": "Time zone name // 时区名称", "type": "string" }, "timezoneOffset": { "description": "Time zone offset in seconds // 时区偏移(秒)", "type": "integer" }, "uptime": { "description": "System uptime // 系统运行时间", "type": "integer" } } }, "dto.AdminLoadInfo": { "type": "object", "properties": { "load1": { "description": "Load 1 min // 1分钟负载", "type": "number" }, "load15": { "description": "Load 15 min // 15分钟负载", "type": "number" }, "load5": { "description": "Load 5 min // 5分钟负载", "type": "number" } } }, "dto.AdminMemoryInfo": { "type": "object", "properties": { "available": { "description": "Available memory // 可用内存", "type": "integer" }, "swapTotal": { "description": "Total swap space // 交换区总量", "type": "integer" }, "swapUsed": { "description": "Used swap space // 交换区已用", "type": "integer" }, "swapUsedPercent": { "description": "Swap usage percentage // 交换区使用率", "type": "number" }, "total": { "description": "Total physical memory // 系统总内存", "type": "integer" }, "used": { "description": "Used memory // 已用内存", "type": "integer" }, "usedPercent": { "description": "Memory usage percentage // 内存使用率", "type": "number" } } }, "dto.AdminNgrokConfig": { "type": "object", "properties": { "authToken": { "description": "ngrok auth token // ngrok 认证令牌", "type": "string" }, "domain": { "description": "Custom domain // 自定义域名", "type": "string" }, "enabled": { "description": "Whether to enable ngrok tunnel // 是否启用 ngrok 隧道", "type": "boolean" } } }, "dto.AdminProcessInfo": { "type": "object", "properties": { "cpuPercent": { "description": "CPU Usage percentage // CPU 使用率", "type": "number" }, "memoryPercent": { "description": "Memory Usage percentage // 内存使用率", "type": "number" }, "name": { "description": "Process Name // 进程名称", "type": "string" }, "pid": { "description": "Process ID // 进程 ID", "type": "integer" }, "ppid": { "description": "Parent Process ID // 父进程 ID", "type": "integer" } } }, "dto.AdminRuntimeInfo": { "type": "object", "properties": { "buckHashSys": { "description": "Memory obtained from system for profiling bucket hash table (bytes) // 分析桶哈希表占用的系统内存", "type": "integer" }, "gcSys": { "description": "Memory obtained from system for metadata for GC (bytes) // GC 元数据占用的系统内存", "type": "integer" }, "heapIdle": { "description": "Memory in idle spans (bytes) // 空闲 Span 占用的内存", "type": "integer" }, "heapInuse": { "description": "Memory in in-use spans (bytes) // 正在使用的 Span 占用的内存", "type": "integer" }, "heapReleased": { "description": "Memory released to OS (bytes) // 释放回操作系统的内存(字节)", "type": "integer" }, "heapSys": { "description": "Memory obtained from system for heap (bytes) // 堆占用的系统内存", "type": "integer" }, "mCacheSys": { "description": "Memory obtained from system for mcache (bytes) // mcache 占用的系统内存", "type": "integer" }, "mSpanSys": { "description": "Memory obtained from system for mspan (bytes) // mspan 占用的系统内存", "type": "integer" }, "memAlloc": { "description": "Allocated memory (bytes) // 已分配内存(字节)", "type": "integer" }, "memSys": { "description": "Memory obtained from system (bytes) // 从系统获取的内存(字节)", "type": "integer" }, "memTotal": { "description": "Total memory allocated (bytes) // 累计分配内存(字节)", "type": "integer" }, "nextGc": { "description": "Target heap size for the next GC cycle // 下次 GC 的目标堆大小", "type": "integer" }, "numGc": { "description": "Number of completed GC cycles // GC 次数", "type": "integer" }, "numGoroutine": { "description": "Number of goroutines // Goroutine 数量", "type": "integer" }, "otherSys": { "description": "Other system memory (bytes) // 其他系统内存", "type": "integer" }, "stackSys": { "description": "Memory obtained from system for stack (bytes) // 栈占用的系统内存", "type": "integer" } } }, "dto.AdminSystemInfo": { "type": "object", "properties": { "cpu": { "description": "CPU information // CPU 信息", "allOf": [ { "$ref": "#/definitions/dto.AdminCPUInfo" } ] }, "host": { "description": "Host information // 主机信息", "allOf": [ { "$ref": "#/definitions/dto.AdminHostInfo" } ] }, "memory": { "description": "Memory information // 内存信息", "allOf": [ { "$ref": "#/definitions/dto.AdminMemoryInfo" } ] }, "process": { "description": "Process information // 进程信息", "allOf": [ { "$ref": "#/definitions/dto.AdminProcessInfo" } ] }, "runtimeStatus": { "description": "Go runtime status // Go 运行时状态", "allOf": [ { "$ref": "#/definitions/dto.AdminRuntimeInfo" } ] }, "startTime": { "description": "Start time // 启动时间", "type": "string" }, "uptime": { "description": "Uptime (seconds) // 运行时间(秒)", "type": "number" } } }, "dto.AdminUserDatabaseConfig": { "type": "object", "properties": { "charset": { "description": "Charset // 字符集", "type": "string" }, "connMaxIdleTime": { "description": "Connection max idle time // 空闲连接最大生命周期", "type": "string" }, "connMaxLifetime": { "description": "Connection max lifetime // 连接最大生命周期", "type": "string" }, "host": { "description": "Host // 主机", "type": "string" }, "maxIdleConns": { "description": "Max idle connections // 最大闲置连接数", "type": "integer" }, "maxOpenConns": { "description": "Max open connections // 最大打开连接数", "type": "integer" }, "maxWriteConcurrency": { "description": "Max write concurrency // 最大并发写入数", "type": "integer" }, "name": { "description": "Database name // 数据库名", "type": "string" }, "parseTime": { "description": "Parse time // 是否解析时间", "type": "boolean" }, "password": { "description": "Password // 密码", "type": "string" }, "path": { "description": "SQLite database file path // SQLite 数据库文件路径", "type": "string" }, "port": { "description": "Port // 端口", "type": "integer" }, "schema": { "description": "Database schema (postgres only) // 数据库 Schema", "type": "string" }, "sslMode": { "description": "SSL mode (postgres only) // SSL 模式", "type": "string" }, "type": { "description": "Database type (mysql, postgres, sqlite) // 数据库类型", "type": "string", "enum": [ "mysql", "postgres", "sqlite" ] }, "userName": { "description": "Username // 用户名", "type": "string" } } }, "dto.AdminWebGUIConfig": { "type": "object", "properties": { "adminUid": { "description": "Admin UID // 管理员 UID", "type": "integer" }, "fontSet": { "description": "Font set // 字体设置", "type": "string" }, "registerIsEnable": { "description": "Registration enablement // 是否开启注册", "type": "boolean" } } }, "dto.BackupConfigDTO": { "type": "object", "properties": { "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "cronExpression": { "description": "Cron expression // Cron表达式", "type": "string" }, "cronStrategy": { "description": "Cron strategy // 定时策略", "type": "string" }, "id": { "description": "Config ID // 配置ID", "type": "integer" }, "includeVaultName": { "description": "Whether sync path includes vault name // 同步路径是否包含仓库名", "type": "boolean" }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean" }, "lastMessage": { "description": "Last run result message // 上次运行结果消息", "type": "string" }, "lastRunTime": { "description": "Last run time // 上次运行时间", "type": "string" }, "lastStatus": { "description": "Last status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) // 上次状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped)", "type": "integer" }, "nextRunTime": { "description": "Next run time // 下次运行时间", "type": "string" }, "retentionDays": { "description": "Retention days // 保留天数", "type": "integer" }, "storageIds": { "description": "Storage ID list // 存储ID列表", "type": "string" }, "type": { "description": "Backup type (full, incremental, sync) // 备份类型 (full, incremental, sync)", "type": "string" }, "uid": { "description": "User UID // 用户ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "vault": { "description": "Associated vault name // 关联库名称", "type": "string" } } }, "dto.BackupConfigRequest": { "type": "object", "required": [ "cronStrategy", "storageIds", "type" ], "properties": { "cronExpression": { "description": "Cron expression // Cron 表达式", "type": "string", "example": "0 0 * * *" }, "cronStrategy": { "description": "Cron strategy // 定时策略", "type": "string", "enum": [ "daily", "weekly", "monthly", "custom" ], "example": "daily" }, "id": { "description": "ID // ID", "type": "integer", "example": 1 }, "includeVaultName": { "description": "Include vault name // 同步路径是否包含仓库名", "type": "boolean", "example": false }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean", "example": true }, "retentionDays": { "description": "Retention days // 保留天数", "type": "integer", "minimum": -1, "example": 7 }, "storageIds": { "description": "Storage IDs // 存储 ID 列表", "type": "string", "example": "[1, 2]" }, "type": { "description": "Backup type // 备份类型", "type": "string", "enum": [ "full", "incremental", "sync" ], "example": "sync" }, "vault": { "description": "Vault name // 仓库名称", "type": "string", "example": "test" } } }, "dto.BackupExecuteRequest": { "type": "object", "properties": { "id": { "description": "ID // ID", "type": "integer", "example": 1 } } }, "dto.BackupHistoryDTO": { "type": "object", "properties": { "configId": { "description": "Config ID // 配置ID", "type": "integer" }, "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "endTime": { "description": "End time // 结束时间", "type": "string" }, "fileCount": { "description": "File count // 文件数量", "type": "integer" }, "filePath": { "description": "File path // 文件路径", "type": "string" }, "fileSize": { "description": "File size // 文件大小", "type": "integer" }, "id": { "description": "History record ID // 历史记录ID", "type": "integer" }, "message": { "description": "Result message // 结果消息", "type": "string" }, "startTime": { "description": "Start time // 开始时间", "type": "string" }, "status": { "description": "Status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) // 状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped)", "type": "integer" }, "storageId": { "description": "Storage ID // 存储ID", "type": "integer" }, "type": { "description": "Backup type // 备份类型", "type": "string" }, "uid": { "description": "User UID // 用户ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" } } }, "dto.FileDTO": { "type": "object", "properties": { "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Updated timestamp // 更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "File path // 文件路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "rename": { "description": "Rename flag // 重命名标记", "type": "integer" }, "size": { "description": "File size // 文件大小", "type": "integer" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" } } }, "dto.FileRecycleClearRequest": { "type": "object", "required": [ "vault" ], "properties": { "path": { "description": "File path, empty for all // 文件路径,为空则清理全部", "type": "string", "example": "path/to/file.png" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FileRenameRequest": { "type": "object", "required": [ "oldPath", "path", "vault" ], "properties": { "oldPath": { "description": "Old path // 旧路径", "type": "string", "example": "OldImage.png" }, "oldPathHash": { "description": "Old path hash // 旧路径哈希", "type": "string", "example": "ofhash456" }, "path": { "description": "New path // 新路径", "type": "string", "example": "NewImage.png" }, "pathHash": { "description": "New path hash // 新路径哈希", "type": "string", "example": "nfhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FileRestoreRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "File path // 文件路径", "type": "string", "example": "Image.png" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FolderCreateRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Folder path // 文件夹路径", "type": "string", "example": "NewFolder" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash456" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FolderDTO": { "type": "object", "properties": { "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Folder path // 文件夹路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希值", "type": "string" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" } } }, "dto.FolderDeleteRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Folder path // 文件夹路径", "type": "string", "example": "OldFolder" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash789" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FolderTreeNode": { "type": "object", "properties": { "children": { "description": "Child nodes // 子节点", "type": "array", "items": { "$ref": "#/definitions/dto.FolderTreeNode" } }, "fileCount": { "description": "File count // 文件数量", "type": "integer" }, "name": { "description": "Node name // 节点名称", "type": "string" }, "noteCount": { "description": "Note count // 笔记数量", "type": "integer" }, "path": { "description": "Node path // 节点路径", "type": "string" } } }, "dto.FolderTreeResponse": { "type": "object", "properties": { "folders": { "description": "Folder tree // 文件夹树", "type": "array", "items": { "$ref": "#/definitions/dto.FolderTreeNode" } }, "rootFileCount": { "description": "File count in root // 根目录中的文件数量", "type": "integer" }, "rootNoteCount": { "description": "Note count in root // 根目录中的笔记数量", "type": "integer" } } }, "dto.GitSyncCleanRequest": { "type": "object", "properties": { "configId": { "type": "integer" } } }, "dto.GitSyncConfigDTO": { "type": "object", "properties": { "branch": { "description": "Branch // 分支", "type": "string" }, "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "delay": { "description": "Delay time (seconds) // 延迟时间(秒)", "type": "integer" }, "id": { "description": "Task ID // 任务ID", "type": "integer" }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean" }, "lastMessage": { "description": "Last run result message // 上次运行结果消息", "type": "string" }, "lastStatus": { "description": "Last status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown) // 上次状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown)", "type": "integer" }, "lastSyncTime": { "description": "Last sync time // 上次同步时间", "type": "string" }, "password": { "description": "Password // 密码", "type": "string" }, "repoUrl": { "description": "Repository URL // 仓库地址", "type": "string" }, "retentionDays": { "description": "History retention days // 历史记录保留天数", "type": "integer" }, "uid": { "description": "User ID // 用户ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "username": { "description": "Username // 用户名", "type": "string" }, "vault": { "description": "Associated vault name // 关联库名称", "type": "string" } } }, "dto.GitSyncConfigRequest": { "type": "object", "required": [ "repoUrl" ], "properties": { "branch": { "type": "string" }, "delay": { "description": "Delay time (seconds) // 延迟时间(秒)", "type": "integer" }, "id": { "type": "integer" }, "isEnabled": { "type": "boolean" }, "password": { "type": "string" }, "repoUrl": { "type": "string" }, "retentionDays": { "type": "integer" }, "username": { "type": "string" }, "vault": { "description": "Associated vault name // 关联笔记本名称", "type": "string" } } }, "dto.GitSyncDeleteRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "integer" } } }, "dto.GitSyncExecuteRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "integer" } } }, "dto.GitSyncHistoryDTO": { "type": "object", "properties": { "configId": { "type": "integer" }, "createdAt": { "type": "string" }, "endTime": { "type": "string" }, "id": { "type": "integer" }, "message": { "type": "string" }, "startTime": { "type": "string" }, "status": { "description": "0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown", "type": "integer" } } }, "dto.GitSyncValidateRequest": { "type": "object", "required": [ "repoUrl" ], "properties": { "branch": { "type": "string" }, "password": { "type": "string" }, "repoUrl": { "type": "string" }, "username": { "type": "string" } } }, "dto.NoteAppendRequest": { "type": "object", "required": [ "content", "path", "vault" ], "properties": { "content": { "description": "Content to append // 追加内容", "type": "string", "example": "Appended content" }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteDTO": { "type": "object", "properties": { "clientName": { "description": "Client name // 客户端名称", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "content": { "description": "Note content // 笔记内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Note path // 笔记路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "size": { "description": "Note size // 笔记大小", "type": "integer" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" }, "version": { "description": "Version number // 版本号", "type": "integer" } } }, "dto.NoteHistoryDTO": { "type": "object", "properties": { "clientName": { "description": "Client that made changes // 产生变更的客户端", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "content": { "description": "Full historical content // 完整历史内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Creation time of this version // 此版本的创建时间", "type": "string" }, "diffs": { "description": "Text differences // 文本差异内容", "type": "array", "items": { "$ref": "#/definitions/diffmatchpatch.Diff" } }, "id": { "description": "History entry ID // 历史项 ID", "type": "integer" }, "noteId": { "description": "Associated note ID // 笔记 ID", "type": "integer" }, "path": { "description": "Note path at that time // 当时的笔记路径", "type": "string" }, "vaultId": { "description": "Associated vault ID // 保险库 ID", "type": "integer" }, "version": { "description": "Historical version number // 历史版本号", "type": "integer" } } }, "dto.NoteHistoryRestoreRequest": { "type": "object", "required": [ "historyId", "vault" ], "properties": { "historyId": { "description": "History version ID // 历史版本 ID", "type": "integer", "example": 1 }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteLinkItem": { "type": "object", "properties": { "context": { "description": "Text context around link // 链接文本上下文", "type": "string" }, "isEmbed": { "description": "Is it an embed (![[...]]) // 是否为嵌入", "type": "boolean" }, "linkText": { "description": "Raw link text (optional) // 原始链接文本(可选)", "type": "string" }, "path": { "description": "Target path // 目标路径", "type": "string" } } }, "dto.NoteModifyOrCreateRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "baseHash": { "description": "Base hash for sync // 同步基准哈希", "type": "string", "example": "bhash789" }, "baseHashMissing": { "description": "Marks if baseHash is unavailable // 标记基准哈希是否缺失", "type": "boolean", "example": false }, "content": { "description": "Note content // 笔记内容", "type": "string", "example": "# Hello World" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string", "example": "chash012" }, "createOnly": { "description": "If true, fail if note already exists // 如果为 true,笔记已存在则失败", "type": "boolean", "example": false }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer", "example": 1700000000 }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer", "example": 1700000000 }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteMoveRequest": { "type": "object", "required": [ "destination", "path", "vault" ], "properties": { "destination": { "description": "Destination path // 目标路径", "type": "string", "example": "Folder/Source.md" }, "overwrite": { "description": "Overwrite existing // 覆盖现有", "type": "boolean", "example": false }, "path": { "description": "Current path // 当前路径", "type": "string", "example": "Source.md" }, "pathHash": { "description": "Current path hash // 当前路径哈希", "type": "string", "example": "src_hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteNoContentDTO": { "type": "object", "properties": { "clientName": { "description": "Client name // 客户端名称", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Note path // 笔记路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "size": { "description": "Note size // 笔记大小", "type": "integer" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" }, "version": { "description": "Version number // 版本号", "type": "integer" } } }, "dto.NotePatchFrontmatterRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "remove": { "description": "Fields to remove // 待移除字段", "type": "array", "items": { "type": "string" }, "example": [ "old_tag" ] }, "updates": { "description": "Fields to update // 待更新字段", "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NotePrependRequest": { "type": "object", "required": [ "content", "path", "vault" ], "properties": { "content": { "description": "Content to prepend // 头部添加内容", "type": "string", "example": "Prepended content\n" }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteRecycleClearRequest": { "type": "object", "required": [ "vault" ], "properties": { "path": { "description": "Note path, empty for all // 笔记路径,为空则清理全部", "type": "string", "example": "path/to/note.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteRenameRequest": { "type": "object", "required": [ "oldPath", "path", "vault" ], "properties": { "oldPath": { "description": "Old path // 旧路径", "type": "string", "example": "OldName.md" }, "oldPathHash": { "description": "Old path hash // 旧路径哈希", "type": "string", "example": "ohash456" }, "path": { "description": "New path // 新路径", "type": "string", "example": "NewName.md" }, "pathHash": { "description": "New path hash // 新路径哈希", "type": "string", "example": "nhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteReplaceRequest": { "type": "object", "required": [ "find", "path", "vault" ], "properties": { "all": { "description": "Replace all matches // 替换所有", "type": "boolean", "example": true }, "failIfNoMatch": { "description": "Fail if no match found // 若无匹配则失败", "type": "boolean", "example": true }, "find": { "description": "String to find // 查找内容", "type": "string", "example": "old text" }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "regex": { "description": "Use regex // 使用正则", "type": "boolean", "example": false }, "replace": { "description": "String to replace with // 替换内容", "type": "string", "example": "new text" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteReplaceResponse": { "type": "object", "properties": { "matchCount": { "description": "Number of matches found // 匹配数量", "type": "integer" }, "note": { "description": "Updated note data // 更新后的笔记数据", "allOf": [ { "$ref": "#/definitions/dto.NoteDTO" } ] } } }, "dto.NoteRestoreRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteWithFileLinksResponse": { "type": "object", "properties": { "content": { "description": "Note content // 笔记内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "fileLinks": { "description": "Map of file link to actual path // 文件链接到实际路径的映射", "type": "object", "additionalProperties": { "type": "string" } }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Note path // 笔记路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "updatedAt": { "description": "Updated at time // 更新时间" }, "version": { "description": "Version number // 版本号", "type": "integer" } } }, "dto.SettingDTO": { "type": "object", "properties": { "content": { "description": "Setting content // 配置内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "id": { "description": "Setting ID // 配置 ID", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Setting path // 配置路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希值", "type": "string" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" } } }, "dto.SettingDeleteRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Setting path // 配置路径", "type": "string", "example": "User/Theme" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.SettingModifyOrCreateRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "content": { "description": "Setting content // 配置内容", "type": "string", "example": "dark" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string", "example": "chash456" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer", "example": 1700000000 }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer", "example": 1700000000 }, "path": { "description": "Setting path // 配置路径", "type": "string", "example": "User/Theme" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.SettingRenameRequest": { "type": "object", "required": [ "newPath", "oldPath", "vault" ], "properties": { "newPath": { "description": "New path // 新路径", "type": "string", "example": "New/Path" }, "newPathHash": { "description": "New path hash // 新路径哈希", "type": "string", "example": "newhash456" }, "oldPath": { "description": "Old path // 旧路径", "type": "string", "example": "Old/Path" }, "oldPathHash": { "description": "Old path hash // 旧路径哈希", "type": "string", "example": "oldhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.ShareCancelRequest": { "type": "object", "required": [ "vault" ], "properties": { "id": { "description": "Share ID (optional) // 分享 ID (可选)", "type": "integer", "example": 1 }, "path": { "description": "Resource path (optional) // 资源路径 (可选)", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Resource path Hash (optional) // 资源路径哈希 (可选)", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "defaultVault" } } }, "dto.ShareCreateRequest": { "type": "object", "required": [ "path", "pathHash", "vault" ], "properties": { "password": { "description": "Share password // 分享密码", "type": "string", "example": "123456" }, "path": { "description": "Resource path // 资源路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Resource path Hash // 资源路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "defaultVault" } } }, "dto.ShareCreateResponse": { "type": "object", "properties": { "expiresAt": { "description": "Expiration time // 过期时间", "type": "string" }, "id": { "description": "ID of the note or file table (primary resource ID) // 笔记或文件表 ID(主资源 ID)", "type": "integer" }, "isPassword": { "description": "Whether password is set // 是否设置了密码", "type": "boolean" }, "shortLink": { "description": "Short link // 短链", "type": "string" }, "token": { "description": "Share Token // 分享 Token", "type": "string" }, "type": { "description": "Resource type: note or file // 资源类型:笔记(note)或文件(file)", "type": "string" } } }, "dto.ShareListItem": { "type": "object", "properties": { "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "expiresAt": { "description": "Expiration time // 过期时间", "type": "string" }, "id": { "description": "Share ID // 分享记录 ID", "type": "integer" }, "isPassword": { "description": "Whether password is set // 是否设置了密码", "type": "boolean" }, "lastViewedAt": { "description": "Last viewed time // 最后访问时间", "type": "string" }, "notePath": { "description": "Note path, for frontend share filter matching // 笔记路径,用于前端分享筛选匹配", "type": "string" }, "res": { "description": "Authorized resources // 资源授权列表", "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "shortLink": { "description": "Short link // 短链", "type": "string" }, "status": { "description": "Status: 1-Active, 2-Cancelled // 状态: 1-有效, 2-已撤销", "type": "integer" }, "title": { "description": "Resource title (note title or file name) // 资源标题(笔记标题或文件名)", "type": "string" }, "uid": { "description": "User ID // 用户 ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "url": { "description": "Share URL (path format: /id/token) // 分享 URL (路径格式: /id/token)", "type": "string" }, "vaultName": { "description": "Vault name where the note belongs // 笔记所属仓库名", "type": "string" }, "viewCount": { "description": "View count // 访问次数", "type": "integer" } } }, "dto.SharePasswordUpdateRequest": { "type": "object", "required": [ "path", "pathHash", "vault" ], "properties": { "password": { "description": "New password // 新密码", "type": "string", "example": "123456" }, "path": { "description": "Resource path // 资源路径", "type": "string", "example": "未命名.md" }, "pathHash": { "description": "Resource path Hash // 资源路径哈希", "type": "string", "example": "-677306325" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "test" } } }, "dto.ShareShortLinkCreateRequest": { "type": "object", "required": [ "path", "pathHash", "vault" ], "properties": { "is_force": { "description": "Whether to force regeneration // 是否强制重新生成", "type": "boolean", "example": false }, "path": { "description": "Path // 路径", "type": "string", "example": "notes/todo.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "..." }, "url": { "description": "Full share URL from client; if provided, used directly without regenerating token // 客户端传入的完整分享链接,非空时直接使用,不重新生成 token", "type": "string", "example": "https://example.com/share/129/CNmkmQlq0s-4elT3NuZG2w" }, "vault": { "description": "Vault name // 库名", "type": "string", "example": "work" } } }, "dto.StorageDTO": { "type": "object", "properties": { "accessKeyId": { "description": "Access key ID // 访问密钥 ID", "type": "string" }, "accessKeySecret": { "description": "Access key secret // 访问密钥秘密", "type": "string" }, "accessUrlPrefix": { "description": "Access URL prefix // 访问地址前缀", "type": "string" }, "accountId": { "description": "Account ID // 账户 ID", "type": "string" }, "bucketName": { "description": "Bucket name // 存储桶名称", "type": "string" }, "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "customPath": { "description": "Custom path // 自定义路径", "type": "string" }, "endpoint": { "description": "Endpoint // 访问端点", "type": "string" }, "id": { "description": "ID // ID", "type": "integer" }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean" }, "password": { "description": "Password // 密码", "type": "string" }, "region": { "description": "Region // 区域", "type": "string" }, "type": { "description": "Storage type // 存储类型", "type": "string" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "user": { "description": "Username // 用户名", "type": "string" } } }, "dto.StoragePostRequest": { "type": "object", "required": [ "accessUrlPrefix", "type" ], "properties": { "accessKeyId": { "description": "Access key ID // 访问密钥ID", "type": "string", "example": "" }, "accessKeySecret": { "description": "Access key secret // 访问密钥秘密", "type": "string", "example": "" }, "accessUrlPrefix": { "description": "Access URL prefix // 访问地址前缀", "type": "string", "maxLength": 100, "minLength": 2, "example": "https://cdn.com" }, "accountId": { "description": "Account ID (R2) // 账户ID r2", "type": "string", "example": "123456789" }, "bucketName": { "description": "Bucket name // 存储桶名称", "type": "string", "example": "my-bucket" }, "customPath": { "description": "Custom path // 自定义路径", "type": "string", "example": "/backups" }, "endpoint": { "description": "Endpoint (OSS) // 端点 oss", "type": "string", "example": "oss-cn-hangzhou.aliyuncs.com" }, "id": { "description": "ID // ID", "type": "integer", "example": 1 }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "integer", "example": 1 }, "password": { "description": "Password // 密码", "type": "string", "example": "secret_password" }, "region": { "description": "Region (S3) // 区域 s3", "type": "string", "example": "us-east-1" }, "type": { "description": "Storage type // 类型", "type": "string", "minLength": 1, "example": "local-fs" }, "user": { "description": "Username // 访问用户名", "type": "string", "example": "admin" } } }, "dto.SyncLogDTO": { "type": "object", "properties": { "action": { "description": "Action type // 操作类型", "type": "string" }, "changedFields": { "description": "Changed fields // 变更字段", "type": "string" }, "clientName": { "description": "Client name // 客户端名称", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "createdAt": { "description": "Log creation time // 创建时间", "type": "string" }, "message": { "description": "Additional message // 附加消息", "type": "string" }, "path": { "description": "Resource path // 资源路径", "type": "string" }, "pathHash": { "description": "Resource path hash // 路径哈希", "type": "string" }, "size": { "description": "Size in bytes // 大小(字节)", "type": "integer" }, "status": { "description": "Status: 1 success, 2 failed // 状态", "type": "integer" }, "type": { "description": "Resource type // 资源类型", "type": "string" }, "vaultId": { "description": "Vault ID // 笔记本 ID", "type": "integer" } } }, "dto.UserChangePasswordRequest": { "type": "object", "required": [ "confirmPassword", "oldPassword", "password" ], "properties": { "confirmPassword": { "description": "Confirm password // 校验密码", "type": "string", "example": "new_password123" }, "oldPassword": { "description": "Old password // 旧密码", "type": "string", "example": "old_password123" }, "password": { "description": "New password // 新密码", "type": "string", "example": "new_password123" } } }, "dto.UserCreateRequest": { "type": "object", "required": [ "confirmPassword", "email", "password", "username" ], "properties": { "confirmPassword": { "description": "Confirm password // 校验密码", "type": "string", "example": "password123" }, "email": { "description": "User email // 用户邮件", "type": "string", "example": "user@example.com" }, "password": { "description": "User password // 用户密码", "type": "string", "example": "password123" }, "username": { "description": "User name // 用户名", "type": "string", "example": "username123" } } }, "dto.UserDTO": { "type": "object", "properties": { "avatar": { "description": "Avatar URL or handle // 头像路径或名称", "type": "string" }, "createdAt": { "description": "Account created time // 账号创建时间", "type": "string" }, "email": { "description": "Email address // 邮件地址", "type": "string" }, "token": { "description": "Authentication Token // 认证 Token", "type": "string" }, "uid": { "description": "User ID (primary key) // 用户唯一标识(主键)", "type": "integer" }, "updatedAt": { "description": "Last updated time // 最后更新时间", "type": "string" }, "username": { "description": "Username // 用户名", "type": "string" } } }, "dto.UserLoginRequest": { "type": "object", "required": [ "credentials", "password" ], "properties": { "credentials": { "description": "Username or Email // 登录凭证(用户名或邮件)", "type": "string", "example": "user@example.com" }, "password": { "description": "Password // 密码", "type": "string", "example": "password123" } } }, "dto.VaultDTO": { "type": "object", "properties": { "createdAt": { "description": "Creation time // 创建时间", "type": "string" }, "fileCount": { "description": "Number of files // 文件数量", "type": "integer" }, "fileSize": { "description": "Size of files // 文件大小", "type": "integer" }, "id": { "description": "Vault ID // 保险库 ID", "type": "integer" }, "noteCount": { "description": "Number of notes // 笔记数量", "type": "integer" }, "noteSize": { "description": "Size of notes // 笔记大小", "type": "integer" }, "size": { "description": "Total size // 总大小", "type": "integer" }, "updatedAt": { "description": "Updated time // 更新时间", "type": "string" }, "vault": { "description": "Vault name // 保险库名称", "type": "string" } } }, "dto.VaultPostRequest": { "type": "object", "required": [ "vault" ], "properties": { "id": { "description": "Vault ID (optional for update) // 保险库 ID(可选,用于更新)", "type": "integer", "example": 1 }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.VersionDTO": { "type": "object", "properties": { "buildTime": { "description": "Build time // 构建时间", "type": "string" }, "gitTag": { "description": "Git tag // Git 标签", "type": "string" }, "pluginVersionNewChangelog": { "description": "New plugin version changelog link // 插件新版本更新日志链接", "type": "string" }, "pluginVersionNewChangelogContent": { "description": "New plugin version changelog content // 插件新版本更新日志内容", "type": "string" }, "pluginVersionNewLink": { "description": "New plugin version link // 插件新版本链接", "type": "string" }, "pluginVersionNewName": { "description": "New plugin version name // 插件新版本名称", "type": "string" }, "version": { "description": "Current version // 当前版本", "type": "string" }, "versionIsNew": { "description": "Is there a new version // 是否有新版本", "type": "boolean" }, "versionNewChangelog": { "description": "New version changelog link // 新版本更新日志链接", "type": "string" }, "versionNewChangelogContent": { "description": "New version changelog content // 新版本更新日志内容", "type": "string" }, "versionNewLink": { "description": "New version download link // 新版本下载链接", "type": "string" }, "versionNewName": { "description": "New version name // 新版本名称", "type": "string" } } } }, "securityDefinitions": { "ShareAuthToken": { "type": "apiKey", "name": "Share-Token", "in": "header" }, "UserAuthToken": { "type": "apiKey", "name": "token", "in": "header" } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "localhost:9000", BasePath: "/", Schemes: []string{}, Title: "Fast Note Sync Service HTTP API", Description: "This is the Fast Note Sync Service HTTP API.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", RightDelim: "}}", } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: docs/skills/fns-mcp/SKILL.md ================================================ --- name: fns-mcp description: Fast Note Sync Service MCP SSE Skill (Bilingual). Allows agents to access and manage notes, files, and vaults via the MCP protocol. version: 1.0.0 --- # Fast Note Sync (FNS) MCP Skill (Bilingual/双语版) This skill allows agents to interact with the Fast Note Sync Service using the Model Context Protocol (MCP). 本技能允许 Agent 通过 Model Context Protocol (MCP) 与 Fast Note Sync Service 交互。 ## Core Capabilities / 核心能力 - **Note Management / 笔记管理**: Listing, searching, CRUD, moving, renaming, restoring, and clearing recycle bin. (列表、检索、增删改查、移动、重命名、恢复、清理回收站) - **Note Enhancement / 笔记增强**: Frontmatter patching, appending/prepending, find & replace, and backlinks/outlinks. (Frontmatter 修补、追加/预置、查找替换、双链/外链) - **File Management / 文件管理**: Attachment listing, metadata retrieval, reading content (Base64), renaming, and deletion. (附件列表、元数据、读取内容、重命名、删除) - **Vault Management / 库管理**: Vault listing, creating/updating, and deletion. (库列表、创建/更新、删除) ## Configuration Guide / 配置指南 Before using this skill, the agent needs to connect to the MCP SSE interface. 在使用此技能前,Agent 需要连接到 MCP SSE 接口。 ### Interface Details / 接口信息 - **SSE Endpoint**: `http://:9000/api/mcp/sse` - **Message Endpoint**: `http://:9000/api/mcp/message` - **Authentication**: Requires `Authorization: Bearer ` in headers. - **Client Identity (Optional)**: - `X-Client`: `` (e.g., `CherryStudio`, `OpenClaw`) - `X-Client-Name`: `` (e.g., `MyAgent`) - `X-Client-Version`: `` - **Default Vault (Optional)**: `X-Default-Vault-Name: ` ### Platform Specific Config / 平台配置示例 - [OpenClaw Configuration](configs/openclaw.json) - [Hermes Agent Configuration](configs/hermes.yaml) - [Cherry Studio Configuration Guide / 配置指南](configs/cherry-studio.md) --- ## Available Tools / 可用工具说明 ### 1. Note Tools / 笔记工具 | Tool Name / 工具 | Description / 描述 | Arguments / 参数 | | :--- | :--- | :--- | | `note_list` | List notes in a vault / 列出笔记 | `vault`, `keyword` | | `note_get` | Get note content / 获取笔记内容 | `vault`, `path` | | `note_create_or_update` | Create/Update note / 创建或更新笔记 | `vault`, `path`, `content` | | `note_delete` | Delete note / 删除笔记 | `vault`, `path` | | `note_rename` | Rename note / 重命名笔记 | `vault`, `oldPath`, `newPath` | | `note_restore` | Restore note / 恢复笔记 | `vault`, `path` | | `note_append` | Append content / 追加内容 | `vault`, `path`, `content` | | `note_prepend` | Prepend content / 预置内容 | `vault`, `path`, `content` | | `note_replace` | Find & Replace / 查找替换 | `vault`, `path`, `find`, `replace`, `regex`, `all` | | `note_patch_frontmatter` | Patch Frontmatter / 修补前置参数 | `vault`, `path`, `updates` (JSON), `remove` (JSON) | | `note_get_backlinks` | Get backlinks / 获取反链 | `vault`, `path` | | `note_get_outlinks` | Get outlinks / 获取出链 | `vault`, `path` | ### 2. File Tools / 文件工具 | Tool Name / 工具 | Description / 描述 | Arguments / 参数 | | :--- | :--- | :--- | | `file_list` | List files / 列出文件 | `vault`, `keyword` | | `file_get_info` | Get metadata / 获取元数据 | `vault`, `path` | | `file_read` | Read content (Base64) / 读取内容 | `vault`, `path` | | `file_delete` | Delete file / 删除文件 | `vault`, `path` | | `file_rename` | Rename file / 重命名文件 | `vault`, `oldPath`, `newPath` | ### 3. Vault Tools / 库工具 | Tool Name / 工具 | Description / 描述 | Arguments / 参数 | | :--- | :--- | :--- | | `vault_list` | List vaults / 列出库 | None | | `vault_get` | Get vault details / 获取详情 | `id` | | `vault_create_or_update` | Create/Update vault / 创建或更新 | `vault`, `id` | | `vault_delete` | Delete vault / 删除库 | `id` | --- ## Best Practices / 最佳实践 1. **Vault Selection**: Explicitly specify `vault` when possible. (尽可能明确指定 `vault` 参数) 2. **Path Handling**: Paths are relative to vault root. (路径相对于库根目录) 3. **Encoding**: `file_read` returns Base64. (文件读取返回 Base64 编码) 4. **Consistency**: Be aware of path changes after rename/move. (重命名后注意路径变更) ================================================ FILE: docs/skills/fns-mcp/configs/cherry-studio.md ================================================ # Cherry Studio MCP Configuration Guide / 配置指南 Follow these steps to use Fast Note Sync Service in Cherry Studio: 按照以下步骤在 Cherry Studio 中使用 Fast Note Sync Service: ### 1. Open MCP Settings / 进入 MCP 设置 - Open **Cherry Studio**. - Click **Settings** (gear icon) / 点击 **设置**(齿轮图标)。 - Select **MCP Servers** / 选择 **MCP 服务器**。 ### 2. Add New Server / 添加服务器 - Click **Add Server** / 点击 **添加服务器**。 - Fill out the form / 填写以下内容: - **Name / 名称**: `FNS-Service` - **Type / 类型**: `SSE (Server-Sent Events)` - **URL**: `http://:9000/api/mcp/sse` - **Headers / 请求头**: - **Key**: `Authorization`, **Value**: `Bearer ` - (Optional) **Key**: `X-Client`, **Value**: `CherryStudio` - (Optional) **Key**: `X-Client-Name`, **Value**: `Cherry Agent` - (Optional) **Key**: `X-Client-Version`, **Value**: `1.0.0` - (Optional) **Key**: `X-Default-Vault-Name`, **Value**: `Default` ### 3. Save and Enable / 保存并启用 - Click **Confirm** / 点击 **确定**。 - Ensure it is **Enabled** / 确保处于 **启用** 状态。 ### 4. Use in Chat / 在聊天中使用 - Start a new chat with a model supporting Tool Calling. - Confirm tools from `FNS-Service` are selected. - Now you can say: "List my notes" or "在库中搜索 X"。 ================================================ FILE: docs/skills/fns-mcp/configs/hermes.yaml ================================================ mcp_servers: fns-service: url: "http://:9000/api/mcp/sse" headers: Authorization: "Bearer " X-Default-Vault-Name: "Default" X-Client: "Hermes" X-Client-Name: "Hermes Agent" X-Client-Version: "1.0.0" ================================================ FILE: docs/skills/fns-mcp/configs/openclaw.json ================================================ { "mcpServers": { "fns-service": { "url": "http://:9000/api/mcp/sse", "headers": { "Authorization": "Bearer ", "X-Default-Vault-Name": "Default", "X-Client": "OpenClaw", "X-Client-Name": "OpenClaw Agent", "X-Client-Version": "1.0.0" } } } } ================================================ FILE: docs/swagger.json ================================================ { "swagger": "2.0", "info": { "description": "This is the Fast Note Sync Service HTTP API.", "title": "Fast Note Sync Service HTTP API", "contact": { "name": "Haierkeys", "url": "https://github.com/haierkeys", "email": "haierkeys@gmail.com" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "1.0" }, "host": "localhost:9000", "basePath": "/", "paths": { "/api/admin/check": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Check if the current logged-in user has system admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Check admin permission", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminCheckResponse" } } } ] } } } } }, "/api/admin/cloudflared_tunnel_download": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Trigger the download of cloudflared binary for the current platform", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Download cloudflared binary", "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get full system configuration information, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get full admin config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify full system configuration information, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update admin config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/cloudflare": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get Cloudflare tunnel configuration, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get Cloudflare config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminCloudflareConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify Cloudflare tunnel configuration, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update Cloudflare config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminCloudflareConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminCloudflareConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/ngrok": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get Ngrok tunnel configuration, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get Ngrok config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminNgrokConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify Ngrok tunnel configuration, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update Ngrok config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminNgrokConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminNgrokConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/user_database": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get user database configuration information, requires admin privileges", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get user database config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Modify user database configuration information, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Update user database config", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/config/user_database/test": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Test if the provided database configuration can connect successfully, requires admin privileges", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Test user database connection", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Config Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.AdminUserDatabaseConfig" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Connection failed", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/gc": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Manually run Go runtime GC and release memory to OS, requires admin privileges", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Trigger manual GC", "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/restart": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Gracefully restart the server", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Trigger server restart", "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/system/info": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get server runtime, CPU, memory, host and process info, requires admin privileges", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get system stats", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminSystemInfo" } } } ] } } } } }, "/api/admin/upgrade": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Download latest version and restart server", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Trigger server upgrade", "parameters": [ { "type": "string", "description": "Version to upgrade (e.g. 2.0.10 or latest)", "name": "version", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/admin/ws_clients": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get a list of all current WebSocket connections, requires admin privileges", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get connected WebSocket clients", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/app.WSClientInfo" } } } } ] } }, "403": { "description": "Insufficient privileges", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/config": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Update backup configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Backup Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.BackupConfigRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.BackupConfigDTO" } } } ] } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Delete backup configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "ID // ID", "name": "id", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/configs": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Get backup configurations", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.BackupConfigDTO" } } } } ] } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/execute": { "post": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Trigger a backup manually", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Backup Execute Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.BackupExecuteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/backup/historys": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Backup" ], "summary": "Get backup history list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "Config ID // 配置 ID", "name": "configId", "in": "query", "required": true }, { "type": "integer", "example": 1, "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "example": 10, "description": "Page size // 每页大小", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.BackupHistoryDTO" } } } } ] } } } ] } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/file": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get raw binary data of an attachment by path, supports strong cache control", "produces": [ "application/octet-stream" ], "tags": [ "File" ], "summary": "Get attachment content", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "Image.png", "description": "File path // 文件路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "type": "file" } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently delete a specific attachment record and its physical file", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Delete attachment", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Image.png", "description": "File path // 文件路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query", "required": true }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/file/info": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get attachment metadata (FileDTO) by path", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Get attachment info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "Image.png", "description": "File path // 文件路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/file/recycle-clear": { "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently clear selected files from recycle bin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Clear recycle bin", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Clear Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FileRecycleClearRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/file/rename": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Rename an attachment to a new path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Rename attachment", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Rename Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FileRenameRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/file/restore": { "put": { "security": [ { "UserAuthToken": [] } ], "description": "Restore deleted attachment from trash", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Restore attachment", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Restore Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FileRestoreRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FileDTO" } } } ] } } } } }, "/api/files": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get attachment list for current user with pagination, search, filter, and sort support", "produces": [ "application/json" ], "tags": [ "File" ], "summary": "Get file list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "vacation", "description": "Search keyword // 搜索关键词", "name": "keyword", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.FileDTO" } } } } ] } } } ] } } } } }, "/api/folder": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get folder info for current user by path or pathHash", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Get folder info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects/Work", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FolderDTO" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Create a new folder or restore a deleted one by path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Create folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Create Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FolderCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FolderDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Soft delete a folder by path or pathHash", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Delete folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Delete Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.FolderDeleteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/folder/files": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "List non-deleted files in a specific folder with pagination and sorting", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "List files in folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.FileDTO" } } } } ] } } } ] } } } } }, "/api/folder/notes": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "List non-deleted notes in a specific folder with pagination and sorting", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "List notes in folder", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteDTO" } } } } ] } } } ] } } } } }, "/api/folder/tree": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get the complete folder tree structure for a vault", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Get folder tree", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "example": 3, "description": "Tree depth // 树深度", "name": "depth", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.FolderTreeResponse" } } } ] } } } } }, "/api/folders": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get folder list for current user by parent path or pathHash", "produces": [ "application/json" ], "tags": [ "Folder" ], "summary": "Get folder list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "Projects", "description": "Folder path // 文件夹路径", "name": "path", "in": "query" }, { "type": "string", "example": "fhash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.FolderDTO" } } } } ] } } } } }, "/api/git-sync/config": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Update git sync configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Git Sync Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncConfigRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.GitSyncConfigDTO" } } } ] } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Delete git sync configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Git Sync ID", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncDeleteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/config/clean": { "delete": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Clean local git workspace", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Clean Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncCleanRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/config/execute": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Trigger a manual git sync", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Execute Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncExecuteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/configs": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Get git sync configurations", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.GitSyncConfigDTO" } } } } ] } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/histories": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Get git sync histories", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "name": "configId", "in": "query" }, { "type": "integer", "name": "page", "in": "query" }, { "type": "integer", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.GitSyncHistoryDTO" } } } } ] } } } ] } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/git-sync/validate": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "GitSync" ], "summary": "Validate git sync parameters", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Validation Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.GitSyncValidateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/health": { "get": { "description": "Check service health status, including database connection", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Health check", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/api_router.HealthResponse" } } } } }, "/api/note": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get specific note content and metadata by path or path hash", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get note details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteWithFileLinksResponse" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Handle note creation, modification, or renaming (identified by path change)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Create or update note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Note Content", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteModifyOrCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Move note to trash", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Delete note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/append": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Append content to the end of a note", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Append content to note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Append Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteAppendRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/backlinks": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all other notes that link to the specified note", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get backlinks", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteLinkItem" } } } } ] } } } } }, "/api/note/frontmatter": { "patch": { "security": [ { "UserAuthToken": [] } ], "description": "Update or delete note frontmatter fields", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Modify note frontmatter", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Frontmatter Modification Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NotePatchFrontmatterRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/histories": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all history records for a specific note with pagination", "produces": [ "application/json" ], "tags": [ "Note History" ], "summary": "Get note history list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteHistoryDTO" } } } } ] } } } ] } } } } }, "/api/note/history": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get specific note history content by history record ID", "produces": [ "application/json" ], "tags": [ "Note History" ], "summary": "Get note history details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "format": "int64", "description": "History Record ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteHistoryDTO" } } } ] } } } } }, "/api/note/history/restore": { "put": { "security": [ { "UserAuthToken": [] } ], "description": "Restore note content to a specific history version", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note History" ], "summary": "Restore note from history", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Restore Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteHistoryRestoreRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/move": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Move a note to a new path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Move note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Move Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteMoveRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/outlinks": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get other notes that the specified note links to", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get outgoing links", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Note path // 笔记路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteLinkItem" } } } } ] } } } } }, "/api/note/prepend": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Insert content at the beginning of a note (after frontmatter)", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Prepend content to note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Prepend Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NotePrependRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/recycle-clear": { "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently clear selected notes from recycle bin", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Clear recycle bin", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Clear Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteRecycleClearRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/note/rename": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Rename a note to a new path", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Rename note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Rename Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteRenameRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/note/replace": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Perform find and replace operation in a note, supporting regular expressions", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Find and replace in note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Find and Replace Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteReplaceRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteReplaceResponse" } } } ] } } } } }, "/api/note/restore": { "put": { "security": [ { "UserAuthToken": [] } ], "description": "Restore deleted note from trash", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Restore note", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Restore Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.NoteRestoreRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/notes": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get note list for current user with pagination", "produces": [ "application/json" ], "tags": [ "Note" ], "summary": "Get note list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "boolean", "example": false, "description": "Is in recycle bin // 是否在回收站", "name": "isRecycle", "in": "query" }, { "type": "string", "example": "todo", "description": "Search keyword // 搜索关键词", "name": "keyword", "in": "query" }, { "type": "string", "example": "note1.md,note2.md", "description": "Comma-separated exact path list for share filter // 逗号分隔的精确路径列表,用于分享筛选", "name": "paths", "in": "query" }, { "type": "boolean", "example": true, "description": "Whether to search content // 是否搜索内容", "name": "searchContent", "in": "query" }, { "type": "string", "example": "content", "description": "Search mode (path, content) // 搜索模式(路径、内容)", "name": "searchMode", "in": "query" }, { "type": "string", "example": "mtime", "description": "Sort by field // 排序字段", "name": "sortBy", "in": "query" }, { "type": "string", "example": "desc", "description": "Sort order // 排序顺序", "name": "sortOrder", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.NoteNoContentDTO" } } } } ] } } } ] } } } } }, "/api/notes/share-paths": { "get": { "security": [ { "UserAuthToken": [] } ], "tags": [ "Share" ], "summary": "Get active shared note paths", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "description": "Vault name", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "string" } } } } ] } } } } }, "/api/setting": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get setting info for current user by path or pathHash", "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Get setting info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "User/Theme", "description": "Setting path // 配置路径", "name": "path", "in": "query" }, { "type": "string", "example": "hash123", "description": "Path hash // 路径哈希", "name": "pathHash", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.SettingDTO" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Create a new setting or update an existing one", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Create or update setting", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Create/Update Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SettingModifyOrCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.SettingDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Soft delete a setting by path or pathHash", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Delete setting", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Delete Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SettingDeleteRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/setting/rename": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Rename a setting and update its path and pathHash", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Rename setting", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Rename Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SettingRenameRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.SettingDTO" } } } ] } } } } }, "/api/settings": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get setting list for current user with pagination and keyword filtering", "produces": [ "application/json" ], "tags": [ "Setting" ], "summary": "Get setting list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "User/", "description": "Keyword // 关键词", "name": "keyword", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.SettingDTO" } } } } ] } } } ] } } } } }, "/api/share": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get share token and info by vault and path", "tags": [ "Share" ], "summary": "Query share by path", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "ReadMe.md", "description": "Resource path // 资源路径", "name": "path", "in": "query", "required": true }, { "type": "string", "example": "hash123", "description": "Resource path Hash // 资源路径哈希", "name": "pathHash", "in": "query", "required": true }, { "type": "string", "example": "defaultVault", "description": "Vault name // 保险库名称", "name": "vault", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.ShareCreateResponse" } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Create a share token for a specific note or attachment, automatically resolve attachment references and authorize", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Create resource share", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Share Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.ShareCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.ShareCreateResponse" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Cancel a share by ID or path parameters", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Cancel share", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Cancel Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.ShareCancelRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/share/file": { "get": { "security": [ { "ShareAuthToken": [] } ], "description": "Get raw binary data of a specific attachment via share token", "produces": [ "application/octet-stream" ], "tags": [ "Share" ], "summary": "Get shared attachment content", "parameters": [ { "type": "string", "description": "Auth Token", "name": "Share-Token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "Resource ID // 资源 ID", "name": "id", "in": "query", "required": true }, { "type": "string", "example": "123456", "description": "Share password // 分享密码", "name": "password", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "type": "file" } } } } }, "/api/share/note": { "get": { "security": [ { "ShareAuthToken": [] } ], "description": "Get specific note content (restricted read-only access) via share token", "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Get shared note details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "Share-Token", "in": "header", "required": true }, { "type": "integer", "example": 1, "description": "Resource ID // 资源 ID", "name": "id", "in": "query", "required": true }, { "type": "string", "example": "123456", "description": "Share password // 分享密码", "name": "password", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.NoteDTO" } } } ] } } } } }, "/api/share/password": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Set or update password for a share record", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Update share password", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Update Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.SharePasswordUpdateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/share/short_link": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Call sink.cool API to generate a short link for a given share record", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "Create short link for share", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Short Link Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.ShareShortLinkCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "string" } } } ] } } } } }, "/api/shares": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all active and inactive shares of the user, supports sorting and pagination", "produces": [ "application/json" ], "tags": [ "Share" ], "summary": "List shares", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "description": "Sort field: created_at, updated_at, expires_at (default: created_at)", "name": "sort_by", "in": "query" }, { "type": "string", "description": "Sort direction: asc or desc (default: desc)", "name": "sort_order", "in": "query" }, { "type": "integer", "description": "Page number", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.ShareListItem" } } } } ] } } } ] } } } } }, "/api/storage": { "get": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Get storage configuration list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.StorageDTO" } } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Create or update storage configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Storage Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.StoragePostRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.StorageDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Delete storage configuration", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "format": "int64", "description": "Storage ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/storage/enabled_types": { "get": { "description": "Get list of enabled storage types. Possible values: localfs, oss, s3, r2, minio, webdav", "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Get enabled storage types", "responses": { "200": { "description": "Success. Data contains: localfs, oss, s3, r2, minio, webdav", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "string" } } } } ] } } } } }, "/api/storage/validate": { "post": { "security": [ { "UserAuthToken": [] } ], "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Storage" ], "summary": "Validate storage connection", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Storage Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.StoragePostRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Params", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Token Required", "schema": { "$ref": "#/definitions/app.Res" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/support": { "get": { "description": "Get support records for the specified language with pagination and sorting", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get support records", "parameters": [ { "type": "string", "description": "Language code (default: en)", "name": "lang", "in": "query" }, { "type": "string", "description": "Sort by field (amount, time, name, item)", "name": "sortBy", "in": "query" }, { "type": "string", "description": "Sort order (asc, desc)", "name": "sortOrder", "in": "query" }, { "type": "integer", "description": "Page number", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/app.ListRes" } } } ] } } } } }, "/api/sync-logs": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get sync log list for current user with optional type/action filters and pagination", "produces": [ "application/json" ], "tags": [ "Sync Log" ], "summary": "Get sync log list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "string", "example": "modify", "description": "Action type // 操作类型", "name": "action", "in": "query" }, { "type": "string", "example": "note", "description": "Resource type: note / file / setting / folder // 资源类型", "name": "type", "in": "query" }, { "type": "string", "example": "MyVault", "description": "Vault name (optional filter) // 保险库名称(可选过滤)", "name": "vault", "in": "query" }, { "type": "integer", "description": "Page number // 页码", "name": "page", "in": "query" }, { "type": "integer", "description": "Page size // 每页数量", "name": "pageSize", "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/definitions/app.ListRes" }, { "type": "object", "properties": { "list": { "type": "array", "items": { "$ref": "#/definitions/dto.SyncLogDTO" } } } } ] } } } ] } } } } }, "/api/user/change_password": { "post": { "security": [ { "UserAuthToken": [] } ], "description": "Handle password change request for current user, validate old password and update new password.\n处理当前用户的修改密码请求,验证旧密码并更新新密码。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "Change user password", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Change Password Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.UserChangePasswordRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } }, "400": { "description": "Invalid Parameters / Old Password Incorrect", "schema": { "$ref": "#/definitions/app.Res" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/user/info": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Handle request to get current user info.\n处理获取当前用户信息的请求。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "Get user info", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.UserDTO" } } } ] } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/user/login": { "post": { "description": "Handle user login HTTP request, validate parameters and return auth token.\n处理用户登录 HTTP 请求,验证参数并返回认证 Token。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "User login", "parameters": [ { "description": "Login Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.UserLoginRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.UserDTO" } } } ] } }, "400": { "description": "Invalid Parameters / Invalid Credentials", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/user/register": { "post": { "description": "Handle user registration HTTP request, validate parameters and call UserService. Registration may be disabled in server settings.\n处理用户注册 HTTP 请求,验证参数并调用 UserService。注册功能可能在服务器设置中被禁用。", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "User" ], "summary": "User registration", "parameters": [ { "description": "Register Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.UserCreateRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.UserDTO" } } } ] } }, "400": { "description": "Invalid Parameters / Registration Disabled / User Already Exists", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/vault": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get all note vaults for current user", "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Get vault list", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/definitions/dto.VaultDTO" } } } } ] } } } }, "post": { "security": [ { "UserAuthToken": [] } ], "description": "Be used to create a new vault or update an existing vault configuration based on the ID in the request parameters", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Create or update vault", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "description": "Vault Parameters", "name": "params", "in": "body", "required": true, "schema": { "$ref": "#/definitions/dto.VaultPostRequest" } } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.VaultDTO" } } } ] } } } }, "delete": { "security": [ { "UserAuthToken": [] } ], "description": "Permanently delete a specific note vault and all associated notes and attachments", "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Delete vault", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "minimum": 1, "type": "integer", "example": 1, "description": "Vault ID // 保险库 ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/app.Res" } } } } }, "/api/vault/get": { "get": { "security": [ { "UserAuthToken": [] } ], "description": "Get specific vault configuration details by vault ID", "produces": [ "application/json" ], "tags": [ "Vault" ], "summary": "Get vault details", "parameters": [ { "type": "string", "description": "Auth Token", "name": "token", "in": "header", "required": true }, { "type": "integer", "format": "int64", "description": "Vault ID", "name": "id", "in": "query", "required": true } ], "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.VaultDTO" } } } ] } } } } }, "/api/version": { "get": { "description": "Get current server software version, Git tag, and build time", "produces": [ "application/json" ], "tags": [ "System" ], "summary": "Get server version info", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.VersionDTO" } } } ] } } } } }, "/api/webgui/config": { "get": { "description": "Get non-sensitive configuration required for frontend display, such as font settings, registration status, etc.", "produces": [ "application/json" ], "tags": [ "Config" ], "summary": "Get WebGUI basic config", "responses": { "200": { "description": "Success", "schema": { "allOf": [ { "$ref": "#/definitions/app.Res" }, { "type": "object", "properties": { "data": { "$ref": "#/definitions/dto.AdminWebGUIConfig" } } } ] } } } } } }, "definitions": { "api_router.HealthResponse": { "type": "object", "properties": { "database": { "description": "\"connected\" or \"error\" // \"connected\" 或 \"error\"", "type": "string" }, "status": { "description": "\"healthy\" or \"unhealthy\" // \"healthy\" 或 \"unhealthy\"", "type": "string" }, "uptime": { "description": "Uptime (seconds) // 运行时间(秒)", "type": "number" }, "version": { "description": "Service version number // 服务版本号", "type": "string" } } }, "app.ListRes": { "type": "object", "properties": { "list": { "description": "Data list // 数据清单" }, "pager": { "description": "Pagination info // 翻页信息", "allOf": [ { "$ref": "#/definitions/app.Pager" } ] } } }, "app.Pager": { "type": "object", "properties": { "page": { "description": "Page number // 页码", "type": "integer" }, "pageSize": { "description": "Page size // 每页数量", "type": "integer" }, "totalRows": { "description": "Total rows // 总行数", "type": "integer" } } }, "app.Res": { "type": "object", "properties": { "code": { "type": "integer" }, "context": {}, "data": {}, "details": {}, "message": {}, "status": { "type": "boolean" }, "vault": {} } }, "app.WSClientInfo": { "type": "object", "properties": { "clientName": { "type": "string" }, "clientType": { "type": "string" }, "clientVersion": { "type": "string" }, "nickname": { "type": "string" }, "platformInfo": { "type": "object", "additionalProperties": { "type": "boolean" } }, "remoteAddr": { "type": "string" }, "startTime": { "type": "string" }, "traceId": { "type": "string" }, "uid": { "type": "string" } } }, "diffmatchpatch.Diff": { "type": "object", "properties": { "text": { "type": "string" }, "type": { "$ref": "#/definitions/diffmatchpatch.Operation" } } }, "diffmatchpatch.Operation": { "type": "integer", "format": "int32", "enum": [ -1, 1, 0 ], "x-enum-varnames": [ "DiffDelete", "DiffInsert", "DiffEqual" ] }, "dto.AdminCPUInfo": { "type": "object", "properties": { "loadAvg": { "description": "Load average // 平均负载", "allOf": [ { "$ref": "#/definitions/dto.AdminLoadInfo" } ] }, "logicalCores": { "description": "Logical cores // 逻辑核心数", "type": "integer" }, "modelName": { "description": "Model name // 型号", "type": "string" }, "percent": { "description": "Usage percentage per core // 每个核心的使用率", "type": "array", "items": { "type": "number" } }, "physicalCores": { "description": "Physical cores // 物理核心数", "type": "integer" } } }, "dto.AdminCheckResponse": { "type": "object", "properties": { "isAdmin": { "description": "Whether have admin privileges // 是否具有管理员权限", "type": "boolean" } } }, "dto.AdminCloudflareConfig": { "type": "object", "properties": { "enabled": { "description": "Whether to enable cloudflare tunnel // 是否启用 cloudflare 隧道", "type": "boolean" }, "logEnabled": { "description": "Whether to enable cloudflare tunnel logging // 是否开启 cloudflare 隧道日志", "type": "boolean" }, "token": { "description": "cloudflare tunnel token // cloudflare 隧道令牌", "type": "string" } } }, "dto.AdminConfig": { "type": "object", "properties": { "adminUid": { "description": "Admin UID // 管理员 UID", "type": "integer" }, "authTokenKey": { "description": "Auth token key // 认证 Token 密钥", "type": "string" }, "defaultApiFolder": { "description": "Default API folder // 默认 API 目录", "type": "string" }, "fileChunkSize": { "description": "File chunk size // 文件分块大小", "type": "string" }, "fontSet": { "description": "Font set // 字体设置", "type": "string" }, "historyKeepVersions": { "description": "History versions to keep // 历史版本保留数", "type": "integer" }, "historySaveDelay": { "description": "History save delay // 历史保存延迟", "type": "string" }, "pullSource": { "description": "Data pull source: auto | github | cnb // 数据拉取源:auto | github | cnb", "type": "string" }, "registerIsEnable": { "description": "Registration enablement // 是否开启注册", "type": "boolean" }, "shareTokenExpiry": { "description": "Share token expiry // 分享 Token 有效期", "type": "string" }, "shareTokenKey": { "description": "Share token key // 分享 Token 密钥", "type": "string" }, "softDeleteRetentionTime": { "description": "Soft delete retention time // 软删除保留时间", "type": "string" }, "tokenExpiry": { "description": "Token expiry // Token 有效期", "type": "string" }, "uploadSessionTimeout": { "description": "Upload session timeout // 上传会话超时时间", "type": "string" } } }, "dto.AdminHostInfo": { "type": "object", "properties": { "arch": { "description": "Architecture // 架构", "type": "string" }, "currentTime": { "description": "Current system time // 当前系统时间", "type": "string" }, "hostname": { "description": "Hostname // 主机名", "type": "string" }, "kernelVersion": { "description": "Kernel version // 内核版本", "type": "string" }, "os": { "description": "Operating system // 操作系统", "type": "string" }, "osPretty": { "description": "Detailed OS name // 详细操作系统名称", "type": "string" }, "platform": { "description": "Platform name // 平台", "type": "string" }, "timezone": { "description": "Time zone name // 时区名称", "type": "string" }, "timezoneOffset": { "description": "Time zone offset in seconds // 时区偏移(秒)", "type": "integer" }, "uptime": { "description": "System uptime // 系统运行时间", "type": "integer" } } }, "dto.AdminLoadInfo": { "type": "object", "properties": { "load1": { "description": "Load 1 min // 1分钟负载", "type": "number" }, "load15": { "description": "Load 15 min // 15分钟负载", "type": "number" }, "load5": { "description": "Load 5 min // 5分钟负载", "type": "number" } } }, "dto.AdminMemoryInfo": { "type": "object", "properties": { "available": { "description": "Available memory // 可用内存", "type": "integer" }, "swapTotal": { "description": "Total swap space // 交换区总量", "type": "integer" }, "swapUsed": { "description": "Used swap space // 交换区已用", "type": "integer" }, "swapUsedPercent": { "description": "Swap usage percentage // 交换区使用率", "type": "number" }, "total": { "description": "Total physical memory // 系统总内存", "type": "integer" }, "used": { "description": "Used memory // 已用内存", "type": "integer" }, "usedPercent": { "description": "Memory usage percentage // 内存使用率", "type": "number" } } }, "dto.AdminNgrokConfig": { "type": "object", "properties": { "authToken": { "description": "ngrok auth token // ngrok 认证令牌", "type": "string" }, "domain": { "description": "Custom domain // 自定义域名", "type": "string" }, "enabled": { "description": "Whether to enable ngrok tunnel // 是否启用 ngrok 隧道", "type": "boolean" } } }, "dto.AdminProcessInfo": { "type": "object", "properties": { "cpuPercent": { "description": "CPU Usage percentage // CPU 使用率", "type": "number" }, "memoryPercent": { "description": "Memory Usage percentage // 内存使用率", "type": "number" }, "name": { "description": "Process Name // 进程名称", "type": "string" }, "pid": { "description": "Process ID // 进程 ID", "type": "integer" }, "ppid": { "description": "Parent Process ID // 父进程 ID", "type": "integer" } } }, "dto.AdminRuntimeInfo": { "type": "object", "properties": { "buckHashSys": { "description": "Memory obtained from system for profiling bucket hash table (bytes) // 分析桶哈希表占用的系统内存", "type": "integer" }, "gcSys": { "description": "Memory obtained from system for metadata for GC (bytes) // GC 元数据占用的系统内存", "type": "integer" }, "heapIdle": { "description": "Memory in idle spans (bytes) // 空闲 Span 占用的内存", "type": "integer" }, "heapInuse": { "description": "Memory in in-use spans (bytes) // 正在使用的 Span 占用的内存", "type": "integer" }, "heapReleased": { "description": "Memory released to OS (bytes) // 释放回操作系统的内存(字节)", "type": "integer" }, "heapSys": { "description": "Memory obtained from system for heap (bytes) // 堆占用的系统内存", "type": "integer" }, "mCacheSys": { "description": "Memory obtained from system for mcache (bytes) // mcache 占用的系统内存", "type": "integer" }, "mSpanSys": { "description": "Memory obtained from system for mspan (bytes) // mspan 占用的系统内存", "type": "integer" }, "memAlloc": { "description": "Allocated memory (bytes) // 已分配内存(字节)", "type": "integer" }, "memSys": { "description": "Memory obtained from system (bytes) // 从系统获取的内存(字节)", "type": "integer" }, "memTotal": { "description": "Total memory allocated (bytes) // 累计分配内存(字节)", "type": "integer" }, "nextGc": { "description": "Target heap size for the next GC cycle // 下次 GC 的目标堆大小", "type": "integer" }, "numGc": { "description": "Number of completed GC cycles // GC 次数", "type": "integer" }, "numGoroutine": { "description": "Number of goroutines // Goroutine 数量", "type": "integer" }, "otherSys": { "description": "Other system memory (bytes) // 其他系统内存", "type": "integer" }, "stackSys": { "description": "Memory obtained from system for stack (bytes) // 栈占用的系统内存", "type": "integer" } } }, "dto.AdminSystemInfo": { "type": "object", "properties": { "cpu": { "description": "CPU information // CPU 信息", "allOf": [ { "$ref": "#/definitions/dto.AdminCPUInfo" } ] }, "host": { "description": "Host information // 主机信息", "allOf": [ { "$ref": "#/definitions/dto.AdminHostInfo" } ] }, "memory": { "description": "Memory information // 内存信息", "allOf": [ { "$ref": "#/definitions/dto.AdminMemoryInfo" } ] }, "process": { "description": "Process information // 进程信息", "allOf": [ { "$ref": "#/definitions/dto.AdminProcessInfo" } ] }, "runtimeStatus": { "description": "Go runtime status // Go 运行时状态", "allOf": [ { "$ref": "#/definitions/dto.AdminRuntimeInfo" } ] }, "startTime": { "description": "Start time // 启动时间", "type": "string" }, "uptime": { "description": "Uptime (seconds) // 运行时间(秒)", "type": "number" } } }, "dto.AdminUserDatabaseConfig": { "type": "object", "properties": { "charset": { "description": "Charset // 字符集", "type": "string" }, "connMaxIdleTime": { "description": "Connection max idle time // 空闲连接最大生命周期", "type": "string" }, "connMaxLifetime": { "description": "Connection max lifetime // 连接最大生命周期", "type": "string" }, "host": { "description": "Host // 主机", "type": "string" }, "maxIdleConns": { "description": "Max idle connections // 最大闲置连接数", "type": "integer" }, "maxOpenConns": { "description": "Max open connections // 最大打开连接数", "type": "integer" }, "maxWriteConcurrency": { "description": "Max write concurrency // 最大并发写入数", "type": "integer" }, "name": { "description": "Database name // 数据库名", "type": "string" }, "parseTime": { "description": "Parse time // 是否解析时间", "type": "boolean" }, "password": { "description": "Password // 密码", "type": "string" }, "path": { "description": "SQLite database file path // SQLite 数据库文件路径", "type": "string" }, "port": { "description": "Port // 端口", "type": "integer" }, "schema": { "description": "Database schema (postgres only) // 数据库 Schema", "type": "string" }, "sslMode": { "description": "SSL mode (postgres only) // SSL 模式", "type": "string" }, "type": { "description": "Database type (mysql, postgres, sqlite) // 数据库类型", "type": "string", "enum": [ "mysql", "postgres", "sqlite" ] }, "userName": { "description": "Username // 用户名", "type": "string" } } }, "dto.AdminWebGUIConfig": { "type": "object", "properties": { "adminUid": { "description": "Admin UID // 管理员 UID", "type": "integer" }, "fontSet": { "description": "Font set // 字体设置", "type": "string" }, "registerIsEnable": { "description": "Registration enablement // 是否开启注册", "type": "boolean" } } }, "dto.BackupConfigDTO": { "type": "object", "properties": { "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "cronExpression": { "description": "Cron expression // Cron表达式", "type": "string" }, "cronStrategy": { "description": "Cron strategy // 定时策略", "type": "string" }, "id": { "description": "Config ID // 配置ID", "type": "integer" }, "includeVaultName": { "description": "Whether sync path includes vault name // 同步路径是否包含仓库名", "type": "boolean" }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean" }, "lastMessage": { "description": "Last run result message // 上次运行结果消息", "type": "string" }, "lastRunTime": { "description": "Last run time // 上次运行时间", "type": "string" }, "lastStatus": { "description": "Last status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) // 上次状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped)", "type": "integer" }, "nextRunTime": { "description": "Next run time // 下次运行时间", "type": "string" }, "retentionDays": { "description": "Retention days // 保留天数", "type": "integer" }, "storageIds": { "description": "Storage ID list // 存储ID列表", "type": "string" }, "type": { "description": "Backup type (full, incremental, sync) // 备份类型 (full, incremental, sync)", "type": "string" }, "uid": { "description": "User UID // 用户ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "vault": { "description": "Associated vault name // 关联库名称", "type": "string" } } }, "dto.BackupConfigRequest": { "type": "object", "required": [ "cronStrategy", "storageIds", "type" ], "properties": { "cronExpression": { "description": "Cron expression // Cron 表达式", "type": "string", "example": "0 0 * * *" }, "cronStrategy": { "description": "Cron strategy // 定时策略", "type": "string", "enum": [ "daily", "weekly", "monthly", "custom" ], "example": "daily" }, "id": { "description": "ID // ID", "type": "integer", "example": 1 }, "includeVaultName": { "description": "Include vault name // 同步路径是否包含仓库名", "type": "boolean", "example": false }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean", "example": true }, "retentionDays": { "description": "Retention days // 保留天数", "type": "integer", "minimum": -1, "example": 7 }, "storageIds": { "description": "Storage IDs // 存储 ID 列表", "type": "string", "example": "[1, 2]" }, "type": { "description": "Backup type // 备份类型", "type": "string", "enum": [ "full", "incremental", "sync" ], "example": "sync" }, "vault": { "description": "Vault name // 仓库名称", "type": "string", "example": "test" } } }, "dto.BackupExecuteRequest": { "type": "object", "properties": { "id": { "description": "ID // ID", "type": "integer", "example": 1 } } }, "dto.BackupHistoryDTO": { "type": "object", "properties": { "configId": { "description": "Config ID // 配置ID", "type": "integer" }, "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "endTime": { "description": "End time // 结束时间", "type": "string" }, "fileCount": { "description": "File count // 文件数量", "type": "integer" }, "filePath": { "description": "File path // 文件路径", "type": "string" }, "fileSize": { "description": "File size // 文件大小", "type": "integer" }, "id": { "description": "History record ID // 历史记录ID", "type": "integer" }, "message": { "description": "Result message // 结果消息", "type": "string" }, "startTime": { "description": "Start time // 开始时间", "type": "string" }, "status": { "description": "Status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) // 状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped)", "type": "integer" }, "storageId": { "description": "Storage ID // 存储ID", "type": "integer" }, "type": { "description": "Backup type // 备份类型", "type": "string" }, "uid": { "description": "User UID // 用户ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" } } }, "dto.FileDTO": { "type": "object", "properties": { "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Updated timestamp // 更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "File path // 文件路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "rename": { "description": "Rename flag // 重命名标记", "type": "integer" }, "size": { "description": "File size // 文件大小", "type": "integer" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" } } }, "dto.FileRecycleClearRequest": { "type": "object", "required": [ "vault" ], "properties": { "path": { "description": "File path, empty for all // 文件路径,为空则清理全部", "type": "string", "example": "path/to/file.png" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FileRenameRequest": { "type": "object", "required": [ "oldPath", "path", "vault" ], "properties": { "oldPath": { "description": "Old path // 旧路径", "type": "string", "example": "OldImage.png" }, "oldPathHash": { "description": "Old path hash // 旧路径哈希", "type": "string", "example": "ofhash456" }, "path": { "description": "New path // 新路径", "type": "string", "example": "NewImage.png" }, "pathHash": { "description": "New path hash // 新路径哈希", "type": "string", "example": "nfhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FileRestoreRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "File path // 文件路径", "type": "string", "example": "Image.png" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FolderCreateRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Folder path // 文件夹路径", "type": "string", "example": "NewFolder" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash456" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FolderDTO": { "type": "object", "properties": { "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Folder path // 文件夹路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希值", "type": "string" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" } } }, "dto.FolderDeleteRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Folder path // 文件夹路径", "type": "string", "example": "OldFolder" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "fhash789" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.FolderTreeNode": { "type": "object", "properties": { "children": { "description": "Child nodes // 子节点", "type": "array", "items": { "$ref": "#/definitions/dto.FolderTreeNode" } }, "fileCount": { "description": "File count // 文件数量", "type": "integer" }, "name": { "description": "Node name // 节点名称", "type": "string" }, "noteCount": { "description": "Note count // 笔记数量", "type": "integer" }, "path": { "description": "Node path // 节点路径", "type": "string" } } }, "dto.FolderTreeResponse": { "type": "object", "properties": { "folders": { "description": "Folder tree // 文件夹树", "type": "array", "items": { "$ref": "#/definitions/dto.FolderTreeNode" } }, "rootFileCount": { "description": "File count in root // 根目录中的文件数量", "type": "integer" }, "rootNoteCount": { "description": "Note count in root // 根目录中的笔记数量", "type": "integer" } } }, "dto.GitSyncCleanRequest": { "type": "object", "properties": { "configId": { "type": "integer" } } }, "dto.GitSyncConfigDTO": { "type": "object", "properties": { "branch": { "description": "Branch // 分支", "type": "string" }, "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "delay": { "description": "Delay time (seconds) // 延迟时间(秒)", "type": "integer" }, "id": { "description": "Task ID // 任务ID", "type": "integer" }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean" }, "lastMessage": { "description": "Last run result message // 上次运行结果消息", "type": "string" }, "lastStatus": { "description": "Last status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown) // 上次状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown)", "type": "integer" }, "lastSyncTime": { "description": "Last sync time // 上次同步时间", "type": "string" }, "password": { "description": "Password // 密码", "type": "string" }, "repoUrl": { "description": "Repository URL // 仓库地址", "type": "string" }, "retentionDays": { "description": "History retention days // 历史记录保留天数", "type": "integer" }, "uid": { "description": "User ID // 用户ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "username": { "description": "Username // 用户名", "type": "string" }, "vault": { "description": "Associated vault name // 关联库名称", "type": "string" } } }, "dto.GitSyncConfigRequest": { "type": "object", "required": [ "repoUrl" ], "properties": { "branch": { "type": "string" }, "delay": { "description": "Delay time (seconds) // 延迟时间(秒)", "type": "integer" }, "id": { "type": "integer" }, "isEnabled": { "type": "boolean" }, "password": { "type": "string" }, "repoUrl": { "type": "string" }, "retentionDays": { "type": "integer" }, "username": { "type": "string" }, "vault": { "description": "Associated vault name // 关联笔记本名称", "type": "string" } } }, "dto.GitSyncDeleteRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "integer" } } }, "dto.GitSyncExecuteRequest": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "integer" } } }, "dto.GitSyncHistoryDTO": { "type": "object", "properties": { "configId": { "type": "integer" }, "createdAt": { "type": "string" }, "endTime": { "type": "string" }, "id": { "type": "integer" }, "message": { "type": "string" }, "startTime": { "type": "string" }, "status": { "description": "0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown", "type": "integer" } } }, "dto.GitSyncValidateRequest": { "type": "object", "required": [ "repoUrl" ], "properties": { "branch": { "type": "string" }, "password": { "type": "string" }, "repoUrl": { "type": "string" }, "username": { "type": "string" } } }, "dto.NoteAppendRequest": { "type": "object", "required": [ "content", "path", "vault" ], "properties": { "content": { "description": "Content to append // 追加内容", "type": "string", "example": "Appended content" }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteDTO": { "type": "object", "properties": { "clientName": { "description": "Client name // 客户端名称", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "content": { "description": "Note content // 笔记内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Note path // 笔记路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "size": { "description": "Note size // 笔记大小", "type": "integer" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" }, "version": { "description": "Version number // 版本号", "type": "integer" } } }, "dto.NoteHistoryDTO": { "type": "object", "properties": { "clientName": { "description": "Client that made changes // 产生变更的客户端", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "content": { "description": "Full historical content // 完整历史内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Creation time of this version // 此版本的创建时间", "type": "string" }, "diffs": { "description": "Text differences // 文本差异内容", "type": "array", "items": { "$ref": "#/definitions/diffmatchpatch.Diff" } }, "id": { "description": "History entry ID // 历史项 ID", "type": "integer" }, "noteId": { "description": "Associated note ID // 笔记 ID", "type": "integer" }, "path": { "description": "Note path at that time // 当时的笔记路径", "type": "string" }, "vaultId": { "description": "Associated vault ID // 保险库 ID", "type": "integer" }, "version": { "description": "Historical version number // 历史版本号", "type": "integer" } } }, "dto.NoteHistoryRestoreRequest": { "type": "object", "required": [ "historyId", "vault" ], "properties": { "historyId": { "description": "History version ID // 历史版本 ID", "type": "integer", "example": 1 }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteLinkItem": { "type": "object", "properties": { "context": { "description": "Text context around link // 链接文本上下文", "type": "string" }, "isEmbed": { "description": "Is it an embed (![[...]]) // 是否为嵌入", "type": "boolean" }, "linkText": { "description": "Raw link text (optional) // 原始链接文本(可选)", "type": "string" }, "path": { "description": "Target path // 目标路径", "type": "string" } } }, "dto.NoteModifyOrCreateRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "baseHash": { "description": "Base hash for sync // 同步基准哈希", "type": "string", "example": "bhash789" }, "baseHashMissing": { "description": "Marks if baseHash is unavailable // 标记基准哈希是否缺失", "type": "boolean", "example": false }, "content": { "description": "Note content // 笔记内容", "type": "string", "example": "# Hello World" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string", "example": "chash012" }, "createOnly": { "description": "If true, fail if note already exists // 如果为 true,笔记已存在则失败", "type": "boolean", "example": false }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer", "example": 1700000000 }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer", "example": 1700000000 }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteMoveRequest": { "type": "object", "required": [ "destination", "path", "vault" ], "properties": { "destination": { "description": "Destination path // 目标路径", "type": "string", "example": "Folder/Source.md" }, "overwrite": { "description": "Overwrite existing // 覆盖现有", "type": "boolean", "example": false }, "path": { "description": "Current path // 当前路径", "type": "string", "example": "Source.md" }, "pathHash": { "description": "Current path hash // 当前路径哈希", "type": "string", "example": "src_hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteNoContentDTO": { "type": "object", "properties": { "clientName": { "description": "Client name // 客户端名称", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Note path // 笔记路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "size": { "description": "Note size // 笔记大小", "type": "integer" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" }, "version": { "description": "Version number // 版本号", "type": "integer" } } }, "dto.NotePatchFrontmatterRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "remove": { "description": "Fields to remove // 待移除字段", "type": "array", "items": { "type": "string" }, "example": [ "old_tag" ] }, "updates": { "description": "Fields to update // 待更新字段", "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NotePrependRequest": { "type": "object", "required": [ "content", "path", "vault" ], "properties": { "content": { "description": "Content to prepend // 头部添加内容", "type": "string", "example": "Prepended content\n" }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteRecycleClearRequest": { "type": "object", "required": [ "vault" ], "properties": { "path": { "description": "Note path, empty for all // 笔记路径,为空则清理全部", "type": "string", "example": "path/to/note.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteRenameRequest": { "type": "object", "required": [ "oldPath", "path", "vault" ], "properties": { "oldPath": { "description": "Old path // 旧路径", "type": "string", "example": "OldName.md" }, "oldPathHash": { "description": "Old path hash // 旧路径哈希", "type": "string", "example": "ohash456" }, "path": { "description": "New path // 新路径", "type": "string", "example": "NewName.md" }, "pathHash": { "description": "New path hash // 新路径哈希", "type": "string", "example": "nhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteReplaceRequest": { "type": "object", "required": [ "find", "path", "vault" ], "properties": { "all": { "description": "Replace all matches // 替换所有", "type": "boolean", "example": true }, "failIfNoMatch": { "description": "Fail if no match found // 若无匹配则失败", "type": "boolean", "example": true }, "find": { "description": "String to find // 查找内容", "type": "string", "example": "old text" }, "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "regex": { "description": "Use regex // 使用正则", "type": "boolean", "example": false }, "replace": { "description": "String to replace with // 替换内容", "type": "string", "example": "new text" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteReplaceResponse": { "type": "object", "properties": { "matchCount": { "description": "Number of matches found // 匹配数量", "type": "integer" }, "note": { "description": "Updated note data // 更新后的笔记数据", "allOf": [ { "$ref": "#/definitions/dto.NoteDTO" } ] } } }, "dto.NoteRestoreRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Note path // 笔记路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.NoteWithFileLinksResponse": { "type": "object", "properties": { "content": { "description": "Note content // 笔记内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "fileLinks": { "description": "Map of file link to actual path // 文件链接到实际路径的映射", "type": "object", "additionalProperties": { "type": "string" } }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Note path // 笔记路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string" }, "updatedAt": { "description": "Updated at time // 更新时间" }, "version": { "description": "Version number // 版本号", "type": "integer" } } }, "dto.SettingDTO": { "type": "object", "properties": { "content": { "description": "Setting content // 配置内容", "type": "string" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string" }, "createdAt": { "description": "Created at time // 创建时间", "type": "string" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer" }, "id": { "description": "Setting ID // 配置 ID", "type": "integer" }, "lastTime": { "description": "Record update timestamp // 记录更新时间戳", "type": "integer" }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer" }, "path": { "description": "Setting path // 配置路径", "type": "string" }, "pathHash": { "description": "Path hash // 路径哈希值", "type": "string" }, "updatedAt": { "description": "Updated at time // 更新时间", "type": "string" } } }, "dto.SettingDeleteRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "path": { "description": "Setting path // 配置路径", "type": "string", "example": "User/Theme" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.SettingModifyOrCreateRequest": { "type": "object", "required": [ "path", "vault" ], "properties": { "content": { "description": "Setting content // 配置内容", "type": "string", "example": "dark" }, "contentHash": { "description": "Content hash // 内容哈希", "type": "string", "example": "chash456" }, "ctime": { "description": "Creation timestamp // 创建时间戳", "type": "integer", "example": 1700000000 }, "mtime": { "description": "Modification timestamp // 修改时间戳", "type": "integer", "example": 1700000000 }, "path": { "description": "Setting path // 配置路径", "type": "string", "example": "User/Theme" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.SettingRenameRequest": { "type": "object", "required": [ "newPath", "oldPath", "vault" ], "properties": { "newPath": { "description": "New path // 新路径", "type": "string", "example": "New/Path" }, "newPathHash": { "description": "New path hash // 新路径哈希", "type": "string", "example": "newhash456" }, "oldPath": { "description": "Old path // 旧路径", "type": "string", "example": "Old/Path" }, "oldPathHash": { "description": "Old path hash // 旧路径哈希", "type": "string", "example": "oldhash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.ShareCancelRequest": { "type": "object", "required": [ "vault" ], "properties": { "id": { "description": "Share ID (optional) // 分享 ID (可选)", "type": "integer", "example": 1 }, "path": { "description": "Resource path (optional) // 资源路径 (可选)", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Resource path Hash (optional) // 资源路径哈希 (可选)", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "defaultVault" } } }, "dto.ShareCreateRequest": { "type": "object", "required": [ "path", "pathHash", "vault" ], "properties": { "password": { "description": "Share password // 分享密码", "type": "string", "example": "123456" }, "path": { "description": "Resource path // 资源路径", "type": "string", "example": "ReadMe.md" }, "pathHash": { "description": "Resource path Hash // 资源路径哈希", "type": "string", "example": "hash123" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "defaultVault" } } }, "dto.ShareCreateResponse": { "type": "object", "properties": { "expiresAt": { "description": "Expiration time // 过期时间", "type": "string" }, "id": { "description": "ID of the note or file table (primary resource ID) // 笔记或文件表 ID(主资源 ID)", "type": "integer" }, "isPassword": { "description": "Whether password is set // 是否设置了密码", "type": "boolean" }, "shortLink": { "description": "Short link // 短链", "type": "string" }, "token": { "description": "Share Token // 分享 Token", "type": "string" }, "type": { "description": "Resource type: note or file // 资源类型:笔记(note)或文件(file)", "type": "string" } } }, "dto.ShareListItem": { "type": "object", "properties": { "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "expiresAt": { "description": "Expiration time // 过期时间", "type": "string" }, "id": { "description": "Share ID // 分享记录 ID", "type": "integer" }, "isPassword": { "description": "Whether password is set // 是否设置了密码", "type": "boolean" }, "lastViewedAt": { "description": "Last viewed time // 最后访问时间", "type": "string" }, "notePath": { "description": "Note path, for frontend share filter matching // 笔记路径,用于前端分享筛选匹配", "type": "string" }, "res": { "description": "Authorized resources // 资源授权列表", "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "shortLink": { "description": "Short link // 短链", "type": "string" }, "status": { "description": "Status: 1-Active, 2-Cancelled // 状态: 1-有效, 2-已撤销", "type": "integer" }, "title": { "description": "Resource title (note title or file name) // 资源标题(笔记标题或文件名)", "type": "string" }, "uid": { "description": "User ID // 用户 ID", "type": "integer" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "url": { "description": "Share URL (path format: /id/token) // 分享 URL (路径格式: /id/token)", "type": "string" }, "vaultName": { "description": "Vault name where the note belongs // 笔记所属仓库名", "type": "string" }, "viewCount": { "description": "View count // 访问次数", "type": "integer" } } }, "dto.SharePasswordUpdateRequest": { "type": "object", "required": [ "path", "pathHash", "vault" ], "properties": { "password": { "description": "New password // 新密码", "type": "string", "example": "123456" }, "path": { "description": "Resource path // 资源路径", "type": "string", "example": "未命名.md" }, "pathHash": { "description": "Resource path Hash // 资源路径哈希", "type": "string", "example": "-677306325" }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "test" } } }, "dto.ShareShortLinkCreateRequest": { "type": "object", "required": [ "path", "pathHash", "vault" ], "properties": { "is_force": { "description": "Whether to force regeneration // 是否强制重新生成", "type": "boolean", "example": false }, "path": { "description": "Path // 路径", "type": "string", "example": "notes/todo.md" }, "pathHash": { "description": "Path hash // 路径哈希", "type": "string", "example": "..." }, "url": { "description": "Full share URL from client; if provided, used directly without regenerating token // 客户端传入的完整分享链接,非空时直接使用,不重新生成 token", "type": "string", "example": "https://example.com/share/129/CNmkmQlq0s-4elT3NuZG2w" }, "vault": { "description": "Vault name // 库名", "type": "string", "example": "work" } } }, "dto.StorageDTO": { "type": "object", "properties": { "accessKeyId": { "description": "Access key ID // 访问密钥 ID", "type": "string" }, "accessKeySecret": { "description": "Access key secret // 访问密钥秘密", "type": "string" }, "accessUrlPrefix": { "description": "Access URL prefix // 访问地址前缀", "type": "string" }, "accountId": { "description": "Account ID // 账户 ID", "type": "string" }, "bucketName": { "description": "Bucket name // 存储桶名称", "type": "string" }, "createdAt": { "description": "Created at // 创建时间", "type": "string" }, "customPath": { "description": "Custom path // 自定义路径", "type": "string" }, "endpoint": { "description": "Endpoint // 访问端点", "type": "string" }, "id": { "description": "ID // ID", "type": "integer" }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "boolean" }, "password": { "description": "Password // 密码", "type": "string" }, "region": { "description": "Region // 区域", "type": "string" }, "type": { "description": "Storage type // 存储类型", "type": "string" }, "updatedAt": { "description": "Updated at // 更新时间", "type": "string" }, "user": { "description": "Username // 用户名", "type": "string" } } }, "dto.StoragePostRequest": { "type": "object", "required": [ "accessUrlPrefix", "type" ], "properties": { "accessKeyId": { "description": "Access key ID // 访问密钥ID", "type": "string", "example": "" }, "accessKeySecret": { "description": "Access key secret // 访问密钥秘密", "type": "string", "example": "" }, "accessUrlPrefix": { "description": "Access URL prefix // 访问地址前缀", "type": "string", "maxLength": 100, "minLength": 2, "example": "https://cdn.com" }, "accountId": { "description": "Account ID (R2) // 账户ID r2", "type": "string", "example": "123456789" }, "bucketName": { "description": "Bucket name // 存储桶名称", "type": "string", "example": "my-bucket" }, "customPath": { "description": "Custom path // 自定义路径", "type": "string", "example": "/backups" }, "endpoint": { "description": "Endpoint (OSS) // 端点 oss", "type": "string", "example": "oss-cn-hangzhou.aliyuncs.com" }, "id": { "description": "ID // ID", "type": "integer", "example": 1 }, "isEnabled": { "description": "Is enabled // 是否启用", "type": "integer", "example": 1 }, "password": { "description": "Password // 密码", "type": "string", "example": "secret_password" }, "region": { "description": "Region (S3) // 区域 s3", "type": "string", "example": "us-east-1" }, "type": { "description": "Storage type // 类型", "type": "string", "minLength": 1, "example": "local-fs" }, "user": { "description": "Username // 访问用户名", "type": "string", "example": "admin" } } }, "dto.SyncLogDTO": { "type": "object", "properties": { "action": { "description": "Action type // 操作类型", "type": "string" }, "changedFields": { "description": "Changed fields // 变更字段", "type": "string" }, "clientName": { "description": "Client name // 客户端名称", "type": "string" }, "clientType": { "description": "Client type // 客户端类型", "type": "string" }, "clientVersion": { "description": "Client version // 客户端版本", "type": "string" }, "createdAt": { "description": "Log creation time // 创建时间", "type": "string" }, "message": { "description": "Additional message // 附加消息", "type": "string" }, "path": { "description": "Resource path // 资源路径", "type": "string" }, "pathHash": { "description": "Resource path hash // 路径哈希", "type": "string" }, "size": { "description": "Size in bytes // 大小(字节)", "type": "integer" }, "status": { "description": "Status: 1 success, 2 failed // 状态", "type": "integer" }, "type": { "description": "Resource type // 资源类型", "type": "string" }, "vaultId": { "description": "Vault ID // 笔记本 ID", "type": "integer" } } }, "dto.UserChangePasswordRequest": { "type": "object", "required": [ "confirmPassword", "oldPassword", "password" ], "properties": { "confirmPassword": { "description": "Confirm password // 校验密码", "type": "string", "example": "new_password123" }, "oldPassword": { "description": "Old password // 旧密码", "type": "string", "example": "old_password123" }, "password": { "description": "New password // 新密码", "type": "string", "example": "new_password123" } } }, "dto.UserCreateRequest": { "type": "object", "required": [ "confirmPassword", "email", "password", "username" ], "properties": { "confirmPassword": { "description": "Confirm password // 校验密码", "type": "string", "example": "password123" }, "email": { "description": "User email // 用户邮件", "type": "string", "example": "user@example.com" }, "password": { "description": "User password // 用户密码", "type": "string", "example": "password123" }, "username": { "description": "User name // 用户名", "type": "string", "example": "username123" } } }, "dto.UserDTO": { "type": "object", "properties": { "avatar": { "description": "Avatar URL or handle // 头像路径或名称", "type": "string" }, "createdAt": { "description": "Account created time // 账号创建时间", "type": "string" }, "email": { "description": "Email address // 邮件地址", "type": "string" }, "token": { "description": "Authentication Token // 认证 Token", "type": "string" }, "uid": { "description": "User ID (primary key) // 用户唯一标识(主键)", "type": "integer" }, "updatedAt": { "description": "Last updated time // 最后更新时间", "type": "string" }, "username": { "description": "Username // 用户名", "type": "string" } } }, "dto.UserLoginRequest": { "type": "object", "required": [ "credentials", "password" ], "properties": { "credentials": { "description": "Username or Email // 登录凭证(用户名或邮件)", "type": "string", "example": "user@example.com" }, "password": { "description": "Password // 密码", "type": "string", "example": "password123" } } }, "dto.VaultDTO": { "type": "object", "properties": { "createdAt": { "description": "Creation time // 创建时间", "type": "string" }, "fileCount": { "description": "Number of files // 文件数量", "type": "integer" }, "fileSize": { "description": "Size of files // 文件大小", "type": "integer" }, "id": { "description": "Vault ID // 保险库 ID", "type": "integer" }, "noteCount": { "description": "Number of notes // 笔记数量", "type": "integer" }, "noteSize": { "description": "Size of notes // 笔记大小", "type": "integer" }, "size": { "description": "Total size // 总大小", "type": "integer" }, "updatedAt": { "description": "Updated time // 更新时间", "type": "string" }, "vault": { "description": "Vault name // 保险库名称", "type": "string" } } }, "dto.VaultPostRequest": { "type": "object", "required": [ "vault" ], "properties": { "id": { "description": "Vault ID (optional for update) // 保险库 ID(可选,用于更新)", "type": "integer", "example": 1 }, "vault": { "description": "Vault name // 保险库名称", "type": "string", "example": "MyVault" } } }, "dto.VersionDTO": { "type": "object", "properties": { "buildTime": { "description": "Build time // 构建时间", "type": "string" }, "gitTag": { "description": "Git tag // Git 标签", "type": "string" }, "pluginVersionNewChangelog": { "description": "New plugin version changelog link // 插件新版本更新日志链接", "type": "string" }, "pluginVersionNewChangelogContent": { "description": "New plugin version changelog content // 插件新版本更新日志内容", "type": "string" }, "pluginVersionNewLink": { "description": "New plugin version link // 插件新版本链接", "type": "string" }, "pluginVersionNewName": { "description": "New plugin version name // 插件新版本名称", "type": "string" }, "version": { "description": "Current version // 当前版本", "type": "string" }, "versionIsNew": { "description": "Is there a new version // 是否有新版本", "type": "boolean" }, "versionNewChangelog": { "description": "New version changelog link // 新版本更新日志链接", "type": "string" }, "versionNewChangelogContent": { "description": "New version changelog content // 新版本更新日志内容", "type": "string" }, "versionNewLink": { "description": "New version download link // 新版本下载链接", "type": "string" }, "versionNewName": { "description": "New version name // 新版本名称", "type": "string" } } } }, "securityDefinitions": { "ShareAuthToken": { "type": "apiKey", "name": "Share-Token", "in": "header" }, "UserAuthToken": { "type": "apiKey", "name": "token", "in": "header" } } } ================================================ FILE: docs/swagger.yaml ================================================ basePath: / definitions: api_router.HealthResponse: properties: database: description: '"connected" or "error" // "connected" 或 "error"' type: string status: description: '"healthy" or "unhealthy" // "healthy" 或 "unhealthy"' type: string uptime: description: Uptime (seconds) // 运行时间(秒) type: number version: description: Service version number // 服务版本号 type: string type: object app.ListRes: properties: list: description: Data list // 数据清单 pager: allOf: - $ref: '#/definitions/app.Pager' description: Pagination info // 翻页信息 type: object app.Pager: properties: page: description: Page number // 页码 type: integer pageSize: description: Page size // 每页数量 type: integer totalRows: description: Total rows // 总行数 type: integer type: object app.Res: properties: code: type: integer context: {} data: {} details: {} message: {} status: type: boolean vault: {} type: object app.WSClientInfo: properties: clientName: type: string clientType: type: string clientVersion: type: string nickname: type: string platformInfo: additionalProperties: type: boolean type: object remoteAddr: type: string startTime: type: string traceId: type: string uid: type: string type: object diffmatchpatch.Diff: properties: text: type: string type: $ref: '#/definitions/diffmatchpatch.Operation' type: object diffmatchpatch.Operation: enum: - -1 - 1 - 0 format: int32 type: integer x-enum-varnames: - DiffDelete - DiffInsert - DiffEqual dto.AdminCPUInfo: properties: loadAvg: allOf: - $ref: '#/definitions/dto.AdminLoadInfo' description: Load average // 平均负载 logicalCores: description: Logical cores // 逻辑核心数 type: integer modelName: description: Model name // 型号 type: string percent: description: Usage percentage per core // 每个核心的使用率 items: type: number type: array physicalCores: description: Physical cores // 物理核心数 type: integer type: object dto.AdminCheckResponse: properties: isAdmin: description: Whether have admin privileges // 是否具有管理员权限 type: boolean type: object dto.AdminCloudflareConfig: properties: enabled: description: Whether to enable cloudflare tunnel // 是否启用 cloudflare 隧道 type: boolean logEnabled: description: Whether to enable cloudflare tunnel logging // 是否开启 cloudflare 隧道日志 type: boolean token: description: cloudflare tunnel token // cloudflare 隧道令牌 type: string type: object dto.AdminConfig: properties: adminUid: description: Admin UID // 管理员 UID type: integer authTokenKey: description: Auth token key // 认证 Token 密钥 type: string defaultApiFolder: description: Default API folder // 默认 API 目录 type: string fileChunkSize: description: File chunk size // 文件分块大小 type: string fontSet: description: Font set // 字体设置 type: string historyKeepVersions: description: History versions to keep // 历史版本保留数 type: integer historySaveDelay: description: History save delay // 历史保存延迟 type: string pullSource: description: 'Data pull source: auto | github | cnb // 数据拉取源:auto | github | cnb' type: string registerIsEnable: description: Registration enablement // 是否开启注册 type: boolean shareTokenExpiry: description: Share token expiry // 分享 Token 有效期 type: string shareTokenKey: description: Share token key // 分享 Token 密钥 type: string softDeleteRetentionTime: description: Soft delete retention time // 软删除保留时间 type: string tokenExpiry: description: Token expiry // Token 有效期 type: string uploadSessionTimeout: description: Upload session timeout // 上传会话超时时间 type: string type: object dto.AdminHostInfo: properties: arch: description: Architecture // 架构 type: string currentTime: description: Current system time // 当前系统时间 type: string hostname: description: Hostname // 主机名 type: string kernelVersion: description: Kernel version // 内核版本 type: string os: description: Operating system // 操作系统 type: string osPretty: description: Detailed OS name // 详细操作系统名称 type: string platform: description: Platform name // 平台 type: string timezone: description: Time zone name // 时区名称 type: string timezoneOffset: description: Time zone offset in seconds // 时区偏移(秒) type: integer uptime: description: System uptime // 系统运行时间 type: integer type: object dto.AdminLoadInfo: properties: load1: description: Load 1 min // 1分钟负载 type: number load5: description: Load 5 min // 5分钟负载 type: number load15: description: Load 15 min // 15分钟负载 type: number type: object dto.AdminMemoryInfo: properties: available: description: Available memory // 可用内存 type: integer swapTotal: description: Total swap space // 交换区总量 type: integer swapUsed: description: Used swap space // 交换区已用 type: integer swapUsedPercent: description: Swap usage percentage // 交换区使用率 type: number total: description: Total physical memory // 系统总内存 type: integer used: description: Used memory // 已用内存 type: integer usedPercent: description: Memory usage percentage // 内存使用率 type: number type: object dto.AdminNgrokConfig: properties: authToken: description: ngrok auth token // ngrok 认证令牌 type: string domain: description: Custom domain // 自定义域名 type: string enabled: description: Whether to enable ngrok tunnel // 是否启用 ngrok 隧道 type: boolean type: object dto.AdminProcessInfo: properties: cpuPercent: description: CPU Usage percentage // CPU 使用率 type: number memoryPercent: description: Memory Usage percentage // 内存使用率 type: number name: description: Process Name // 进程名称 type: string pid: description: Process ID // 进程 ID type: integer ppid: description: Parent Process ID // 父进程 ID type: integer type: object dto.AdminRuntimeInfo: properties: buckHashSys: description: Memory obtained from system for profiling bucket hash table (bytes) // 分析桶哈希表占用的系统内存 type: integer gcSys: description: Memory obtained from system for metadata for GC (bytes) // GC 元数据占用的系统内存 type: integer heapIdle: description: Memory in idle spans (bytes) // 空闲 Span 占用的内存 type: integer heapInuse: description: Memory in in-use spans (bytes) // 正在使用的 Span 占用的内存 type: integer heapReleased: description: Memory released to OS (bytes) // 释放回操作系统的内存(字节) type: integer heapSys: description: Memory obtained from system for heap (bytes) // 堆占用的系统内存 type: integer mCacheSys: description: Memory obtained from system for mcache (bytes) // mcache 占用的系统内存 type: integer mSpanSys: description: Memory obtained from system for mspan (bytes) // mspan 占用的系统内存 type: integer memAlloc: description: Allocated memory (bytes) // 已分配内存(字节) type: integer memSys: description: Memory obtained from system (bytes) // 从系统获取的内存(字节) type: integer memTotal: description: Total memory allocated (bytes) // 累计分配内存(字节) type: integer nextGc: description: Target heap size for the next GC cycle // 下次 GC 的目标堆大小 type: integer numGc: description: Number of completed GC cycles // GC 次数 type: integer numGoroutine: description: Number of goroutines // Goroutine 数量 type: integer otherSys: description: Other system memory (bytes) // 其他系统内存 type: integer stackSys: description: Memory obtained from system for stack (bytes) // 栈占用的系统内存 type: integer type: object dto.AdminSystemInfo: properties: cpu: allOf: - $ref: '#/definitions/dto.AdminCPUInfo' description: CPU information // CPU 信息 host: allOf: - $ref: '#/definitions/dto.AdminHostInfo' description: Host information // 主机信息 memory: allOf: - $ref: '#/definitions/dto.AdminMemoryInfo' description: Memory information // 内存信息 process: allOf: - $ref: '#/definitions/dto.AdminProcessInfo' description: Process information // 进程信息 runtimeStatus: allOf: - $ref: '#/definitions/dto.AdminRuntimeInfo' description: Go runtime status // Go 运行时状态 startTime: description: Start time // 启动时间 type: string uptime: description: Uptime (seconds) // 运行时间(秒) type: number type: object dto.AdminUserDatabaseConfig: properties: charset: description: Charset // 字符集 type: string connMaxIdleTime: description: Connection max idle time // 空闲连接最大生命周期 type: string connMaxLifetime: description: Connection max lifetime // 连接最大生命周期 type: string host: description: Host // 主机 type: string maxIdleConns: description: Max idle connections // 最大闲置连接数 type: integer maxOpenConns: description: Max open connections // 最大打开连接数 type: integer maxWriteConcurrency: description: Max write concurrency // 最大并发写入数 type: integer name: description: Database name // 数据库名 type: string parseTime: description: Parse time // 是否解析时间 type: boolean password: description: Password // 密码 type: string path: description: SQLite database file path // SQLite 数据库文件路径 type: string port: description: Port // 端口 type: integer schema: description: Database schema (postgres only) // 数据库 Schema type: string sslMode: description: SSL mode (postgres only) // SSL 模式 type: string type: description: Database type (mysql, postgres, sqlite) // 数据库类型 enum: - mysql - postgres - sqlite type: string userName: description: Username // 用户名 type: string type: object dto.AdminWebGUIConfig: properties: adminUid: description: Admin UID // 管理员 UID type: integer fontSet: description: Font set // 字体设置 type: string registerIsEnable: description: Registration enablement // 是否开启注册 type: boolean type: object dto.BackupConfigDTO: properties: createdAt: description: Created at // 创建时间 type: string cronExpression: description: Cron expression // Cron表达式 type: string cronStrategy: description: Cron strategy // 定时策略 type: string id: description: Config ID // 配置ID type: integer includeVaultName: description: Whether sync path includes vault name // 同步路径是否包含仓库名 type: boolean isEnabled: description: Is enabled // 是否启用 type: boolean lastMessage: description: Last run result message // 上次运行结果消息 type: string lastRunTime: description: Last run time // 上次运行时间 type: string lastStatus: description: Last status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) // 上次状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) type: integer nextRunTime: description: Next run time // 下次运行时间 type: string retentionDays: description: Retention days // 保留天数 type: integer storageIds: description: Storage ID list // 存储ID列表 type: string type: description: Backup type (full, incremental, sync) // 备份类型 (full, incremental, sync) type: string uid: description: User UID // 用户ID type: integer updatedAt: description: Updated at // 更新时间 type: string vault: description: Associated vault name // 关联库名称 type: string type: object dto.BackupConfigRequest: properties: cronExpression: description: Cron expression // Cron 表达式 example: 0 0 * * * type: string cronStrategy: description: Cron strategy // 定时策略 enum: - daily - weekly - monthly - custom example: daily type: string id: description: ID // ID example: 1 type: integer includeVaultName: description: Include vault name // 同步路径是否包含仓库名 example: false type: boolean isEnabled: description: Is enabled // 是否启用 example: true type: boolean retentionDays: description: Retention days // 保留天数 example: 7 minimum: -1 type: integer storageIds: description: Storage IDs // 存储 ID 列表 example: '[1, 2]' type: string type: description: Backup type // 备份类型 enum: - full - incremental - sync example: sync type: string vault: description: Vault name // 仓库名称 example: test type: string required: - cronStrategy - storageIds - type type: object dto.BackupExecuteRequest: properties: id: description: ID // ID example: 1 type: integer type: object dto.BackupHistoryDTO: properties: configId: description: Config ID // 配置ID type: integer createdAt: description: Created at // 创建时间 type: string endTime: description: End time // 结束时间 type: string fileCount: description: File count // 文件数量 type: integer filePath: description: File path // 文件路径 type: string fileSize: description: File size // 文件大小 type: integer id: description: History record ID // 历史记录ID type: integer message: description: Result message // 结果消息 type: string startTime: description: Start time // 开始时间 type: string status: description: Status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) // 状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Stopped) type: integer storageId: description: Storage ID // 存储ID type: integer type: description: Backup type // 备份类型 type: string uid: description: User UID // 用户ID type: integer updatedAt: description: Updated at // 更新时间 type: string type: object dto.FileDTO: properties: contentHash: description: Content hash // 内容哈希 type: string createdAt: description: Created at time // 创建时间 type: string ctime: description: Creation timestamp // 创建时间戳 type: integer lastTime: description: Updated timestamp // 更新时间戳 type: integer mtime: description: Modification timestamp // 修改时间戳 type: integer path: description: File path // 文件路径 type: string pathHash: description: Path hash // 路径哈希 type: string rename: description: Rename flag // 重命名标记 type: integer size: description: File size // 文件大小 type: integer updatedAt: description: Updated at time // 更新时间 type: string type: object dto.FileRecycleClearRequest: properties: path: description: File path, empty for all // 文件路径,为空则清理全部 example: path/to/file.png type: string pathHash: description: Path hash // 路径哈希 example: fhash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - vault type: object dto.FileRenameRequest: properties: oldPath: description: Old path // 旧路径 example: OldImage.png type: string oldPathHash: description: Old path hash // 旧路径哈希 example: ofhash456 type: string path: description: New path // 新路径 example: NewImage.png type: string pathHash: description: New path hash // 新路径哈希 example: nfhash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - oldPath - path - vault type: object dto.FileRestoreRequest: properties: path: description: File path // 文件路径 example: Image.png type: string pathHash: description: Path hash // 路径哈希 example: fhash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.FolderCreateRequest: properties: path: description: Folder path // 文件夹路径 example: NewFolder type: string pathHash: description: Path hash // 路径哈希 example: fhash456 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.FolderDTO: properties: createdAt: description: Created at time // 创建时间 type: string ctime: description: Creation timestamp // 创建时间戳 type: integer lastTime: description: Record update timestamp // 记录更新时间戳 type: integer mtime: description: Modification timestamp // 修改时间戳 type: integer path: description: Folder path // 文件夹路径 type: string pathHash: description: Path hash // 路径哈希值 type: string updatedAt: description: Updated at time // 更新时间 type: string type: object dto.FolderDeleteRequest: properties: path: description: Folder path // 文件夹路径 example: OldFolder type: string pathHash: description: Path hash // 路径哈希 example: fhash789 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.FolderTreeNode: properties: children: description: Child nodes // 子节点 items: $ref: '#/definitions/dto.FolderTreeNode' type: array fileCount: description: File count // 文件数量 type: integer name: description: Node name // 节点名称 type: string noteCount: description: Note count // 笔记数量 type: integer path: description: Node path // 节点路径 type: string type: object dto.FolderTreeResponse: properties: folders: description: Folder tree // 文件夹树 items: $ref: '#/definitions/dto.FolderTreeNode' type: array rootFileCount: description: File count in root // 根目录中的文件数量 type: integer rootNoteCount: description: Note count in root // 根目录中的笔记数量 type: integer type: object dto.GitSyncCleanRequest: properties: configId: type: integer type: object dto.GitSyncConfigDTO: properties: branch: description: Branch // 分支 type: string createdAt: description: Created at // 创建时间 type: string delay: description: Delay time (seconds) // 延迟时间(秒) type: integer id: description: Task ID // 任务ID type: integer isEnabled: description: Is enabled // 是否启用 type: boolean lastMessage: description: Last run result message // 上次运行结果消息 type: string lastStatus: description: Last status (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown) // 上次状态 (0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown) type: integer lastSyncTime: description: Last sync time // 上次同步时间 type: string password: description: Password // 密码 type: string repoUrl: description: Repository URL // 仓库地址 type: string retentionDays: description: History retention days // 历史记录保留天数 type: integer uid: description: User ID // 用户ID type: integer updatedAt: description: Updated at // 更新时间 type: string username: description: Username // 用户名 type: string vault: description: Associated vault name // 关联库名称 type: string type: object dto.GitSyncConfigRequest: properties: branch: type: string delay: description: Delay time (seconds) // 延迟时间(秒) type: integer id: type: integer isEnabled: type: boolean password: type: string repoUrl: type: string retentionDays: type: integer username: type: string vault: description: Associated vault name // 关联笔记本名称 type: string required: - repoUrl type: object dto.GitSyncDeleteRequest: properties: id: type: integer required: - id type: object dto.GitSyncExecuteRequest: properties: id: type: integer required: - id type: object dto.GitSyncHistoryDTO: properties: configId: type: integer createdAt: type: string endTime: type: string id: type: integer message: type: string startTime: type: string status: description: 0:Idle, 1:Running, 2:Success, 3:Failed, 4:Shutdown type: integer type: object dto.GitSyncValidateRequest: properties: branch: type: string password: type: string repoUrl: type: string username: type: string required: - repoUrl type: object dto.NoteAppendRequest: properties: content: description: Content to append // 追加内容 example: Appended content type: string path: description: Note path // 笔记路径 example: ReadMe.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - content - path - vault type: object dto.NoteDTO: properties: clientName: description: Client name // 客户端名称 type: string clientType: description: Client type // 客户端类型 type: string clientVersion: description: Client version // 客户端版本 type: string content: description: Note content // 笔记内容 type: string contentHash: description: Content hash // 内容哈希 type: string createdAt: description: Created at time // 创建时间 type: string ctime: description: Creation timestamp // 创建时间戳 type: integer lastTime: description: Record update timestamp // 记录更新时间戳 type: integer mtime: description: Modification timestamp // 修改时间戳 type: integer path: description: Note path // 笔记路径 type: string pathHash: description: Path hash // 路径哈希 type: string size: description: Note size // 笔记大小 type: integer updatedAt: description: Updated at time // 更新时间 type: string version: description: Version number // 版本号 type: integer type: object dto.NoteHistoryDTO: properties: clientName: description: Client that made changes // 产生变更的客户端 type: string clientType: description: Client type // 客户端类型 type: string clientVersion: description: Client version // 客户端版本 type: string content: description: Full historical content // 完整历史内容 type: string contentHash: description: Content hash // 内容哈希 type: string createdAt: description: Creation time of this version // 此版本的创建时间 type: string diffs: description: Text differences // 文本差异内容 items: $ref: '#/definitions/diffmatchpatch.Diff' type: array id: description: History entry ID // 历史项 ID type: integer noteId: description: Associated note ID // 笔记 ID type: integer path: description: Note path at that time // 当时的笔记路径 type: string vaultId: description: Associated vault ID // 保险库 ID type: integer version: description: Historical version number // 历史版本号 type: integer type: object dto.NoteHistoryRestoreRequest: properties: historyId: description: History version ID // 历史版本 ID example: 1 type: integer vault: description: Vault name // 保险库名称 example: MyVault type: string required: - historyId - vault type: object dto.NoteLinkItem: properties: context: description: Text context around link // 链接文本上下文 type: string isEmbed: description: Is it an embed (![[...]]) // 是否为嵌入 type: boolean linkText: description: Raw link text (optional) // 原始链接文本(可选) type: string path: description: Target path // 目标路径 type: string type: object dto.NoteModifyOrCreateRequest: properties: baseHash: description: Base hash for sync // 同步基准哈希 example: bhash789 type: string baseHashMissing: description: Marks if baseHash is unavailable // 标记基准哈希是否缺失 example: false type: boolean content: description: Note content // 笔记内容 example: '# Hello World' type: string contentHash: description: Content hash // 内容哈希 example: chash012 type: string createOnly: description: If true, fail if note already exists // 如果为 true,笔记已存在则失败 example: false type: boolean ctime: description: Creation timestamp // 创建时间戳 example: 1700000000 type: integer mtime: description: Modification timestamp // 修改时间戳 example: 1700000000 type: integer path: description: Note path // 笔记路径 example: ReadMe.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.NoteMoveRequest: properties: destination: description: Destination path // 目标路径 example: Folder/Source.md type: string overwrite: description: Overwrite existing // 覆盖现有 example: false type: boolean path: description: Current path // 当前路径 example: Source.md type: string pathHash: description: Current path hash // 当前路径哈希 example: src_hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - destination - path - vault type: object dto.NoteNoContentDTO: properties: clientName: description: Client name // 客户端名称 type: string clientType: description: Client type // 客户端类型 type: string clientVersion: description: Client version // 客户端版本 type: string createdAt: description: Created at time // 创建时间 type: string ctime: description: Creation timestamp // 创建时间戳 type: integer lastTime: description: Record update timestamp // 记录更新时间戳 type: integer mtime: description: Modification timestamp // 修改时间戳 type: integer path: description: Note path // 笔记路径 type: string pathHash: description: Path hash // 路径哈希 type: string size: description: Note size // 笔记大小 type: integer updatedAt: description: Updated at time // 更新时间 type: string version: description: Version number // 版本号 type: integer type: object dto.NotePatchFrontmatterRequest: properties: path: description: Note path // 笔记路径 example: ReadMe.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string remove: description: Fields to remove // 待移除字段 example: - old_tag items: type: string type: array updates: additionalProperties: items: type: string type: array description: Fields to update // 待更新字段 type: object vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.NotePrependRequest: properties: content: description: Content to prepend // 头部添加内容 example: | Prepended content type: string path: description: Note path // 笔记路径 example: ReadMe.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - content - path - vault type: object dto.NoteRecycleClearRequest: properties: path: description: Note path, empty for all // 笔记路径,为空则清理全部 example: path/to/note.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - vault type: object dto.NoteRenameRequest: properties: oldPath: description: Old path // 旧路径 example: OldName.md type: string oldPathHash: description: Old path hash // 旧路径哈希 example: ohash456 type: string path: description: New path // 新路径 example: NewName.md type: string pathHash: description: New path hash // 新路径哈希 example: nhash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - oldPath - path - vault type: object dto.NoteReplaceRequest: properties: all: description: Replace all matches // 替换所有 example: true type: boolean failIfNoMatch: description: Fail if no match found // 若无匹配则失败 example: true type: boolean find: description: String to find // 查找内容 example: old text type: string path: description: Note path // 笔记路径 example: ReadMe.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string regex: description: Use regex // 使用正则 example: false type: boolean replace: description: String to replace with // 替换内容 example: new text type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - find - path - vault type: object dto.NoteReplaceResponse: properties: matchCount: description: Number of matches found // 匹配数量 type: integer note: allOf: - $ref: '#/definitions/dto.NoteDTO' description: Updated note data // 更新后的笔记数据 type: object dto.NoteRestoreRequest: properties: path: description: Note path // 笔记路径 example: ReadMe.md type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.NoteWithFileLinksResponse: properties: content: description: Note content // 笔记内容 type: string contentHash: description: Content hash // 内容哈希 type: string createdAt: description: Created at time // 创建时间 ctime: description: Creation timestamp // 创建时间戳 type: integer fileLinks: additionalProperties: type: string description: Map of file link to actual path // 文件链接到实际路径的映射 type: object lastTime: description: Record update timestamp // 记录更新时间戳 type: integer mtime: description: Modification timestamp // 修改时间戳 type: integer path: description: Note path // 笔记路径 type: string pathHash: description: Path hash // 路径哈希 type: string updatedAt: description: Updated at time // 更新时间 version: description: Version number // 版本号 type: integer type: object dto.SettingDTO: properties: content: description: Setting content // 配置内容 type: string contentHash: description: Content hash // 内容哈希 type: string createdAt: description: Created at time // 创建时间 type: string ctime: description: Creation timestamp // 创建时间戳 type: integer id: description: Setting ID // 配置 ID type: integer lastTime: description: Record update timestamp // 记录更新时间戳 type: integer mtime: description: Modification timestamp // 修改时间戳 type: integer path: description: Setting path // 配置路径 type: string pathHash: description: Path hash // 路径哈希值 type: string updatedAt: description: Updated at time // 更新时间 type: string type: object dto.SettingDeleteRequest: properties: path: description: Setting path // 配置路径 example: User/Theme type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.SettingModifyOrCreateRequest: properties: content: description: Setting content // 配置内容 example: dark type: string contentHash: description: Content hash // 内容哈希 example: chash456 type: string ctime: description: Creation timestamp // 创建时间戳 example: 1700000000 type: integer mtime: description: Modification timestamp // 修改时间戳 example: 1700000000 type: integer path: description: Setting path // 配置路径 example: User/Theme type: string pathHash: description: Path hash // 路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - path - vault type: object dto.SettingRenameRequest: properties: newPath: description: New path // 新路径 example: New/Path type: string newPathHash: description: New path hash // 新路径哈希 example: newhash456 type: string oldPath: description: Old path // 旧路径 example: Old/Path type: string oldPathHash: description: Old path hash // 旧路径哈希 example: oldhash123 type: string vault: description: Vault name // 保险库名称 example: MyVault type: string required: - newPath - oldPath - vault type: object dto.ShareCancelRequest: properties: id: description: Share ID (optional) // 分享 ID (可选) example: 1 type: integer path: description: Resource path (optional) // 资源路径 (可选) example: ReadMe.md type: string pathHash: description: Resource path Hash (optional) // 资源路径哈希 (可选) example: hash123 type: string vault: description: Vault name // 保险库名称 example: defaultVault type: string required: - vault type: object dto.ShareCreateRequest: properties: password: description: Share password // 分享密码 example: "123456" type: string path: description: Resource path // 资源路径 example: ReadMe.md type: string pathHash: description: Resource path Hash // 资源路径哈希 example: hash123 type: string vault: description: Vault name // 保险库名称 example: defaultVault type: string required: - path - pathHash - vault type: object dto.ShareCreateResponse: properties: expiresAt: description: Expiration time // 过期时间 type: string id: description: ID of the note or file table (primary resource ID) // 笔记或文件表 ID(主资源 ID) type: integer isPassword: description: Whether password is set // 是否设置了密码 type: boolean shortLink: description: Short link // 短链 type: string token: description: Share Token // 分享 Token type: string type: description: 'Resource type: note or file // 资源类型:笔记(note)或文件(file)' type: string type: object dto.ShareListItem: properties: createdAt: description: Created at // 创建时间 type: string expiresAt: description: Expiration time // 过期时间 type: string id: description: Share ID // 分享记录 ID type: integer isPassword: description: Whether password is set // 是否设置了密码 type: boolean lastViewedAt: description: Last viewed time // 最后访问时间 type: string notePath: description: Note path, for frontend share filter matching // 笔记路径,用于前端分享筛选匹配 type: string res: additionalProperties: items: type: string type: array description: Authorized resources // 资源授权列表 type: object shortLink: description: Short link // 短链 type: string status: description: 'Status: 1-Active, 2-Cancelled // 状态: 1-有效, 2-已撤销' type: integer title: description: Resource title (note title or file name) // 资源标题(笔记标题或文件名) type: string uid: description: User ID // 用户 ID type: integer updatedAt: description: Updated at // 更新时间 type: string url: description: 'Share URL (path format: /id/token) // 分享 URL (路径格式: /id/token)' type: string vaultName: description: Vault name where the note belongs // 笔记所属仓库名 type: string viewCount: description: View count // 访问次数 type: integer type: object dto.SharePasswordUpdateRequest: properties: password: description: New password // 新密码 example: "123456" type: string path: description: Resource path // 资源路径 example: 未命名.md type: string pathHash: description: Resource path Hash // 资源路径哈希 example: "-677306325" type: string vault: description: Vault name // 保险库名称 example: test type: string required: - path - pathHash - vault type: object dto.ShareShortLinkCreateRequest: properties: is_force: description: Whether to force regeneration // 是否强制重新生成 example: false type: boolean path: description: Path // 路径 example: notes/todo.md type: string pathHash: description: Path hash // 路径哈希 example: '...' type: string url: description: Full share URL from client; if provided, used directly without regenerating token // 客户端传入的完整分享链接,非空时直接使用,不重新生成 token example: https://example.com/share/129/CNmkmQlq0s-4elT3NuZG2w type: string vault: description: Vault name // 库名 example: work type: string required: - path - pathHash - vault type: object dto.StorageDTO: properties: accessKeyId: description: Access key ID // 访问密钥 ID type: string accessKeySecret: description: Access key secret // 访问密钥秘密 type: string accessUrlPrefix: description: Access URL prefix // 访问地址前缀 type: string accountId: description: Account ID // 账户 ID type: string bucketName: description: Bucket name // 存储桶名称 type: string createdAt: description: Created at // 创建时间 type: string customPath: description: Custom path // 自定义路径 type: string endpoint: description: Endpoint // 访问端点 type: string id: description: ID // ID type: integer isEnabled: description: Is enabled // 是否启用 type: boolean password: description: Password // 密码 type: string region: description: Region // 区域 type: string type: description: Storage type // 存储类型 type: string updatedAt: description: Updated at // 更新时间 type: string user: description: Username // 用户名 type: string type: object dto.StoragePostRequest: properties: accessKeyId: description: Access key ID // 访问密钥ID example: "" type: string accessKeySecret: description: Access key secret // 访问密钥秘密 example: "" type: string accessUrlPrefix: description: Access URL prefix // 访问地址前缀 example: https://cdn.com maxLength: 100 minLength: 2 type: string accountId: description: Account ID (R2) // 账户ID r2 example: "123456789" type: string bucketName: description: Bucket name // 存储桶名称 example: my-bucket type: string customPath: description: Custom path // 自定义路径 example: /backups type: string endpoint: description: Endpoint (OSS) // 端点 oss example: oss-cn-hangzhou.aliyuncs.com type: string id: description: ID // ID example: 1 type: integer isEnabled: description: Is enabled // 是否启用 example: 1 type: integer password: description: Password // 密码 example: secret_password type: string region: description: Region (S3) // 区域 s3 example: us-east-1 type: string type: description: Storage type // 类型 example: local-fs minLength: 1 type: string user: description: Username // 访问用户名 example: admin type: string required: - accessUrlPrefix - type type: object dto.SyncLogDTO: properties: action: description: Action type // 操作类型 type: string changedFields: description: Changed fields // 变更字段 type: string clientName: description: Client name // 客户端名称 type: string clientType: description: Client type // 客户端类型 type: string clientVersion: description: Client version // 客户端版本 type: string createdAt: description: Log creation time // 创建时间 type: string message: description: Additional message // 附加消息 type: string path: description: Resource path // 资源路径 type: string pathHash: description: Resource path hash // 路径哈希 type: string size: description: Size in bytes // 大小(字节) type: integer status: description: 'Status: 1 success, 2 failed // 状态' type: integer type: description: Resource type // 资源类型 type: string vaultId: description: Vault ID // 笔记本 ID type: integer type: object dto.UserChangePasswordRequest: properties: confirmPassword: description: Confirm password // 校验密码 example: new_password123 type: string oldPassword: description: Old password // 旧密码 example: old_password123 type: string password: description: New password // 新密码 example: new_password123 type: string required: - confirmPassword - oldPassword - password type: object dto.UserCreateRequest: properties: confirmPassword: description: Confirm password // 校验密码 example: password123 type: string email: description: User email // 用户邮件 example: user@example.com type: string password: description: User password // 用户密码 example: password123 type: string username: description: User name // 用户名 example: username123 type: string required: - confirmPassword - email - password - username type: object dto.UserDTO: properties: avatar: description: Avatar URL or handle // 头像路径或名称 type: string createdAt: description: Account created time // 账号创建时间 type: string email: description: Email address // 邮件地址 type: string token: description: Authentication Token // 认证 Token type: string uid: description: User ID (primary key) // 用户唯一标识(主键) type: integer updatedAt: description: Last updated time // 最后更新时间 type: string username: description: Username // 用户名 type: string type: object dto.UserLoginRequest: properties: credentials: description: Username or Email // 登录凭证(用户名或邮件) example: user@example.com type: string password: description: Password // 密码 example: password123 type: string required: - credentials - password type: object dto.VaultDTO: properties: createdAt: description: Creation time // 创建时间 type: string fileCount: description: Number of files // 文件数量 type: integer fileSize: description: Size of files // 文件大小 type: integer id: description: Vault ID // 保险库 ID type: integer noteCount: description: Number of notes // 笔记数量 type: integer noteSize: description: Size of notes // 笔记大小 type: integer size: description: Total size // 总大小 type: integer updatedAt: description: Updated time // 更新时间 type: string vault: description: Vault name // 保险库名称 type: string type: object dto.VaultPostRequest: properties: id: description: Vault ID (optional for update) // 保险库 ID(可选,用于更新) example: 1 type: integer vault: description: Vault name // 保险库名称 example: MyVault type: string required: - vault type: object dto.VersionDTO: properties: buildTime: description: Build time // 构建时间 type: string gitTag: description: Git tag // Git 标签 type: string pluginVersionNewChangelog: description: New plugin version changelog link // 插件新版本更新日志链接 type: string pluginVersionNewChangelogContent: description: New plugin version changelog content // 插件新版本更新日志内容 type: string pluginVersionNewLink: description: New plugin version link // 插件新版本链接 type: string pluginVersionNewName: description: New plugin version name // 插件新版本名称 type: string version: description: Current version // 当前版本 type: string versionIsNew: description: Is there a new version // 是否有新版本 type: boolean versionNewChangelog: description: New version changelog link // 新版本更新日志链接 type: string versionNewChangelogContent: description: New version changelog content // 新版本更新日志内容 type: string versionNewLink: description: New version download link // 新版本下载链接 type: string versionNewName: description: New version name // 新版本名称 type: string type: object host: localhost:9000 info: contact: email: haierkeys@gmail.com name: Haierkeys url: https://github.com/haierkeys description: This is the Fast Note Sync Service HTTP API. license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html title: Fast Note Sync Service HTTP API version: "1.0" paths: /api/admin/check: get: description: Check if the current logged-in user has system admin privileges produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminCheckResponse' type: object security: - UserAuthToken: [] summary: Check admin permission tags: - Config /api/admin/cloudflared_tunnel_download: get: description: Trigger the download of cloudflared binary for the current platform produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Download cloudflared binary tags: - System /api/admin/config: get: description: Get full system configuration information, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get full admin config tags: - Config post: consumes: - application/json description: Modify full system configuration information, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string - description: Config Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.AdminConfig' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update admin config tags: - Config /api/admin/config/cloudflare: get: description: Get Cloudflare tunnel configuration, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminCloudflareConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get Cloudflare config tags: - Config post: consumes: - application/json description: Modify Cloudflare tunnel configuration, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string - description: Config Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.AdminCloudflareConfig' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminCloudflareConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update Cloudflare config tags: - Config /api/admin/config/ngrok: get: description: Get Ngrok tunnel configuration, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminNgrokConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get Ngrok config tags: - Config post: consumes: - application/json description: Modify Ngrok tunnel configuration, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string - description: Config Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.AdminNgrokConfig' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminNgrokConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update Ngrok config tags: - Config /api/admin/config/user_database: get: description: Get user database configuration information, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminUserDatabaseConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get user database config tags: - Config post: consumes: - application/json description: Modify user database configuration information, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string - description: Config Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.AdminUserDatabaseConfig' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminUserDatabaseConfig' type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update user database config tags: - Config /api/admin/config/user_database/test: post: consumes: - application/json description: Test if the provided database configuration can connect successfully, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string - description: Config Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.AdminUserDatabaseConfig' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Connection failed schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Test user database connection tags: - Config /api/admin/gc: get: description: Manually run Go runtime GC and release memory to OS, requires admin privileges produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Trigger manual GC tags: - System /api/admin/restart: get: description: Gracefully restart the server produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Trigger server restart tags: - System /api/admin/system/info: get: description: Get server runtime, CPU, memory, host and process info, requires admin privileges produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminSystemInfo' type: object security: - UserAuthToken: [] summary: Get system stats tags: - System /api/admin/upgrade: get: description: Download latest version and restart server parameters: - description: Version to upgrade (e.g. 2.0.10 or latest) in: query name: version required: true type: string produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Trigger server upgrade tags: - System /api/admin/ws_clients: get: description: Get a list of all current WebSocket connections, requires admin privileges parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/app.WSClientInfo' type: array type: object "403": description: Insufficient privileges schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get connected WebSocket clients tags: - System /api/backup/config: delete: parameters: - description: Auth Token in: header name: token required: true type: string - description: ID // ID example: 1 in: query name: id type: integer produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Delete backup configuration tags: - Backup post: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Backup Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.BackupConfigRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.BackupConfigDTO' type: object "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update backup configuration tags: - Backup /api/backup/configs: get: parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.BackupConfigDTO' type: array type: object "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get backup configurations tags: - Backup /api/backup/execute: post: parameters: - description: Auth Token in: header name: token required: true type: string - description: Backup Execute Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.BackupExecuteRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Trigger a backup manually tags: - Backup /api/backup/historys: get: parameters: - description: Auth Token in: header name: token required: true type: string - description: Config ID // 配置 ID example: 1 in: query name: configId required: true type: integer - description: Page number // 页码 example: 1 in: query name: page type: integer - description: Page size // 每页大小 example: 10 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.BackupHistoryDTO' type: array type: object type: object "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get backup history list tags: - Backup /api/file: delete: description: Permanently delete a specific attachment record and its physical file parameters: - description: Auth Token in: header name: token required: true type: string - description: File path // 文件路径 example: Image.png in: query name: path required: true type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash required: true type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FileDTO' type: object security: - UserAuthToken: [] summary: Delete attachment tags: - File get: description: Get raw binary data of an attachment by path, supports strong cache control parameters: - description: Auth Token in: header name: token required: true type: string - description: Is in recycle bin // 是否在回收站 example: false in: query name: isRecycle type: boolean - description: File path // 文件路径 example: Image.png in: query name: path required: true type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/octet-stream responses: "200": description: Success schema: type: file security: - UserAuthToken: [] summary: Get attachment content tags: - File /api/file/info: get: description: Get attachment metadata (FileDTO) by path parameters: - description: Auth Token in: header name: token required: true type: string - description: Is in recycle bin // 是否在回收站 example: false in: query name: isRecycle type: boolean - description: File path // 文件路径 example: Image.png in: query name: path required: true type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FileDTO' type: object security: - UserAuthToken: [] summary: Get attachment info tags: - File /api/file/recycle-clear: delete: consumes: - application/json description: Permanently clear selected files from recycle bin parameters: - description: Auth Token in: header name: token required: true type: string - description: Clear Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.FileRecycleClearRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Clear recycle bin tags: - File /api/file/rename: post: consumes: - application/json description: Rename an attachment to a new path parameters: - description: Auth Token in: header name: token required: true type: string - description: Rename Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.FileRenameRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FileDTO' type: object security: - UserAuthToken: [] summary: Rename attachment tags: - File /api/file/restore: put: description: Restore deleted attachment from trash parameters: - description: Auth Token in: header name: token required: true type: string - description: Restore Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.FileRestoreRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FileDTO' type: object security: - UserAuthToken: [] summary: Restore attachment tags: - File /api/files: get: description: Get attachment list for current user with pagination, search, filter, and sort support parameters: - description: Auth Token in: header name: token required: true type: string - description: Is in recycle bin // 是否在回收站 example: false in: query name: isRecycle type: boolean - description: Search keyword // 搜索关键词 example: vacation in: query name: keyword type: string - description: Sort by field // 排序字段 example: mtime in: query name: sortBy type: string - description: Sort order // 排序顺序 example: desc in: query name: sortOrder type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.FileDTO' type: array type: object type: object security: - UserAuthToken: [] summary: Get file list tags: - File /api/folder: delete: consumes: - application/json description: Soft delete a folder by path or pathHash parameters: - description: Auth Token in: header name: token required: true type: string - description: Delete Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.FolderDeleteRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Delete folder tags: - Folder get: description: Get folder info for current user by path or pathHash parameters: - description: Auth Token in: header name: token required: true type: string - description: Folder path // 文件夹路径 example: Projects/Work in: query name: path type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FolderDTO' type: object security: - UserAuthToken: [] summary: Get folder info tags: - Folder post: consumes: - application/json description: Create a new folder or restore a deleted one by path parameters: - description: Auth Token in: header name: token required: true type: string - description: Create Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.FolderCreateRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FolderDTO' type: object security: - UserAuthToken: [] summary: Create folder tags: - Folder /api/folder/files: get: description: List non-deleted files in a specific folder with pagination and sorting parameters: - description: Auth Token in: header name: token required: true type: string - description: Folder path // 文件夹路径 example: Projects in: query name: path type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash type: string - description: Sort by field // 排序字段 example: mtime in: query name: sortBy type: string - description: Sort order // 排序顺序 example: desc in: query name: sortOrder type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.FileDTO' type: array type: object type: object security: - UserAuthToken: [] summary: List files in folder tags: - Folder /api/folder/notes: get: description: List non-deleted notes in a specific folder with pagination and sorting parameters: - description: Auth Token in: header name: token required: true type: string - description: Folder path // 文件夹路径 example: Projects in: query name: path type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash type: string - description: Sort by field // 排序字段 example: mtime in: query name: sortBy type: string - description: Sort order // 排序顺序 example: desc in: query name: sortOrder type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.NoteDTO' type: array type: object type: object security: - UserAuthToken: [] summary: List notes in folder tags: - Folder /api/folder/tree: get: description: Get the complete folder tree structure for a vault parameters: - description: Auth Token in: header name: token required: true type: string - description: Tree depth // 树深度 example: 3 in: query name: depth type: integer - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.FolderTreeResponse' type: object security: - UserAuthToken: [] summary: Get folder tree tags: - Folder /api/folders: get: description: Get folder list for current user by parent path or pathHash parameters: - description: Auth Token in: header name: token required: true type: string - description: Folder path // 文件夹路径 example: Projects in: query name: path type: string - description: Path hash // 路径哈希 example: fhash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.FolderDTO' type: array type: object security: - UserAuthToken: [] summary: Get folder list tags: - Folder /api/git-sync/config: delete: parameters: - description: Auth Token in: header name: token required: true type: string - description: Git Sync ID in: body name: params required: true schema: $ref: '#/definitions/dto.GitSyncDeleteRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Delete git sync configuration tags: - GitSync post: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Git Sync Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.GitSyncConfigRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.GitSyncConfigDTO' type: object "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update git sync configuration tags: - GitSync /api/git-sync/config/clean: delete: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Clean Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.GitSyncCleanRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Clean local git workspace tags: - GitSync /api/git-sync/config/execute: post: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Execute Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.GitSyncExecuteRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Trigger a manual git sync tags: - GitSync /api/git-sync/configs: get: parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.GitSyncConfigDTO' type: array type: object "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get git sync configurations tags: - GitSync /api/git-sync/histories: get: parameters: - description: Auth Token in: header name: token required: true type: string - in: query name: configId type: integer - in: query name: page type: integer - in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.GitSyncHistoryDTO' type: array type: object type: object "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get git sync histories tags: - GitSync /api/git-sync/validate: post: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Validation Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.GitSyncValidateRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Validate git sync parameters tags: - GitSync /api/health: get: description: Check service health status, including database connection produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/api_router.HealthResponse' summary: Health check tags: - System /api/note: delete: description: Move note to trash parameters: - description: Auth Token in: header name: token required: true type: string - description: Note path // 笔记路径 example: ReadMe.md in: query name: path required: true type: string - description: Path hash // 路径哈希 example: hash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Delete note tags: - Note get: description: Get specific note content and metadata by path or path hash parameters: - description: Auth Token in: header name: token required: true type: string - description: Is in recycle bin // 是否在回收站 example: false in: query name: isRecycle type: boolean - description: Note path // 笔记路径 example: ReadMe.md in: query name: path required: true type: string - description: Path hash // 路径哈希 example: hash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteWithFileLinksResponse' type: object security: - UserAuthToken: [] summary: Get note details tags: - Note post: consumes: - application/json description: Handle note creation, modification, or renaming (identified by path change) parameters: - description: Auth Token in: header name: token required: true type: string - description: Note Content in: body name: params required: true schema: $ref: '#/definitions/dto.NoteModifyOrCreateRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Create or update note tags: - Note /api/note/append: post: consumes: - application/json description: Append content to the end of a note parameters: - description: Auth Token in: header name: token required: true type: string - description: Append Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteAppendRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Append content to note tags: - Note /api/note/backlinks: get: description: Get all other notes that link to the specified note parameters: - description: Auth Token in: header name: token required: true type: string - description: Note path // 笔记路径 example: ReadMe.md in: query name: path required: true type: string - description: Path hash // 路径哈希 example: hash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.NoteLinkItem' type: array type: object security: - UserAuthToken: [] summary: Get backlinks tags: - Note /api/note/frontmatter: patch: consumes: - application/json description: Update or delete note frontmatter fields parameters: - description: Auth Token in: header name: token required: true type: string - description: Frontmatter Modification Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NotePatchFrontmatterRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Modify note frontmatter tags: - Note /api/note/histories: get: description: Get all history records for a specific note with pagination parameters: - description: Auth Token in: header name: token required: true type: string - description: Is in recycle bin // 是否在回收站 example: false in: query name: isRecycle type: boolean - description: Note path // 笔记路径 example: ReadMe.md in: query name: path required: true type: string - description: Path hash // 路径哈希 example: hash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.NoteHistoryDTO' type: array type: object type: object security: - UserAuthToken: [] summary: Get note history list tags: - Note History /api/note/history: get: description: Get specific note history content by history record ID parameters: - description: Auth Token in: header name: token required: true type: string - description: History Record ID format: int64 in: query name: id required: true type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteHistoryDTO' type: object security: - UserAuthToken: [] summary: Get note history details tags: - Note History /api/note/history/restore: put: consumes: - application/json description: Restore note content to a specific history version parameters: - description: Auth Token in: header name: token required: true type: string - description: Restore Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteHistoryRestoreRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Restore note from history tags: - Note History /api/note/move: post: consumes: - application/json description: Move a note to a new path parameters: - description: Auth Token in: header name: token required: true type: string - description: Move Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteMoveRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Move note tags: - Note /api/note/outlinks: get: description: Get other notes that the specified note links to parameters: - description: Auth Token in: header name: token required: true type: string - description: Note path // 笔记路径 example: ReadMe.md in: query name: path required: true type: string - description: Path hash // 路径哈希 example: hash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.NoteLinkItem' type: array type: object security: - UserAuthToken: [] summary: Get outgoing links tags: - Note /api/note/prepend: post: consumes: - application/json description: Insert content at the beginning of a note (after frontmatter) parameters: - description: Auth Token in: header name: token required: true type: string - description: Prepend Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NotePrependRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Prepend content to note tags: - Note /api/note/recycle-clear: delete: consumes: - application/json description: Permanently clear selected notes from recycle bin parameters: - description: Auth Token in: header name: token required: true type: string - description: Clear Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteRecycleClearRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Clear recycle bin tags: - Note /api/note/rename: post: consumes: - application/json description: Rename a note to a new path parameters: - description: Auth Token in: header name: token required: true type: string - description: Rename Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteRenameRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Rename note tags: - Note /api/note/replace: post: consumes: - application/json description: Perform find and replace operation in a note, supporting regular expressions parameters: - description: Auth Token in: header name: token required: true type: string - description: Find and Replace Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteReplaceRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteReplaceResponse' type: object security: - UserAuthToken: [] summary: Find and replace in note tags: - Note /api/note/restore: put: description: Restore deleted note from trash parameters: - description: Auth Token in: header name: token required: true type: string - description: Restore Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.NoteRestoreRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - UserAuthToken: [] summary: Restore note tags: - Note /api/notes: get: description: Get note list for current user with pagination parameters: - description: Auth Token in: header name: token required: true type: string - description: Is in recycle bin // 是否在回收站 example: false in: query name: isRecycle type: boolean - description: Search keyword // 搜索关键词 example: todo in: query name: keyword type: string - description: Comma-separated exact path list for share filter // 逗号分隔的精确路径列表,用于分享筛选 example: note1.md,note2.md in: query name: paths type: string - description: Whether to search content // 是否搜索内容 example: true in: query name: searchContent type: boolean - description: Search mode (path, content) // 搜索模式(路径、内容) example: content in: query name: searchMode type: string - description: Sort by field // 排序字段 example: mtime in: query name: sortBy type: string - description: Sort order // 排序顺序 example: desc in: query name: sortOrder type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.NoteNoContentDTO' type: array type: object type: object security: - UserAuthToken: [] summary: Get note list tags: - Note /api/notes/share-paths: get: parameters: - description: Auth Token in: header name: token required: true type: string - description: Vault name in: query name: vault required: true type: string responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: type: string type: array type: object security: - UserAuthToken: [] summary: Get active shared note paths tags: - Share /api/setting: delete: consumes: - application/json description: Soft delete a setting by path or pathHash parameters: - description: Auth Token in: header name: token required: true type: string - description: Delete Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.SettingDeleteRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Delete setting tags: - Setting get: description: Get setting info for current user by path or pathHash parameters: - description: Auth Token in: header name: token required: true type: string - description: Setting path // 配置路径 example: User/Theme in: query name: path type: string - description: Path hash // 路径哈希 example: hash123 in: query name: pathHash type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.SettingDTO' type: object security: - UserAuthToken: [] summary: Get setting info tags: - Setting post: consumes: - application/json description: Create a new setting or update an existing one parameters: - description: Auth Token in: header name: token required: true type: string - description: Create/Update Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.SettingModifyOrCreateRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.SettingDTO' type: object security: - UserAuthToken: [] summary: Create or update setting tags: - Setting /api/setting/rename: post: consumes: - application/json description: Rename a setting and update its path and pathHash parameters: - description: Auth Token in: header name: token required: true type: string - description: Rename Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.SettingRenameRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.SettingDTO' type: object security: - UserAuthToken: [] summary: Rename setting tags: - Setting /api/settings: get: description: Get setting list for current user with pagination and keyword filtering parameters: - description: Auth Token in: header name: token required: true type: string - description: Keyword // 关键词 example: User/ in: query name: keyword type: string - description: Vault name // 保险库名称 example: MyVault in: query name: vault required: true type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.SettingDTO' type: array type: object type: object security: - UserAuthToken: [] summary: Get setting list tags: - Setting /api/share: delete: consumes: - application/json description: Cancel a share by ID or path parameters parameters: - description: Auth Token in: header name: token required: true type: string - description: Cancel Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.ShareCancelRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Cancel share tags: - Share get: description: Get share token and info by vault and path parameters: - description: Auth Token in: header name: token required: true type: string - description: Resource path // 资源路径 example: ReadMe.md in: query name: path required: true type: string - description: Resource path Hash // 资源路径哈希 example: hash123 in: query name: pathHash required: true type: string - description: Vault name // 保险库名称 example: defaultVault in: query name: vault required: true type: string responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.ShareCreateResponse' type: object security: - UserAuthToken: [] summary: Query share by path tags: - Share post: consumes: - application/json description: Create a share token for a specific note or attachment, automatically resolve attachment references and authorize parameters: - description: Auth Token in: header name: token required: true type: string - description: Share Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.ShareCreateRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.ShareCreateResponse' type: object security: - UserAuthToken: [] summary: Create resource share tags: - Share /api/share/file: get: description: Get raw binary data of a specific attachment via share token parameters: - description: Auth Token in: header name: Share-Token required: true type: string - description: Resource ID // 资源 ID example: 1 in: query name: id required: true type: integer - description: Share password // 分享密码 example: "123456" in: query name: password type: string produces: - application/octet-stream responses: "200": description: Success schema: type: file security: - ShareAuthToken: [] summary: Get shared attachment content tags: - Share /api/share/note: get: description: Get specific note content (restricted read-only access) via share token parameters: - description: Auth Token in: header name: Share-Token required: true type: string - description: Resource ID // 资源 ID example: 1 in: query name: id required: true type: integer - description: Share password // 分享密码 example: "123456" in: query name: password type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.NoteDTO' type: object security: - ShareAuthToken: [] summary: Get shared note details tags: - Share /api/share/password: post: consumes: - application/json description: Set or update password for a share record parameters: - description: Auth Token in: header name: token required: true type: string - description: Update Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.SharePasswordUpdateRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Update share password tags: - Share /api/share/short_link: post: consumes: - application/json description: Call sink.cool API to generate a short link for a given share record parameters: - description: Auth Token in: header name: token required: true type: string - description: Short Link Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.ShareShortLinkCreateRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: type: string type: object security: - UserAuthToken: [] summary: Create short link for share tags: - Share /api/shares: get: description: Get all active and inactive shares of the user, supports sorting and pagination parameters: - description: Auth Token in: header name: token required: true type: string - description: 'Sort field: created_at, updated_at, expires_at (default: created_at)' in: query name: sort_by type: string - description: 'Sort direction: asc or desc (default: desc)' in: query name: sort_order type: string - description: Page number in: query name: page type: integer - description: Page size in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.ShareListItem' type: array type: object type: object security: - UserAuthToken: [] summary: List shares tags: - Share /api/storage: delete: parameters: - description: Auth Token in: header name: token required: true type: string - description: Storage ID format: int64 in: query name: id required: true type: integer produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Delete storage configuration tags: - Storage get: parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.StorageDTO' type: array type: object security: - UserAuthToken: [] summary: Get storage configuration list tags: - Storage post: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Storage Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.StoragePostRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.StorageDTO' type: object security: - UserAuthToken: [] summary: Create or update storage configuration tags: - Storage /api/storage/enabled_types: get: description: 'Get list of enabled storage types. Possible values: localfs, oss, s3, r2, minio, webdav' produces: - application/json responses: "200": description: 'Success. Data contains: localfs, oss, s3, r2, minio, webdav' schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: type: string type: array type: object summary: Get enabled storage types tags: - Storage /api/storage/validate: post: consumes: - application/json parameters: - description: Auth Token in: header name: token required: true type: string - description: Storage Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.StoragePostRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Params schema: $ref: '#/definitions/app.Res' "401": description: Token Required schema: $ref: '#/definitions/app.Res' "500": description: Internal Server Error schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Validate storage connection tags: - Storage /api/support: get: description: Get support records for the specified language with pagination and sorting parameters: - description: 'Language code (default: en)' in: query name: lang type: string - description: Sort by field (amount, time, name, item) in: query name: sortBy type: string - description: Sort order (asc, desc) in: query name: sortOrder type: string - description: Page number in: query name: page type: integer - description: Page size in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/app.ListRes' type: object summary: Get support records tags: - System /api/sync-logs: get: description: Get sync log list for current user with optional type/action filters and pagination parameters: - description: Auth Token in: header name: token required: true type: string - description: Action type // 操作类型 example: modify in: query name: action type: string - description: 'Resource type: note / file / setting / folder // 资源类型' example: note in: query name: type type: string - description: Vault name (optional filter) // 保险库名称(可选过滤) example: MyVault in: query name: vault type: string - description: Page number // 页码 in: query name: page type: integer - description: Page size // 每页数量 in: query name: pageSize type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: allOf: - $ref: '#/definitions/app.ListRes' - properties: list: items: $ref: '#/definitions/dto.SyncLogDTO' type: array type: object type: object security: - UserAuthToken: [] summary: Get sync log list tags: - Sync Log /api/user/change_password: post: consumes: - application/json description: |- Handle password change request for current user, validate old password and update new password. 处理当前用户的修改密码请求,验证旧密码并更新新密码。 parameters: - description: Auth Token in: header name: token required: true type: string - description: Change Password Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.UserChangePasswordRequest' produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' "400": description: Invalid Parameters / Old Password Incorrect schema: $ref: '#/definitions/app.Res' "401": description: Unauthorized schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Change user password tags: - User /api/user/info: get: consumes: - application/json description: |- Handle request to get current user info. 处理获取当前用户信息的请求。 parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.UserDTO' type: object "401": description: Unauthorized schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Get user info tags: - User /api/user/login: post: consumes: - application/json description: |- Handle user login HTTP request, validate parameters and return auth token. 处理用户登录 HTTP 请求,验证参数并返回认证 Token。 parameters: - description: Login Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.UserLoginRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.UserDTO' type: object "400": description: Invalid Parameters / Invalid Credentials schema: $ref: '#/definitions/app.Res' summary: User login tags: - User /api/user/register: post: consumes: - application/json description: |- Handle user registration HTTP request, validate parameters and call UserService. Registration may be disabled in server settings. 处理用户注册 HTTP 请求,验证参数并调用 UserService。注册功能可能在服务器设置中被禁用。 parameters: - description: Register Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.UserCreateRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.UserDTO' type: object "400": description: Invalid Parameters / Registration Disabled / User Already Exists schema: $ref: '#/definitions/app.Res' summary: User registration tags: - User /api/vault: delete: description: Permanently delete a specific note vault and all associated notes and attachments parameters: - description: Auth Token in: header name: token required: true type: string - description: Vault ID // 保险库 ID example: 1 in: query minimum: 1 name: id required: true type: integer produces: - application/json responses: "200": description: Success schema: $ref: '#/definitions/app.Res' security: - UserAuthToken: [] summary: Delete vault tags: - Vault get: description: Get all note vaults for current user parameters: - description: Auth Token in: header name: token required: true type: string produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: items: $ref: '#/definitions/dto.VaultDTO' type: array type: object security: - UserAuthToken: [] summary: Get vault list tags: - Vault post: consumes: - application/json description: Be used to create a new vault or update an existing vault configuration based on the ID in the request parameters parameters: - description: Auth Token in: header name: token required: true type: string - description: Vault Parameters in: body name: params required: true schema: $ref: '#/definitions/dto.VaultPostRequest' produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.VaultDTO' type: object security: - UserAuthToken: [] summary: Create or update vault tags: - Vault /api/vault/get: get: description: Get specific vault configuration details by vault ID parameters: - description: Auth Token in: header name: token required: true type: string - description: Vault ID format: int64 in: query name: id required: true type: integer produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.VaultDTO' type: object security: - UserAuthToken: [] summary: Get vault details tags: - Vault /api/version: get: description: Get current server software version, Git tag, and build time produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.VersionDTO' type: object summary: Get server version info tags: - System /api/webgui/config: get: description: Get non-sensitive configuration required for frontend display, such as font settings, registration status, etc. produces: - application/json responses: "200": description: Success schema: allOf: - $ref: '#/definitions/app.Res' - properties: data: $ref: '#/definitions/dto.AdminWebGUIConfig' type: object summary: Get WebGUI basic config tags: - Config securityDefinitions: ShareAuthToken: in: header name: Share-Token type: apiKey UserAuthToken: in: header name: token type: apiKey swagger: "2.0" ================================================ FILE: docs/test_ws_debug.html ================================================ FNSS Websocket Debug Platform

FNSS Websocket Debug Platform

Config
Note
File
Folder

🔐 Connection & Auth

⚙️ Setting Operations

📜 Communication Log

================================================ FILE: docs/websocket_integration.md ================================================ # WebSocket 同步协议更新说明 (2026-03-05) 本文档详细说明了近期 WebSocket 协议的变更,主要涉及笔记、附件和配置同步消息中 `lastTime` 字段的补充。 ## 1. 核心变更:引入 `lastTime` 为了增强前端增量同步的可靠性,我们在多个资源变更消息中补充了 `lastTime` 字段。 - **字段名**: `lastTime` (对应后端 `UpdatedTimestamp`) - **数据类型**: `int64` (毫秒级时间戳) - **物理意义**: 该资源记录在数据库中的最后更新时间。前端在进行增量同步请求时,应记录此值,并作为下次同步请求的起点。 --- ## 2. 字段更新详情 ### 2.1 笔记消息 (Note) | 消息 Action (Action) | 新增字段 | 说明 | | :--- | :--- | :--- | | `NoteSyncRename` | `lastTime` | 笔记重命名后的同步消息 | | `NoteSyncMtime` | `lastTime` | 笔记修改时间变更(无需下载内容时) | | `NoteSyncDelete` | `lastTime` | 笔记已删除的同步消息 | | `NoteSyncModify` | `lastTime` | (固有) 笔记创建或更新消息 | ### 2.2 附件/文件消息 (File) | 消息 Action (Action) | 新增字段 | 说明 | | :--- | :--- | :--- | | `FileSyncRename` | `lastTime` | 附件重命名后的同步消息 | | `FileSyncMtime` | `lastTime` | 附件修改时间变更 | | `FileSyncDelete` | `lastTime` | 附件已删除的同步消息 | | `FileSyncUpdate` | `lastTime` | (固有) 附件创建或更新消息 | ### 2.3 配置消息 (Setting) | 消息 Action (Action) | 新增字段 | 说明 | | :--- | :--- | :--- | | `SettingSyncMtime` | `lastTime` | 配置修改时间变更 | | `SettingSyncDelete` | `lastTime`, `pathHash`, `ctime`, `mtime` | 配置已删除同步(结构大幅增强以保持一致性) | | `SettingSyncModify` | `lastTime` | (固有) 配置创建或更新消息 | --- ## 3. 详细结构定义 (JSON 示例) ### 配置删除消息示例 (结构增强) ```json { "action": "SettingSyncDelete", "data": { "path": "User/Theme", "pathHash": "shash789", "ctime": 1700000000, "mtime": 1700000000, "lastTime": 1700000001 } } ``` ### 笔记重命名消息示例 ```json { "action": "NoteSyncRename", "data": { "path": "NewName.md", "pathHash": "nfhash123", "oldPath": "OldName.md", "oldPathHash": "ofhash456", "lastTime": 1700001000, ... } } ``` --- ## 4. 前端对接建议 1. **状态更新**: 当收到上述任何带有 `lastTime` 字段的消息时,前端应更新本地缓存中对应资源的 `lastTime` 属性。 2. **同步基准**: 在调用 `NoteSync`, `FileSync`, `SettingSync` 时,参数中的 `lastTime` 应取本地所有资源(包含已逻辑删除的资源)中最大的那个 `lastTime` 值,以确保不遗漏任何服务端变更。 3. **删除处理**: `SettingSyncDelete` 现在的字段更丰富(补全了 `pathHash` 等),前端可以更统一地处理各类资源的删除逻辑。 ================================================ FILE: docs/ws_api.md ================================================ # WebSocket API 全量对接文档 (100% 完整版) 本手册为前端开发人员提供服务端 WebSocket 接口的**完全定义**。涵盖所有模块(笔记、文件夹、文件、设置)的请求、响应、推送消息及详细字段结构。 --- ## 1. 协议规范 ### 1.1 连接 - **Endpoint**: `GET /api/user/sync` - **协议升级**: 标准 WebSocket (RFC 6455)。 ### 1.2 消息封装格式 WebSocket 文本帧统一使用 `Action|JSON` 字符串格式。 - **示例**: `Authorization|"token_string_here"` ### 1.3 统一响应外壳 (`Res`) 服务端发回的 JSON 消息体(管道符 `|` 之后的部分)结构如下: | JSON Key | 类型 | 说明 | |:----------|:-----------|:--------------------------------------------------------------| | `code` | int | 业务状态码 (1: 成功, 6: 无需同步, 441: 冲突, 305: 参数错误等) | | `status` | bool | 逻辑成功标识 | | `message` | string | 错误或提示信息 (i18n) | | `data` | object/any | 具体的业务负载内容 | | `details` | string | 详细错误信息 (omitempty) | | `vault` | string | 所属仓库名称 (omitempty) | | `context` | string | 上下文标识 (omitempty) | --- ## 2. 基础控制消息 ### 2.1 鉴权 (Authorization) - **流向**: 客户端 -> 服务端 - **Action**: `Authorization` - **内容 (Data)**: Token 字符串文本。 - **响应 (Data)**: - `version`: string (服务端版本号) - `gitTag`: string (Git Tag 信息) - `buildTime`: string (编译时间) ### 2.2 客户端信息声明 (ClientInfo) - **流向**: 客户端 -> 服务端 - **Action**: `ClientInfo` - **请求内容 (Data)**: | 字段 | 类型 | 必填 | 说明 | |:----------------------|:-------|:-----|:-----------------------------------------| | `name` | string | 是 | 客户端名称 (设备名) | | `version` | string | 是 | 客户端当前版本 | | `type` | string | 是 | 客户端类型 (如 `obsidianPlugin`) | | `offlineSyncStrategy` | string | 否 | 策略: `newTimeMerge` / `ignoreTimeMerge` | - **响应 (Data)**: | 字段 | 类型 | 说明 | |:-----------------------|:-------|:---------------------| | `versionIsNew` | bool | 服务端版本是否有更新 | | `versionNewName` | string | 服务端新版本号 | | `versionNewLink` | string | 服务端新版本下载链接 | | `pluginVersionIsNew` | bool | 插件版本是否有更新 | | `pluginVersionNewName` | string | 插件新版本号 | | `pluginVersionNewLink` | string | 插件新版本下载链接 | --- ## 3. 笔记模块 (Notes) ### 3.1 核心动作对照表 | 流向 | Action | 说明 | 数据结构 (Data) | |:-------|:-------------------|:-------------------|:----------------------------| | C -> S | `NoteSync` | 请求笔记增量同步 | `NoteSyncRequest` | | C -> S | `NoteModify` | 提交修改/新建 | `NoteModifyOrCreateRequest` | | C -> S | `NoteDelete` | 删除笔记 | `NoteDeleteRequest` | | C -> S | `NoteRename` | 重命名笔记 | `NoteRenameRequest` | | C -> S | `NoteCheck` | 检查笔记更新必要性 | `NoteUpdateCheckRequest` | | C -> S | `NoteRePush` | 请求重推某笔记 | `NoteGetRequest` | | S -> C | `NoteSyncModify` | 推送/同步笔记详情 | `NoteSyncModifyMessage` | | S -> C | `NoteSyncDelete` | 指示客户端删除 | `NoteSyncDeleteMessage` | | S -> C | `NoteSyncRename` | 指示客户端重命名 | `NoteSyncRenameMessage` | | S -> C | `NoteSyncMtime` | 仅同步修改时间 | `NoteSyncMtimeMessage` | | S -> C | `NoteSyncNeedPush` | 要求客户端上传本地 | `NoteSyncNeedPushMessage` | | S -> C | `NoteSyncEnd` | 完成同步响应 | `NoteSyncEndMessage` | ### 3.2 详细 DTO 定义 #### `NoteSyncEndMessage` | 字段 | 类型 | 说明 | |:---------------------|:------|:----------------------------------------------------| | `lastTime` | int64 | **[关键]** 本次同步后的最新时间戳 (毫秒),下传传此值 | | `needUploadCount` | int64 | 需要客户端上传的笔记总数 | | `needModifyCount` | int64 | 服务端下发修改的笔记总数 | | `needSyncMtimeCount` | int64 | 仅同步时间的笔记总数 | | `needDeleteCount` | int64 | 指示删除的笔记总数 | | `messages` | array | 变更消息队列,详见第 7 章节 | #### `NoteModifyOrCreateRequest` | 字段 | 类型 | 说明 | |:--------------|:-------|:----------------------------------| | `vault` | string | **[必填]** 仓库名 | | `path` | string | **[必填]** 笔记完整路径 | | `pathHash` | string | 路径哈希 | | `content` | string | 笔记文本全文 | | `contentHash` | string | 内容哈希 | | `baseHash` | string | 修改前的基础哈希 (用于冲突合并) | | `ctime` | int64 | 创建时间 (秒) | | `mtime` | int64 | 修改时间 (秒) | | `createOnly` | bool | 设置为 true 时,若笔记已存在则报错 | --- ## 4. 文件夹模块 (Folders) ### 4.1 核心动作 - `FolderSync` (C->S): `{ "vault": string, "lastTime": int64, "folders": Array }` - `FolderModify` (C->S): `{ "vault": string, "path": string }` - `FolderDelete` (C->S): `{ "vault": string, "path": string, "pathHash": string }` - `FolderRename` (C->S): `{ "vault": string, "oldPath": string, "path": string, ... }` - `FolderSyncEnd` (S->C): `{ "lastTime": int64, "needModifyCount": int, "needDeleteCount": int, "messages": [] }` --- ## 5. 文件模块 (Files) ### 5.1 二进制分片上传逻辑 (BC Frame) 1. 客户端发送 `FileUploadCheck` (JSON)。 2. 服务端响应 `FileUpload` (JSON),返回 `sessionId` 和 `chunkSize`。 3. 客户端循环发送 **二进制帧**。帧前缀固定为 `BC` (ASCII 0x42 0x43)。 - **帧格式 (Binary)**: `[36字节 SessionID][4字节 uint32 大端序 ChunkIndex][原始分片数据]` ### 5.2 文件同步动作汇总 - `FileSyncUpdate` (推送): `{ "path", "pathHash", "contentHash", "size", "ctime", "mtime", "lastTime" }` - `FileSyncEnd` (推送): `{ "lastTime", "needUploadCount", "needModifyCount", "needSyncMtimeCount", "needDeleteCount", "messages" }` --- ## 6. 设置模块 (Settings) - `SettingSync`: `{ "vault": string, "settings": Array, "cover": bool }` - `SettingSyncEnd`: `{ "lastTime", "needUploadCount", "needModifyCount", "needSyncMtimeCount", "needDeleteCount", "messages" }` --- ## 7. 队列消息结构 (`WSQueuedMessage`) 在所有 `*SyncEnd` 消息的 `messages` 数组中,每个项的结构为: ```json { "action": "NoteSyncModify", // 具体的推送 Action 名 "data": { ... } // 该 Action 对应的具体 Data 结构 } ``` --- ## 8. WebSocket 专属状态码汇总 | Code | 说明 | |:------|:---------------------------------------------------------------| | `1` | 成功 (Success) | | `6` | 服务端数据与客户端一致,无需更新 (SuccessNoUpdate) | | `305` | 客户端提交的 JSON 参数不符合 binding 校验 (ErrorInvalidParams) | | `433` | 笔记保存入库失败 (ErrorNoteModifyOrCreateFailed) | | `441` | 发生内容冲突,且无法自动合并 (ErrorNoteConflict) | | `463` | 分片上传 Session 已过期或无效 (ErrorFileUploadSessionNotFound) | | `490` | 同步逻辑冲突 (ErrorSyncConflict) | --- *注:文档中所有 `int64` 时间戳除 `lastTime`(毫秒) 外均默认为 **秒**。* ================================================ FILE: docs/ws_setting_clear_api.md ================================================ # WebSocket API: 清理配置 (SettingClear) 对接文档 本文档描述了如何通过 WebSocket 接口清理指定笔记库(Vault)的所有配置信息。 ## 1. 客户端请求 (Client -> Server) 客户端发送一个 JSON 消息来请求清理指定笔记本的配置。 - **Action**: `SettingClear` - **Data 结构**: `SettingClearRequest` ### 消息示例 ```json { "action": "SettingClear", "data": { "vault": "我的笔记库" } } ``` ### 字段说明 | 字段名 | 类型 | 必填 | 说明 | |:-------|:-------|:-----|:--------------------------| | vault | string | 是 | 要清理的笔记本(Vault)名称 | --- ## 2. 服务端广播 (Server -> Other Clients) 为了保持多端同步,服务端在处理成功后,会向该用户**除发起者外**的所有其他在线客户端广播清理消息。 - **Action**: `SettingSyncClear` ### 广播消息示例 ```json { "code": 200, "msg": "success", "action": "SettingSyncClear", "vault": "我的笔记库", "data": null } ``` ### 前端处理建议 1. 当收到 `action: "SettingSyncClear"` 时,前端应清空本地对应 `vault` 的所有配置缓存或存储。 2. 建议在发起 `SettingClear` 请求前,显式弹出确认框提示用户该操作为**物理清理**且不可逆。 --- ## 3. 错误处理 如果发生错误(如 Vault 不存在或数据库异常),服务端会返回对应的错误代码: | Code | 说明 | |:-----|:----------------------------| | 400 | 参数错误 (如未传 vault) | | 473 | 清理失败 (数据库或系统异常) | | 401 | 认证失效 | ================================================ FILE: frontend/assets/alert-dialog-CfMssux5.js ================================================ import{c as a,ar as e,r as s,j as t,as as l,at as d,f as o,au as m,av as r,aw as i,ax as c,ay as f,az as n}from"./font-loader-CIrh3KnA.js"; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const p=a("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]),x=a("Laptop",[["path",{d:"M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16",key:"tarvll"}]]),y=a("Smartphone",[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2",key:"1yt0o3"}],["path",{d:"M12 18h.01",key:"mhygvu"}]]),N=e,u=l,g=s.forwardRef(({className:a,...e},s)=>t.jsx(n,{className:o("fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",a),...e,ref:s})); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */g.displayName=n.displayName;const h=s.forwardRef(({className:a,...e},s)=>t.jsxs(u,{children:[t.jsx(g,{}),t.jsx(d,{ref:s,className:o("fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",a),...e})]}));h.displayName=d.displayName;const j=({className:a,...e})=>t.jsx("div",{className:o("flex flex-col space-y-2 text-center sm:text-left",a),...e});j.displayName="AlertDialogHeader";const w=({className:a,...e})=>t.jsx("div",{className:o("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",a),...e});w.displayName="AlertDialogFooter";const b=s.forwardRef(({className:a,...e},s)=>t.jsx(m,{ref:s,className:o("text-lg font-semibold",a),...e}));b.displayName=m.displayName;const k=s.forwardRef(({className:a,...e},s)=>t.jsx(r,{ref:s,className:o("text-sm text-muted-foreground",a),...e}));k.displayName=r.displayName;const v=s.forwardRef(({className:a,...e},s)=>t.jsx(f,{ref:s,className:o(c(),a),...e}));v.displayName=f.displayName;const z=s.forwardRef(({className:a,...e},s)=>t.jsx(i,{ref:s,className:o(c({variant:"outline"}),"mt-2 sm:mt-0",a),...e}));z.displayName=i.displayName;export{N as A,p as G,x as L,y as S,h as a,j as b,b as c,k as d,w as e,z as f,v as g}; ================================================ FILE: frontend/assets/auth-form-BjZ9qVzL.js ================================================ import{r as e,u as s,a,e as t,b as r,G as i,j as o,N as l,S as n,M as c,H as d,K as m,I as u,t as h}from"./font-loader-CIrh3KnA.js";import{o as p,c as g,s as x,u as w,t as f}from"./zod-B54Zg8Xp.js";import{G as j}from"./github-Bzk-4SPC.js";import{W as b,A as N,m as v}from"./main-BIi-kGYY.js";const y=e=>x().min(6,e("ui.auth.passwordMinLength"));p({oldPassword:x().optional(),password:x().optional(),confirmPassword:x().optional()});const S={hidden:{opacity:0,y:5},visible:{opacity:1,y:0,transition:{duration:.3,ease:"easeOut"}},exit:{opacity:0,y:-5,transition:{duration:.2}}};function k({onSuccess:k,registerIsEnable:P=!0}){const{t:I}=s(),{isLoading:L,login:F,registerUser:z}=function(){const[i,o]=e.useState(!1),{t:l}=s();return{isLoading:i,login:async e=>{o(!0);try{const s=await fetch(a(t.API_URL+"/api/user/login"),{method:"POST",body:JSON.stringify(e),headers:r({token:null})});if(!s.ok)throw new Error("Network response was not ok");const i=await s.json();return i.code<100&&i.code>0?(localStorage.setItem("token",i.data.token),localStorage.setItem("username",i.data.username),localStorage.setItem("uid",i.data.uid),localStorage.setItem("avatar",i.data.avatar),localStorage.setItem("email",i.data.email),{success:!0,message:i.data.message}):{success:!1,error:i.details?`${i.message}: ${i.details}`:i.message}}catch(s){return{success:!1,error:l("ui.auth.loginRequestFailed")}}finally{o(!1)}},registerUser:async e=>{o(!0);try{const s=await fetch(a(t.API_URL+"/api/user/register"),{method:"POST",body:JSON.stringify(e),headers:r({token:null})});if(!s.ok)throw new Error("Network response was not ok");const i=await s.json();return i.code<100&&i.code>0?(localStorage.setItem("token",i.data.token),localStorage.setItem("username",i.data.username),localStorage.setItem("uid",i.data.uid),localStorage.setItem("avatar",i.data.avatar),localStorage.setItem("email",i.data.email),{success:!0}):{success:!1,error:i.details?`${i.message}: ${i.details}`:i.message}}catch(s){return{success:!1,error:l("ui.auth.registerRequestFailed")}}finally{o(!1)}}}}(),{theme:M,setTheme:T,resolvedTheme:C}=i(),[O,R]=e.useState("login"),$=(_=I,p({credentials:x().min(1,_("ui.auth.credentialsRequired")),password:y(_),remember:g.boolean().optional().default(!1)}));var _;const A=(e=>p({username:x().min(3,e("ui.auth.usernameMinLength")),email:x().email(e("ui.auth.emailInvalid")),password:y(e),confirmPassword:x().min(6,e("ui.auth.passwordMinLength"))}).refine(e=>e.password===e.confirmPassword,{message:e("ui.auth.passwordMismatch"),path:["confirmPassword"]}))(I),U=w({resolver:f($)}),q=w({resolver:f(A)}),E=e=>{e!==O&&("register"!==e||P?R(e):h.info(I("ui.auth.registerClosed")))};return o.jsxs("div",{className:`auth-page-container ${C}`,children:[o.jsx("div",{className:"auth-background-layer",children:o.jsx(l,{})}),o.jsxs("div",{className:"auth-floating-actions",children:[o.jsx("button",{onClick:()=>{window.open("https://github.com/haierkeys/fast-note-sync-service","_blank","noopener,noreferrer")},className:"auth-floating-switcher",title:I("ui.common.sourceCode"),children:o.jsx(j,{size:18})}),o.jsx("button",{onClick:()=>{T("light"===M?"dark":"dark"===M?"auto":"light")},className:"auth-floating-switcher",title:I("auto"===M?"ui.settings.themeAuto":"dark"===C?"ui.settings.themeDark":"ui.settings.themeLight"),children:"auto"===M?o.jsx(n,{size:18}):"dark"===C?o.jsx(c,{size:18}):o.jsx(d,{size:18})}),o.jsx(m,{showText:!1,className:"auth-floating-switcher"})]}),o.jsxs("main",{className:"relative z-50 w-full px-6 py-12 flex flex-col items-center",children:[o.jsxs("div",{className:"auth-logo-wrapper",children:[o.jsx("div",{className:"auth-logo-box",children:o.jsx(b,{size:40,className:"auth-logo-icon"})}),o.jsx("h1",{className:"auth-title",children:"Fast Note Sync"}),o.jsx("p",{className:"auth-subtitle",children:I("ui.common.subtitle")})]}),o.jsxs("div",{className:"auth-card",children:[o.jsx("div",{className:"auth-tabs-container",children:o.jsxs("div",{className:"auth-tabs",children:[o.jsx("button",{onClick:()=>E("login"),className:"auth-tab "+("login"===O?"active":""),children:I("ui.auth.login")}),o.jsx("button",{onClick:()=>E("register"),className:"auth-tab "+("register"===O?"active":""),children:I("ui.auth.registerButton")})]})}),o.jsx(N,{mode:"wait",children:"login"===O?o.jsxs(v.form,{variants:S,initial:"hidden",animate:"visible",exit:"exit",onSubmit:U.handleSubmit(async e=>{const s=await F(e);s.success?k():h.error(s.error)}),children:[o.jsxs("div",{children:[o.jsxs("div",{className:"relative group",children:[o.jsx("label",{htmlFor:"login-credentials",className:"sr-only",children:I("ui.auth.credentials")}),o.jsx(u,{id:"login-credentials",placeholder:I("ui.auth.credentialsPlaceholder"),...U.register("credentials"),className:"auth-input"}),U.formState.errors.credentials&&o.jsx("p",{className:"text-[10px] text-destructive/80 font-bold uppercase tracking-wider mt-1 ml-1 mb-2",children:U.formState.errors.credentials.message})]}),o.jsxs("div",{className:"relative group",children:[o.jsx("label",{htmlFor:"login-password",className:"sr-only",children:I("ui.auth.password")}),o.jsx(u,{id:"login-password",type:"password",placeholder:I("ui.auth.passwordPlaceholder"),...U.register("password"),className:"auth-input"}),U.formState.errors.password&&o.jsx("p",{className:"text-[10px] text-destructive/80 font-bold uppercase tracking-wider mt-1 ml-1 mb-2",children:U.formState.errors.password.message})]})]}),o.jsx("button",{type:"submit",disabled:L,className:"auth-button-primary",children:L?o.jsx(v.div,{animate:{rotate:360},transition:{duration:1,repeat:1/0,ease:"linear"},className:"w-4 h-4 border-2 border-white/30 border-t-white rounded-full"}):I("ui.auth.login")})]},"login"):o.jsxs(v.form,{variants:S,initial:"hidden",animate:"visible",exit:"exit",onSubmit:q.handleSubmit(async e=>{const s=await z(e);s.success?k():h.error(s.error)}),children:[o.jsxs("div",{className:"space-y-0",children:[o.jsxs("div",{className:"relative group",children:[o.jsx("label",{htmlFor:"register-username",className:"sr-only",children:I("ui.auth.username")}),o.jsx(u,{id:"register-username",placeholder:I("ui.auth.usernamePlaceholder"),...q.register("username"),className:"auth-input"}),q.formState.errors.username&&o.jsx("p",{className:"text-[10px] text-destructive/80 font-bold uppercase tracking-wider mt-1 ml-1 mb-2",children:q.formState.errors.username.message})]}),o.jsxs("div",{className:"relative group",children:[o.jsx(u,{type:"email",placeholder:I("ui.auth.emailPlaceholder"),...q.register("email"),className:"auth-input"}),q.formState.errors.email&&o.jsx("p",{className:"text-[10px] text-destructive/80 font-bold uppercase tracking-wider mt-1 ml-1 mb-2",children:q.formState.errors.email.message})]}),o.jsxs("div",{className:"relative group",children:[o.jsx("label",{htmlFor:"register-password",className:"sr-only",children:I("ui.auth.password")}),o.jsx(u,{id:"register-password",type:"password",placeholder:I("ui.auth.passwordPlaceholder"),...q.register("password"),className:"auth-input"}),q.formState.errors.password&&o.jsx("p",{className:"text-[10px] text-destructive/80 font-bold uppercase tracking-wider mt-1 ml-1 mb-2",children:q.formState.errors.password.message})]}),o.jsxs("div",{className:"relative group",children:[o.jsx("label",{htmlFor:"register-confirm-password",className:"sr-only",children:I("ui.auth.confirmPassword")}),o.jsx(u,{id:"register-confirm-password",type:"password",placeholder:I("ui.auth.confirmPasswordPlaceholder"),...q.register("confirmPassword"),className:"auth-input"}),q.formState.errors.confirmPassword&&o.jsx("p",{className:"text-[10px] text-destructive/80 font-bold uppercase tracking-wider mt-1 ml-1 mb-2",children:q.formState.errors.confirmPassword.message})]})]}),o.jsx("button",{type:"submit",disabled:L,className:"auth-button-primary",children:L?o.jsx(v.div,{animate:{rotate:360},transition:{duration:1,repeat:1/0,ease:"linear"},className:"w-4 h-4 border-2 border-white/30 border-t-white rounded-full"}):I("ui.auth.registerButton")})]},"register")})]}),o.jsx("div",{className:"auth-footer-wrapper",children:o.jsx("footer",{className:"auth-brand-footer",dangerouslySetInnerHTML:{__html:I("ui.common.footerTitle")}})})]})]})}export{k as AuthForm}; ================================================ FILE: frontend/assets/badge-C63ATniC.js ================================================ import{j as r,f as e,l as t}from"./font-loader-CIrh3KnA.js";const n=t("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",secondary:"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",destructive:"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",outline:"text-foreground"}},defaultVariants:{variant:"default"}});function o({className:t,variant:o,...a}){return r.jsx("div",{className:e(n({variant:o}),t),...a})}export{o as B}; ================================================ FILE: frontend/assets/canvas-viewer-Bt8OKmt9.css ================================================ .canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.25rem;margin-bottom:.25rem}@media(min-width:640px){.canvas-toolbar{gap:1rem;margin-bottom:1rem}}.canvas-toolbar-left{display:flex;align-items:center;gap:.25rem;min-width:0;flex:1}@media(min-width:640px){.canvas-toolbar-left{gap:.75rem}}.canvas-toolbar-title{font-size:.875rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:hsl(var(--foreground))}@media(min-width:640px){.canvas-toolbar-title{font-size:1rem}}.canvas-toolbar-right{display:flex;align-items:center;gap:.125rem;flex-shrink:0}@media(min-width:640px){.canvas-toolbar-right{gap:.5rem}}.canvas-zoom-label{font-size:.75rem;color:hsl(var(--muted-foreground));min-width:3rem;text-align:center;-webkit-user-select:none;user-select:none}.canvas-container{position:relative;width:100%;height:100%;overflow:hidden;border-radius:.75rem;border:1px solid hsl(var(--border));background:hsl(var(--background));cursor:grab;touch-action:none}.canvas-container.is-dragging{cursor:grabbing}.canvas-transform-layer{position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform}.canvas-node{position:absolute;border-radius:.5rem;border:2px solid hsl(var(--border));background:hsl(var(--card));box-shadow:0 2px 6px hsl(var(--foreground) / .1);overflow:hidden;box-sizing:border-box;transition:box-shadow .15s ease}.canvas-node:hover{box-shadow:0 4px 12px hsl(var(--foreground) / .15)}.canvas-node-text{padding:.75rem;overflow:auto;font-size:.875rem;line-height:1.5}.canvas-node-text .markdown-preview{max-height:100%}.canvas-node-file{display:flex;align-items:center;gap:.5rem;padding:.75rem;cursor:pointer;font-size:.875rem;color:hsl(var(--primary))}.canvas-node-file:hover{background:hsl(var(--primary) / .05)}.canvas-node-file-icon{flex-shrink:0;width:1rem;height:1rem}.canvas-node-file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.canvas-node-link{display:flex;align-items:center;gap:.5rem;padding:.75rem;cursor:pointer;font-size:.875rem;color:hsl(var(--primary))}.canvas-node-link:hover{background:hsl(var(--primary) / .05)}.canvas-node-link-icon{flex-shrink:0;width:1rem;height:1rem}.canvas-node-link-url{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.canvas-group{position:absolute;border-radius:.75rem;border:3px dashed hsl(var(--border));background:hsl(var(--muted) / .3);box-sizing:border-box}.canvas-group-label{position:absolute;top:-1.5rem;left:.5rem;font-size:.75rem;font-weight:500;color:hsl(var(--muted-foreground));white-space:nowrap}.canvas-edge-layer{position:absolute;top:0;left:0;overflow:visible;pointer-events:none}.canvas-edge-path{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.canvas-edge-label{font-size:.75rem;fill:hsl(var(--muted-foreground));text-anchor:middle;dominant-baseline:central}.canvas-empty,.canvas-error{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:.5rem;color:hsl(var(--muted-foreground));font-size:.875rem}.canvas-error{color:hsl(var(--destructive))}.canvas-loading{display:flex;align-items:center;justify-content:center;height:100%;color:hsl(var(--muted-foreground))} ================================================ FILE: frontend/assets/canvas-viewer-Cxwbo1vR.js ================================================ import{c as e,r as t,b as n,e as s,a,t as r,a8 as o,j as i,B as l}from"./font-loader-CIrh3KnA.js";import{T as c}from"./tooltip-Dr-qRlmI.js";import{a as d}from"./markdown-editor-CX5kQlgI.js";import{F as h}from"./format-CdHm7RWL.js";import{E as u}from"./main-BIi-kGYY.js"; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const m=e("ArrowLeft",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]),f=e("ArrowUpNarrowWide",[["path",{d:"m3 8 4-4 4 4",key:"11wl7u"}],["path",{d:"M7 4v16",key:"1glfcx"}],["path",{d:"M11 12h4",key:"q8tih4"}],["path",{d:"M11 16h7",key:"uosisv"}],["path",{d:"M11 20h10",key:"jvxblo"}]]),p=e("Folder",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]),y=e("Maximize",[["path",{d:"M8 3H5a2 2 0 0 0-2 2v3",key:"1dcmit"}],["path",{d:"M21 8V5a2 2 0 0 0-2-2h-3",key:"1e4gt3"}],["path",{d:"M3 16v3a2 2 0 0 0 2 2h3",key:"wsl5sc"}],["path",{d:"M16 21h3a2 2 0 0 0 2-2v-3",key:"18trek"}]]),x=e("RotateCcw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]),g=e("ZoomIn",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14",key:"1vmskp"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]),k=e("ZoomOut",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */function w(){const e=t.useCallback(()=>{const e=localStorage.getItem("token")||"";return n({token:e})},[]),i=t.useCallback(()=>{localStorage.removeItem("token"),window.location.reload()},[]),l=t.useCallback(async(t,n,o,l=!1,c="",d="mtime",h="desc",u)=>{try{const m=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,f=Math.floor(n).toString(),p=Math.floor(o).toString();let y=`${m}/api/files?vault=${encodeURIComponent(t)}&page=${f}&pageSize=${p}`;l&&(y+="&isRecycle=1"),c&&(y+=`&keyword=${encodeURIComponent(c)}`),d&&"mtime"!==d&&(y+=`&sortBy=${d}`),h&&"desc"!==h&&(y+=`&sortOrder=${h}`);const x=await fetch(a(y),{method:"GET",headers:e()});if(!x.ok)return 508===x.status?i():r.error("Network response was not ok"),void u(null);const g=await x.json();if(g.code>0&&g.code<=200){u(g.data||{list:[],pager:{page:1,pageSize:o,totalRows:0,totalPages:0}})}else 508===g.code?i():r.error(g.message),u(null)}catch(m){r.error(m instanceof Error?m.message:String(m)),u(null)}},[e,i]),c=t.useCallback(async(t,n,o,i)=>{try{const l={vault:t,path:n,pathHash:o},c=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,d=await fetch(a(`${c}/api/file`),{method:"DELETE",body:JSON.stringify(l),headers:e()});if(!d.ok)throw new Error("Network response was not ok");const h=await d.json();h.code>0&&h.code<=200?(r.success(h.message),i()):r.error(h.message+(h.details?": "+h.details.join(", "):""))}catch(l){r.error(l instanceof Error?l.message:String(l))}},[e]),d=t.useCallback(async(t,n,o,i)=>{try{const l={vault:t,path:n,pathHash:o},c=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,d=await fetch(a(`${c}/api/file/recycle-clear`),{method:"DELETE",body:JSON.stringify(l),headers:e()});if(!d.ok)throw new Error("Network response was not ok");const h=await d.json();h.code>0&&h.code<=200?(r.success(h.message),i()):r.error(h.message+(h.details?": "+h.details.join(", "):""))}catch(l){r.error(l instanceof Error?l.message:String(l))}},[e]),h=t.useCallback(async(t,n)=>{try{const o={vault:t},i=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,l=await fetch(a(`${i}/api/file/recycle-clear`),{method:"DELETE",body:JSON.stringify(o),headers:e()});if(!l.ok)throw new Error("Network response was not ok");const c=await l.json();c.code>0&&c.code<=200?(r.success(c.message),n()):r.error(c.message)}catch(o){r.error(o instanceof Error?o.message:String(o))}},[e]),u=t.useCallback(async(t,n,o,i)=>{try{const l={vault:t,path:n,pathHash:o},c=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,d=await fetch(a(`${c}/api/file/restore`),{method:"PUT",body:JSON.stringify(l),headers:e()});if(!d.ok)throw new Error("Network response was not ok");const h=await d.json();h.code>0&&h.code<=200?(r.success(h.message),i()):r.error(h.message+(h.details?": "+h.details.join(", "):""))}catch(l){r.error(l instanceof Error?l.message:String(l))}},[e]),m=t.useCallback(async(t,n)=>{try{const o=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,i=await fetch(a(`${o}/api/file/rename`),{method:"POST",body:JSON.stringify(t),headers:e()});if(!i.ok)throw new Error("Network response was not ok");const l=await i.json();l.code>0&&l.code<=200?(r.success(l.message),n()):r.error(l.message)}catch(o){r.error(o instanceof Error?o.message:String(o))}},[e]),f=t.useCallback((e,t,n)=>{const a=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,r=localStorage.getItem("token")||"",i=o();let l=`${a}/api/file?vault=${encodeURIComponent(e)}&path=${encodeURIComponent(t)}&token=${encodeURIComponent(r)}&lang=${i}`;return n&&(l+=`&pathHash=${n}`),l},[]),p=t.useCallback(async(t,n="",o="",l)=>{try{let c=`${s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL}/api/folders?vault=${encodeURIComponent(t)}`;n&&(c+=`&path=${encodeURIComponent(n)}`),o&&(c+=`&path_hash=${encodeURIComponent(o)}`);const d=await fetch(a(c),{method:"GET",headers:e()});if(!d.ok)return 508===d.status?i():r.error("Network response was not ok"),void l(null);const h=await d.json();h.code>0&&h.code<=200?l(h.data||[]):(508===h.code?i():r.error(h.message),l(null))}catch(c){r.error(c instanceof Error?c.message:String(c)),l(null)}},[e,i]),y=t.useCallback(async(t,n="",o="",l,c,d="mtime",h="desc",u)=>{try{const m=s.API_URL.endsWith("/")?s.API_URL.slice(0,-1):s.API_URL,f=Math.floor(l).toString(),p=Math.floor(c).toString();let y=`${m}/api/folder/files?vault=${encodeURIComponent(t)}&page=${f}&pageSize=${p}`;n&&(y+=`&path=${encodeURIComponent(n)}`),o&&(y+=`&path_hash=${encodeURIComponent(o)}`),d&&(y+=`&sortBy=${d}`),h&&(y+=`&sortOrder=${h}`);const x=await fetch(a(y),{method:"GET",headers:e()});if(!x.ok)return 508===x.status?i():r.error("Network response was not ok"),void u(null);const g=await x.json();if(g.code>0&&g.code<=200){const e=g.data||{list:[],pager:{page:l,pageSize:c,totalRows:0,totalPages:0}};e.list||(e.list=[]),u(e)}else 508===g.code?i():r.error(g.message),u(null)}catch(m){r.error(m instanceof Error?m.message:String(m)),u(null)}},[e,i]);return t.useMemo(()=>({handleFileList:l,handleDeleteFile:c,handlePermanentDeleteFile:d,handleClearFileRecycle:h,handleRestoreFile:u,handleRenameFile:m,getRawFileUrl:f,handleFolderList:p,handleFolderFiles:y}),[l,c,d,h,u,m,f,p,y])}const v={1:"#fb464c",2:"#e9973f",3:"#e0de71",4:"#44cf6e",5:"#53dfdd",6:"#a882ff"};function j(e){if(e)return v[e]??e}function R(e,t){const n=e.replace("#","");if(/^[0-9a-fA-F]{6}$/.test(n)){return`rgba(${parseInt(n.slice(0,2),16)}, ${parseInt(n.slice(2,4),16)}, ${parseInt(n.slice(4,6),16)}, ${t})`}return e}function b(e,t){const n=e.x+e.width/2,s=e.y+e.height/2;switch(t){case"top":return{x:n,y:e.y};case"bottom":return{x:n,y:e.y+e.height};case"left":return{x:e.x,y:s};case"right":return{x:e.x+e.width,y:s};default:return{x:n,y:s}}}function C(e,t){switch(e){case"top":return{dx:0,dy:-t};case"bottom":return{dx:0,dy:t};case"left":return{dx:-t,dy:0};case"right":return{dx:t,dy:0};default:return{dx:0,dy:0}}}function N({edge:e,nodeMap:t}){const n=t.get(e.fromNode),s=t.get(e.toNode);if(!n||!s)return null;const a=b(n,e.fromSide),r=b(s,e.toSide),o=Math.abs(r.x-a.x),l=Math.abs(r.y-a.y),c=Math.min(o/2,l/2,100),d=Math.max(c,30),h=C(e.fromSide,d),u=C(e.toSide,d),m=`M ${a.x} ${a.y} C ${a.x+h.dx} ${a.y+h.dy}, ${r.x+u.dx} ${r.y+u.dy}, ${r.x} ${r.y}`,f=j(e.color),p=f??"default",y="none"!==e.toEnd,x="arrow"===e.fromEnd;return i.jsxs("g",{children:[i.jsx("path",{d:m,className:"canvas-edge-path",style:f?{stroke:f}:void 0,markerEnd:y?`url(#marker-to-${p})`:void 0,markerStart:x?`url(#marker-from-${p})`:void 0}),e.label&&i.jsx("text",{x:(a.x+r.x)/2,y:(a.y+r.y)/2,className:"canvas-edge-label",dy:"-6",children:e.label})]})}function $({node:e,onNodeClick:t,onWikiLinkClick:n,isDragRef:s}){var a;const r=j(e.color),o=r?{borderColor:r,backgroundColor:R(r,.09)}:void 0;if("group"===e.type)return i.jsx("div",{className:"canvas-group",style:{left:e.x,top:e.y,width:e.width,height:e.height,...r?{borderColor:r,background:R(r,.08)}:{}},children:e.label&&i.jsx("span",{className:"canvas-group-label",children:e.label})});return i.jsxs("div",{className:"canvas-node",style:{left:e.x,top:e.y,width:e.width,height:e.height,...o},onClick:"text"!==e.type?()=>{s.current||("file"===e.type&&e.file&&n?n(e.file):"link"===e.type&&e.url&&window.open(e.url,"_blank","noopener,noreferrer"),null==t||t(e))}:void 0,children:["text"===e.type&&i.jsx("div",{className:"canvas-node-text markdown-preview",children:i.jsx(d,{content:e.text??""})}),"file"===e.type&&i.jsxs("div",{className:"canvas-node-file",children:[i.jsx(h,{className:"canvas-node-file-icon"}),i.jsx("span",{className:"canvas-node-file-name",children:(null==(a=e.file)?void 0:a.split("/").pop())??e.file})]}),"link"===e.type&&i.jsxs("div",{className:"canvas-node-link",children:[i.jsx(u,{className:"canvas-node-link-icon"}),i.jsx("span",{className:"canvas-node-link-url",children:e.url})]})]})}function I({nodes:e,edges:n,viewport:s,onViewportChange:a,onNodeClick:r,onWikiLinkClick:o}){const l=t.useRef(null),[c,d]=t.useState(!1),h=t.useRef(null),u=t.useRef(!1),m=t.useRef([]),f=t.useRef(null),p=t.useMemo(()=>{const t=new Map;for(const n of e)t.set(n.id,n);return t},[e]),y=t.useMemo(()=>e.filter(e=>"group"===e.type),[e]),x=t.useMemo(()=>e.filter(e=>"group"!==e.type),[e]),g=t.useMemo(()=>{if(0===e.length)return{minX:0,minY:0,w:100,h:100};let t=1/0,n=1/0,s=-1/0,a=-1/0;for(const o of e)t=Math.min(t,o.x),n=Math.min(n,o.y),s=Math.max(s,o.x+o.width),a=Math.max(a,o.y+o.height);const r=500;return{minX:t-r,minY:n-r,w:s-t+1e3,h:a-n+1e3}},[e]),k=t.useMemo(()=>{const e=new Map;e.set("default","currentColor");for(const t of n){const n=j(t.color);n&&e.set(n,n)}return e},[n]),w=e=>Math.min(3,Math.max(.1,e)),v=t.useCallback(e=>{var t;if(e.preventDefault(),e.ctrlKey||e.metaKey){const n=null==(t=l.current)?void 0:t.getBoundingClientRect();if(!n)return;const r=e.clientX-n.left,o=e.clientY-n.top,i=e.deltaY>0?.9:1.1,c=w(s.zoom*i),d=c/s.zoom;a({x:r-(r-s.x)*d,y:o-(o-s.y)*d,zoom:c})}else a({...s,x:s.x-e.deltaX,y:s.y-e.deltaY})},[s,a]),R=t.useCallback(e=>{var t,n;if(0!==e.button)return;const a=m.current;a.push(e.nativeEvent),1===a.length&&(d(!0),u.current=!1,h.current={x:e.clientX,y:e.clientY,vx:s.x,vy:s.y},null==(n=(t=e.target).setPointerCapture)||n.call(t,e.pointerId))},[s.x,s.y]),b=t.useCallback(e=>{var t;const n=m.current,r=n.findIndex(t=>t.pointerId===e.pointerId);if(r>=0&&(n[r]=e.nativeEvent),2===n.length){const e=Math.hypot(n[0].clientX-n[1].clientX,n[0].clientY-n[1].clientY);if(null!==f.current){const r=e/f.current,o=null==(t=l.current)?void 0:t.getBoundingClientRect();if(o){const e=(n[0].clientX+n[1].clientX)/2-o.left,t=(n[0].clientY+n[1].clientY)/2-o.top,i=w(s.zoom*r),l=i/s.zoom;a({x:e-(e-s.x)*l,y:t-(t-s.y)*l,zoom:i})}}f.current=e}else if(1===n.length&&h.current){const t=e.clientX-h.current.x,n=e.clientY-h.current.y;(Math.abs(t)>3||Math.abs(n)>3)&&(u.current=!0),a({...s,x:h.current.vx+t,y:h.current.vy+n})}},[s,a]),C=t.useCallback(e=>{const t=m.current,n=t.findIndex(t=>t.pointerId===e.pointerId);n>=0&&t.splice(n,1),t.length<2&&(f.current=null),0===t.length&&(d(!1),h.current=null)},[]);return t.useEffect(()=>{const e=l.current;if(!e)return;const t=e=>e.preventDefault();return e.addEventListener("wheel",t,{passive:!1}),()=>e.removeEventListener("wheel",t)},[]),i.jsx("div",{ref:l,className:"canvas-container"+(c?" is-dragging":""),onWheel:v,onPointerDown:R,onPointerMove:b,onPointerUp:C,onPointerCancel:C,children:i.jsxs("div",{className:"canvas-transform-layer",style:{transform:`translate(${s.x}px, ${s.y}px) scale(${s.zoom})`},children:[y.map(e=>i.jsx($,{node:e,onNodeClick:r,onWikiLinkClick:o,isDragRef:u},e.id)),i.jsxs("svg",{className:"canvas-edge-layer",style:{left:g.minX,top:g.minY,width:g.w,height:g.h,color:"hsl(var(--foreground))"},viewBox:`${g.minX} ${g.minY} ${g.w} ${g.h}`,children:[i.jsx("defs",{children:Array.from(k.entries()).map(([e,t])=>i.jsxs("g",{children:[i.jsx("marker",{id:`marker-to-${e}`,markerWidth:"12",markerHeight:"10",refX:"11",refY:"5",orient:"auto",markerUnits:"userSpaceOnUse",children:i.jsx("polygon",{points:"0 0, 12 5, 0 10",fill:t})}),i.jsx("marker",{id:`marker-from-${e}`,markerWidth:"12",markerHeight:"10",refX:"1",refY:"5",orient:"auto",markerUnits:"userSpaceOnUse",children:i.jsx("polygon",{points:"12 0, 0 5, 12 10",fill:t})})]},e))}),n.map(e=>i.jsx(N,{edge:e,nodeMap:p},e.id))]}),x.map(e=>i.jsx($,{node:e,onNodeClick:r,onWikiLinkClick:o,isDragRef:u},e.id))]})})}function M(e,t,n){if(0===e.length||0===t||0===n)return{x:0,y:0,zoom:1};let s=1/0,a=1/0,r=-1/0,o=-1/0;for(const h of e)s=Math.min(s,h.x),a=Math.min(a,h.y),r=Math.max(r,h.x+h.width),o=Math.max(o,h.y+h.height);const i=r-s,l=o-a,c=Math.min((t-80)/i,(n-80)/l,1.5),d=Math.max(.1,c);return{x:t/2-(s+r)/2*d,y:n/2-(a+o)/2*d,zoom:d}}function L({vault:e,note:s,onBack:a,onWikiLinkClick:r,isRecycle:o}){var d;const{getRawFileUrl:h}=w(),[u,f]=t.useState(null),[p,x]=t.useState(null),[v,j]=t.useState(!1),[R,b]=t.useState({x:0,y:0,zoom:1}),[C,N]=t.useState({w:0,h:0}),$=t.useRef(null),L=t.useRef(null);t.useEffect(()=>{const e=$.current;if(!e)return;const t=()=>{const t=e.getBoundingClientRect(),n=window.innerHeight-t.top-16;e.style.height=`${Math.max(200,n)}px`};return t(),window.addEventListener("resize",t),()=>window.removeEventListener("resize",t)},[]),t.useEffect(()=>{const e=L.current;if(!e)return;const t=()=>{const{clientWidth:t,clientHeight:n}=e;t>0&&n>0&&N(e=>e.w===t&&e.h===n?e:{w:t,h:n})};requestAnimationFrame(t);const n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]),t.useEffect(()=>{if(!s)return;let t=!1;j(!0),x(null);let a=h(e,s.path,s.pathHash);return o&&(a+=(a.includes("?")?"&":"?")+"isRecycle=1"),fetch(a,{cache:"no-store",headers:n({token:localStorage.getItem("token"),includeContentType:!1,includeDomain:!1,includeLang:!1})}).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.text()}).then(e=>{if(!t)try{const t=function(e){const t=JSON.parse(e);return{nodes:Array.isArray(t.nodes)?t.nodes.map(e=>({...e,x:Number(e.x)||0,y:Number(e.y)||0,width:Number(e.width)||100,height:Number(e.height)||50})):[],edges:Array.isArray(t.edges)?t.edges:[]}}(e);f(t)}catch{x("Canvas JSON parse error"),f(null)}}).catch(e=>{t||(x(e.message??"Failed to load canvas"),f(null))}).finally(()=>{t||j(!1)}),()=>{t=!0}},[e,s,h,o]),t.useEffect(()=>{u&&C.w>0&&C.h>0&&b(M(u.nodes,C.w,C.h))},[u,C]);const U=t.useCallback(()=>{u&&C.w>0&&C.h>0&&b(M(u.nodes,C.w,C.h))},[u,C]),E=t.useCallback(()=>{b(e=>{const t=Math.min(3,1.2*e.zoom),n=t/e.zoom,s=C.w/2,a=C.h/2;return{x:s-(s-e.x)*n,y:a-(a-e.y)*n,zoom:t}})},[C]),S=t.useCallback(()=>{b(e=>{const t=Math.max(.1,.8*e.zoom),n=t/e.zoom,s=C.w/2,a=C.h/2;return{x:s-(s-e.x)*n,y:a-(a-e.y)*n,zoom:t}})},[C]),A=(null==(d=null==s?void 0:s.path)?void 0:d.split("/").pop())??"canvas";return i.jsxs("div",{ref:$,className:"w-full flex flex-col",children:[i.jsxs("div",{className:"canvas-toolbar",children:[i.jsxs("div",{className:"canvas-toolbar-left",children:[i.jsx(l,{variant:"ghost",size:"icon",onClick:a,className:"shrink-0 rounded-lg sm:rounded-xl h-7 w-7 sm:h-10 sm:w-10",children:i.jsx(m,{className:"h-4 w-4 sm:h-5 sm:w-5"})}),i.jsx("span",{className:"canvas-toolbar-title",children:A})]}),i.jsxs("div",{className:"canvas-toolbar-right",children:[i.jsxs("span",{className:"canvas-zoom-label",children:[Math.round(100*R.zoom),"%"]}),i.jsx(c,{content:"Zoom Out",side:"bottom",delay:200,children:i.jsx(l,{variant:"outline",size:"icon",onClick:S,className:"rounded-lg sm:rounded-xl h-7 w-7 sm:h-10 sm:w-10",children:i.jsx(k,{className:"h-3.5 w-3.5 sm:h-4 sm:w-4"})})}),i.jsx(c,{content:"Zoom In",side:"bottom",delay:200,children:i.jsx(l,{variant:"outline",size:"icon",onClick:E,className:"rounded-lg sm:rounded-xl h-7 w-7 sm:h-10 sm:w-10",children:i.jsx(g,{className:"h-3.5 w-3.5 sm:h-4 sm:w-4"})})}),i.jsx(c,{content:"Fit to View",side:"bottom",delay:200,children:i.jsx(l,{variant:"outline",size:"icon",onClick:U,className:"rounded-lg sm:rounded-xl h-7 w-7 sm:h-10 sm:w-10",children:i.jsx(y,{className:"h-3.5 w-3.5 sm:h-4 sm:w-4"})})})]})]}),i.jsxs("div",{ref:L,className:"flex-1 min-h-0",children:[v&&i.jsx("div",{className:"canvas-loading",children:i.jsxs("div",{className:"flex flex-col items-center gap-2",children:[i.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-primary"}),i.jsx("span",{children:"Loading..."})]})}),p&&i.jsx("div",{className:"canvas-error",children:i.jsx("span",{children:p})}),!v&&!p&&u&&0===u.nodes.length&&i.jsx("div",{className:"canvas-empty",children:i.jsx("span",{children:"Empty canvas"})}),!v&&!p&&u&&u.nodes.length>0&&i.jsx(I,{nodes:u.nodes,edges:u.edges,vault:e,viewport:R,onViewportChange:b,onWikiLinkClick:r})]})]})}export{m as A,L as C,p as F,x as R,f as a,w as u}; ================================================ FILE: frontend/assets/checkbox-DhTHgmeh.js ================================================ import{r as e,j as r,ah as t,ao as o,aj as n,ag as a,aB as s,an as c,aC as d,f as i,a5 as l}from"./font-loader-CIrh3KnA.js";import{u}from"./select-CJF_alSt.js";var f="Checkbox",[p]=t(f),[h,b]=p(f);function m(t){const{__scopeCheckbox:o,checked:n,children:s,defaultChecked:c,disabled:d,form:i,name:l,onCheckedChange:u,required:p,value:b="on",internal_do_not_use_render:m}=t,[k,C]=a({prop:n,defaultProp:c??!1,onChange:u,caller:f}),[x,v]=e.useState(null),[y,_]=e.useState(null),g=e.useRef(!1),R=!x||(!!i||!!x.closest("form")),E={checked:k,disabled:d,setChecked:C,control:x,setControl:v,name:l,form:i,value:b,hasConsumerStoppedPropagationRef:g,required:p,defaultChecked:!w(c)&&c,isFormControl:R,bubbleInput:y,setBubbleInput:_};return r.jsx(h,{scope:o,...E,children:j(m)?m(E):s})}var k="CheckboxTrigger",C=e.forwardRef(({__scopeCheckbox:t,onKeyDown:o,onClick:a,...d},i)=>{const{control:l,value:u,disabled:f,checked:p,required:h,setControl:m,setChecked:C,hasConsumerStoppedPropagationRef:x,isFormControl:v,bubbleInput:y}=b(k,t),_=s(i,m),g=e.useRef(p);return e.useEffect(()=>{const e=null==l?void 0:l.form;if(e){const r=()=>C(g.current);return e.addEventListener("reset",r),()=>e.removeEventListener("reset",r)}},[l,C]),r.jsx(n.button,{type:"button",role:"checkbox","aria-checked":w(p)?"mixed":p,"aria-required":h,"data-state":R(p),"data-disabled":f?"":void 0,disabled:f,value:u,...d,ref:_,onKeyDown:c(o,e=>{"Enter"===e.key&&e.preventDefault()}),onClick:c(a,e=>{C(e=>!!w(e)||!e),y&&v&&(x.current=e.isPropagationStopped(),x.current||e.stopPropagation())})})});C.displayName=k;var x=e.forwardRef((e,t)=>{const{__scopeCheckbox:o,name:n,checked:a,defaultChecked:s,required:c,disabled:d,value:i,onCheckedChange:l,form:u,...f}=e;return r.jsx(m,{__scopeCheckbox:o,checked:a,defaultChecked:s,disabled:d,required:c,onCheckedChange:l,name:n,form:u,value:i,internal_do_not_use_render:({isFormControl:e})=>r.jsxs(r.Fragment,{children:[r.jsx(C,{...f,ref:t,__scopeCheckbox:o}),e&&r.jsx(g,{__scopeCheckbox:o})]})})});x.displayName=f;var v="CheckboxIndicator",y=e.forwardRef((e,t)=>{const{__scopeCheckbox:a,forceMount:s,...c}=e,d=b(v,a);return r.jsx(o,{present:s||w(d.checked)||!0===d.checked,children:r.jsx(n.span,{"data-state":R(d.checked),"data-disabled":d.disabled?"":void 0,...c,ref:t,style:{pointerEvents:"none",...e.style}})})});y.displayName=v;var _="CheckboxBubbleInput",g=e.forwardRef(({__scopeCheckbox:t,...o},a)=>{const{control:c,hasConsumerStoppedPropagationRef:i,checked:l,defaultChecked:f,required:p,disabled:h,name:m,value:k,form:C,bubbleInput:x,setBubbleInput:v}=b(_,t),y=s(a,v),g=u(l),j=d(c);e.useEffect(()=>{const e=x;if(!e)return;const r=window.HTMLInputElement.prototype,t=Object.getOwnPropertyDescriptor(r,"checked").set,o=!i.current;if(g!==l&&t){const r=new Event("click",{bubbles:o});e.indeterminate=w(l),t.call(e,!w(l)&&l),e.dispatchEvent(r)}},[x,g,l,i]);const R=e.useRef(!w(l)&&l);return r.jsx(n.input,{type:"checkbox","aria-hidden":!0,defaultChecked:f??R.current,required:p,disabled:h,name:m,value:k,form:C,...o,tabIndex:-1,ref:y,style:{...o.style,...j,position:"absolute",pointerEvents:"none",opacity:0,margin:0,transform:"translateX(-100%)"}})});function j(e){return"function"==typeof e}function w(e){return"indeterminate"===e}function R(e){return w(e)?"indeterminate":e?"checked":"unchecked"}g.displayName=_;const E=e.forwardRef(({className:e,...t},o)=>r.jsx(x,{ref:o,className:i("peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",e),...t,children:r.jsx(y,{className:i("flex items-center justify-center text-current"),children:r.jsx(l,{className:"h-4 w-4"})})}));E.displayName=x.displayName;export{E as C}; ================================================ FILE: frontend/assets/circle-alert-EFzISefA.js ================================================ import{c as e}from"./font-loader-CIrh3KnA.js"; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const y=e("CircleAlert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);export{y as C}; ================================================ FILE: frontend/assets/circle-alert-EFzISefA.js.br ================================================ @lN_. pL}"KӁtQzFY6xxmObsidian Fast Note Sync Plugin","ui.common.loading":"Loading...","ui.common.downloading":"Downloading...","ui.common.actions":"Actions","ui.common.save":"Save","ui.common.add":"Add","ui.common.edit":"Edit","ui.common.view":"View","ui.common.viewDetail":"View Details","ui.common.delete":"Delete","ui.common.restore":"Restore","ui.common.permanentDelete":"Permanent Delete","ui.common.clear":"Clear","ui.common.batchPermanentDelete":"Batch Permanent Delete","ui.common.batchPermanentDeleteConfirm":"Are you sure you want to permanently delete the selected {{count}} items? This action cannot be undone!","ui.common.search":"Search","ui.common.refresh":"Refresh","ui.common.retry":"Retry","ui.common.refreshSuccess":"Refresh successful","ui.common.close":"Close","ui.common.cancel":"Cancel","ui.common.confirm":"Confirm","ui.common.success":"Success","ui.common.error":"Error","ui.common.warning":"Warning","ui.common.info":"Info","ui.common.total":"Total","ui.common.isEnabled":"Enabled","ui.common.isDisabled":"Disabled","ui.common.switchLanguage":"Switch Language","ui.common.comingSoon":"Coming Soon...","ui.common.comingSoonDescription":"This feature is under development. Stay tuned...","ui.common.helpAndSupport":"Help & Support","ui.common.githubIssue":"Feedback","ui.common.githubIssueDesc":"Submit Bug or Feature Request","ui.common.telegramGroup":"Community","ui.common.telegramGroupDesc":"Telegram community discussion and help","ui.common.planned":"Planned","ui.common.unknown":"Unknown","ui.common.copied":"Copied","ui.common.restoring":"Restoring...","ui.common.rename":"Rename","ui.common.toggleTheme":"Toggle Theme","ui.common.previous":"Previous","ui.common.next":"Next","ui.common.page":"Page","ui.common.of":"of","ui.common.to":"to","ui.common.perPage":"per page","ui.common.copy":"Copy","ui.common.createdAt":"Created At","ui.common.updatedAt":"Updated At","ui.common.noSearchResults":"No matching results found","ui.common.selectAll":"Select All","ui.common.items":"items","ui.common.count":"count","ui.common.saveSuccess":"Saved successfully","ui.common.selectVault":"Select Vault","ui.common.sourceCode":"Source Code","ui.common.wideMode":"Wide Mode","ui.common.narrowMode":"Normal Width","ui.common.fold":"Collapse","ui.common.noChange":"No changes made","ui.common.na":"N/A","ui.common.name":"Name","ui.auth.login":"Login","ui.auth.logout":"Logout","ui.auth.register":"Register","ui.auth.registerButton":"Register","ui.auth.registerClosed":"User registration is closed","ui.auth.credentials":"Username (or Email)","ui.auth.username":"Username","ui.auth.password":"Password","ui.auth.remember":"Remember me","ui.auth.confirmPassword":"Confirm Password","ui.auth.email":"Email","ui.auth.credentialsPlaceholder":"Enter username (or email)","ui.auth.usernamePlaceholder":"Enter username","ui.auth.passwordPlaceholder":"Enter password","ui.auth.emailPlaceholder":"Enter email","ui.auth.confirmPasswordPlaceholder":"Confirm password","ui.auth.credentialsRequired":"Username (or email) cannot be empty","ui.auth.passwordMinLength":"Password must be at least 6 characters","ui.auth.usernameMinLength":"Username must be at least 3 characters","ui.auth.emailInvalid":"Please enter a valid email address","ui.auth.passwordMismatch":"Passwords do not match","ui.auth.changePassword":"Change Password","ui.auth.currentPassword":"Current Password","ui.auth.newPassword":"New Password","ui.auth.confirmNewPassword":"Confirm New Password","ui.auth.passwordChangedSuccess":"Password changed successfully","ui.auth.passwordChangeFailed":"Failed to change password","ui.auth.sessionExpired":"User session expired, please log in again","ui.auth.submitting":"Submitting...","ui.auth.unknownUser":"Unknown User","ui.auth.userUid":"User UID: {{uid}}","ui.auth.loginRequestFailed":"Login request failed, please check network status","ui.auth.registerRequestFailed":"Registration failed, please try again","ui.user.password":"Access Password","ui.nav.navigation":"Navigation","ui.nav.menuDashboard":"Dashboard","ui.nav.menuVaults":"Vaults","ui.nav.menuNotes":"Note Management","ui.nav.menuTrash":"Trash","ui.nav.menuSync":"Backup & Sync","ui.nav.menuSettings":"System Settings","ui.nav.menuGit":"Git Automation","ui.nav.menuFiles":"Attachment Management","ui.nav.menuShares":"Share Management","ui.nav.menuSettingsBrowser":"Vault Configuration Files","ui.nav.menuSyncLogs":"Vault Update Logs","ui.nav.mainNavigation":"Navigation","ui.vault.vault":"Vault","ui.vault.title":"Vault Management","ui.vault.add":"Add Vault","ui.vault.edit":"Edit Vault","ui.vault.delete":"Delete Vault","ui.vault.name":"Vault Name","ui.vault.nameRequired":"Vault name cannot be empty","ui.vault.confirmDelete":"Are you sure you want to delete this vault? This action cannot be undone!","ui.vault.noVaults":"No vaults yet","ui.vault.count":"Total {{count}} vaults","ui.vault.searchPlaceholder":"Search vaults...","ui.vault.note":"Note","ui.vault.attachmentCount":"Attachments","ui.vault.totalSize":"Total Size: {{size}}","ui.vault.authTokenConfig":"Authorize Client","ui.vault.copyConfig":"Copy Connection Config","ui.vault.copyConfigSuccess":"Configuration copied to clipboard","ui.vault.copyConfigError":"Copy failed, please select and copy manually","ui.vault.oneClickImport":"One-click Authorize Obsidian","ui.vault.pleaseCreateVault":"Please create a vault or authorize configuration to Obsidian to create automatically","ui.vault.createVaultFirst":"Please create a vault first, then manage it","ui.vault.goToVaultManagement":"Go to Vault Management","ui.vault.setAsDefault":"Set as Default","ui.note.note":"Note","ui.note.notes":"Note List","ui.note.newNote":"New Note","ui.note.noNotes":"No notes yet","ui.note.viewNote":"View Note","ui.note.editNote":"Edit Note","ui.note.search":"Search","ui.note.searchPlaceholder":"Search notes...","ui.note.noteTitlePlaceholder":"Note title (e.g., note.md)","ui.note.noteContentPlaceholder":"Enter note content...","ui.note.noteCount":"Note count","ui.note.noteTitleRequired":"Title cannot be empty","ui.note.results":"notes","ui.note.saving":"Saving...","ui.note.lastSavedAt":"Last saved","ui.note.renameNote":"Rename note","ui.note.renameNotePlaceholder":"Enter new note name (e.g., new-note.md)","ui.note.renameSuccess":"Note renamed successfully","ui.note.deleteNoteConfirm":'Are you sure you want to delete the note "{{title}}"?',"ui.note.permanentDeleteConfirm":'Are you sure you want to permanently delete the note "{{title}}"? This action cannot be undone!',"ui.note.restoreNoteConfirm":'Are you sure you want to restore the note "{{title}}"?',"ui.note.clearRecycleConfirm":"Are you sure you want to empty the note recycle bin? This action cannot be undone!","ui.note.noVaultsForNotes":"No note vaults yet","ui.note.searchPath":"Path","ui.note.searchContentMode":"Content","ui.note.sortByBy":"Sort by","ui.note.sortByMtime":"Modification time","ui.note.sortByCtime":"Creation time","ui.note.sortByPath":"Path","ui.note.sortAsc":"Ascending","ui.note.sortDesc":"Descending","ui.note.viewFlat":"Flat view","ui.note.viewFolder":"Folder view","ui.note.editorHr":"Horizontal rule","ui.note.exportPdfPlanned":"PDF export feature in development...","ui.note.undo":"Undo","ui.note.redo":"Redo","ui.note.cut":"Cut","ui.note.copy":"Copy","ui.note.paste":"Paste","ui.note.selectAll":"Select all","ui.note.fullscreen":"Fullscreen","ui.note.exitFullscreen":"Exit fullscreen","ui.note.contextMenu":"Context menu","ui.note.loadingEditor":"Loading editor...","ui.note.unsavedContentWithoutTitle":"Content not saved because title is empty.","ui.note.wikiLinkNotFound":'Note "{{target}}" not found',"ui.history.title":"Note history","ui.history.description":"View and restore historical versions of notes","ui.history.version":"Version","ui.history.versionLabel":"Version","ui.history.time":"Update time","ui.history.action":"Action","ui.history.view":"View","ui.history.count":"{{count}} history records in total","ui.history.loading":"Loading...","ui.history.noHistory":"No history records yet","ui.history.clientSource":"Client","ui.history.diffDetails":"Version v{{version}} diff details","ui.history.showDiffOnly":"Show diff only","ui.history.showOriginalContent":"Content before modification","ui.history.diffLegendAdd":"Added","ui.history.diffLegendDel":"Deleted","ui.history.restore":"Restore","ui.history.restoreToVersion":"Restore to this version","ui.history.restoreVersionConfirmTitle":"Confirm restore","ui.history.restoreVersionConfirmDesc":"Are you sure you want to restore the note to version v{{version}}? Current content will be overwritten but will be automatically saved as a new history version.","ui.history.restoring":"Restoring...","ui.file.file":"Attachment","ui.file.files":"Attachment list","ui.file.noFiles":"No attachments yet","ui.file.searchFilePlaceholder":"Search attachments...","ui.file.deleteFileConfirm":'Are you sure you want to delete the attachment "{{title}}"?',"ui.file.restoreFileConfirm":'Are you sure you want to restore the attachment "{{title}}"?',"ui.file.permanentDeleteConfirm":'Are you sure you want to permanently delete the attachment "{{title}}"? This action cannot be undone!',"ui.file.clearRecycleConfirm":"Are you sure you want to empty the attachment recycle bin? This action cannot be undone!","ui.file.attachmentCount":"Attachments","ui.file.results":"attachment items","ui.file.searchPlaceholder":"Search attachments...","ui.file.totalSize":"Total {{size}}","ui.file.renameFile":"Rename Attachment","ui.file.renameFilePlaceholder":"Enter new attachment name","ui.file.renameSuccess":"Attachment renamed successfully","ui.file.size":"Space Used","ui.file.fileDetail":"Attachment Details","ui.file.imagePreview":"Image Preview","ui.file.audioPreview":"Audio Playback","ui.file.videoPreview":"Video Playback","ui.file.pdfPreview":"PDF Document","ui.file.codePreview":"Script Code","ui.file.unsupportedPreview":"This file type does not support direct preview yet","ui.file.openInNewWindow":"Open in New Window","ui.file.browserDownload":"Browser Download","ui.file.batchRestore":"Batch Restore","ui.file.batchPermanentDelete":"Batch Permanent Delete","ui.file.selectedCount":"{{count}} items selected","ui.file.batchRestoreConfirm":"Are you sure you want to restore the selected {{count}} items?","ui.file.batchPermanentDeleteConfirm":"Are you sure you want to permanently delete the selected {{count}} items? This action cannot be undone!","ui.file.noVaultsForFiles":"No vaults yet","ui.settings.systemConfig":"Short Link Sharing","ui.settings.securityConfig":"Security Token","ui.settings.noteRelatedConfig":"Note Attachments","ui.settings.fontConfig":"General","ui.settings.saveSettings":"Save Settings","ui.settings.saveSuccess":"Settings saved successfully","ui.settings.saveFailed":"Failed to save settings","ui.settings.fontSet":"WebGui Font Settings","ui.settings.authTokenKey":"User Service Token Encryption Obfuscation String","ui.settings.tokenExpiry":"User Service Token Expiry Time","ui.settings.shareTokenKey":"Share Token Encryption Obfuscation String","ui.settings.shareTokenExpiry":"Share Token Expiry Time","ui.settings.registerIsEnable":"Open Registration","ui.settings.fileChunkSize":"Attachment Upload Chunk Size","ui.settings.softDeleteRetentionTime":"Recycle Bin (Soft Delete) Retention Time","ui.settings.uploadSessionTimeout":"Attachment Upload Session Timeout","ui.settings.historyKeepVersions":"Number of Note History Versions to Keep","ui.settings.historySaveDelay":"Note History Save Delay","ui.settings.historySaveDelayFormatError":"Invalid history save delay format","ui.settings.adminUid":"System Settings Access Restriction (User UID)","ui.settings.adminUidDesc":"Specify the UID with admin privileges. 0 means all logged-in users can manage. Default: 0","ui.settings.onlyAdminAccess":"Only administrators can access this page","ui.settings.fontSetDesc":"Set the WebGui interface font. Leave empty to use the system default font.\nSupports remote CSS font stylesheets or presets CSS stylesheet keywords or font network addresses and local fonts (placed in the storage/user_static/ directory). Default: empty.\n\nExamples: \n  - Remote CSS font stylesheet: https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap\n  - Preset CSS stylesheet keyword: local or remote\n  - Local preset CSS font stylesheet: /static/fonts/font.css\n  - Font network address: https://ik.imagekit.io/name/your-font.woff2\n  - Local font: /user_static/your-font.woff2, requires placing the font file your-font.woff2 in the storage/user_static/ directory","ui.settings.authTokenKeyDesc":"This setting participates in the generation of all user service tokens. For server security after the first deployment, it is strongly recommended to modify this item. Once modified, existing tokens will immediately become invalid, and WebGui will require re-login.","ui.settings.tokenExpiryDesc":"Expiry time for user service tokens. Supported formats: 7d (days), 24h (hours), 30m (minutes). Default: 365d","ui.settings.shareTokenKeyDesc":"Encryption obfuscation Key used to generate share tokens. Modifying this will invalidate existing share links. Default: fns","ui.settings.shareTokenExpiryDesc":"Expiry time for share tokens. Supported formats: 7d (days), 24h (hours), 30m (minutes). Default: 30d","ui.settings.registerIsEnableDesc":"Whether to allow new users to create accounts on this service. Default: Enabled","ui.settings.fileChunkSizeDesc":"Chunk size during upload (e.g., 1MB, 512KB). Recommended 512KB-2MB. Default: 512KB","ui.settings.softDeleteRetentionTimeDesc":"Retention duration after deleting notes and attachments (e.g., 30d, 24h). 0 means no automatic cleanup. Setting this too short may prevent offline devices from syncing deletion operations. Please set carefully. Default: 7d","ui.settings.uploadSessionTimeoutDesc":"Validity period for file chunk upload sessions (e.g., 1h, 30m). Default: 1d","ui.settings.historyKeepVersionsDesc":"Number of historical versions to keep for each note. When exceeded, the oldest versions are automatically deleted. Minimum: 100. Default: 100","ui.settings.historySaveDelayDesc":"Delay time for saving history, used to prevent excessive history versions from frequent edits (e.g., 10s, 1m). Default: 10s","ui.settings.toastPosition":"Notification Display Position","ui.settings.position.top-left":"Top Left","ui.settings.position.top-center":"Top Center","ui.settings.position.top-right":"Top Right","ui.settings.position.bottom-left":"Bottom Left","ui.settings.position.bottom-center":"Bottom Center","ui.settings.position.bottom-right":"Bottom Right","ui.settings.colorScheme":"Color Scheme","ui.settings.colorSchemeSwitched":"Switched to {{scheme}} color scheme","ui.settings.colorScheme.default":"Standard","ui.settings.colorScheme.green":"Green","ui.settings.colorScheme.blue":"Blue","ui.settings.colorScheme.skyBlue":"Sky Blue","ui.settings.colorScheme.purple":"Purple","ui.settings.colorScheme.orange":"Orange","ui.settings.colorScheme.rose":"Rose","ui.settings.colorScheme.teal":"Teal","ui.settings.themeAuto":"Auto (18:00-06:00 Dark)","ui.settings.themeLight":"Light","ui.settings.themeDark":"Dark","ui.settings.cloudflaredTestRequired":"Please click the Tunnel Program Download button first","ui.settings.downloadSuccess":"Download successful","ui.settings.downloadFailed":"Download failed","ui.settings.tunnelGatewayConfig":"Tunnel Gateway","ui.settings.tunnelGatewayDesc":"FNS integrated intranet penetration service, allowing your local FNS service to be securely accessed directly via the public network without configuring cumbersome HTTPS and WebSocket proxies, enabling secure access services.\nSupports access via Ngrok or Cloudflare tunnels.","ui.settings.ngrokDesc":'Use Ngrok intranet penetration tunnel to securely expose local services to the public network.\nOfficial application entry: https://dashboard.ngrok.com/get-started/setup\n Note: Ngrok free accounts have various dimension limits, please refer to https://ngrok.com/docs/pricing-limits/free-plan-limits',"ui.settings.customDomain":"Custom Domain","ui.settings.customDomainDesc":"Optional, requires a paid ngrok account","ui.settings.saveNgrok":"Save Settings","ui.settings.cloudflareDesc":'Reverse tunnel built on Cloudflare Zero Trust network, providing higher security and stability.\nOfficial application entry: https://one.dash.cloudflare.com\nBefore saving settings, you need to perform Tunnel Program Download first',"ui.settings.enableLog":"Enable Logging","ui.settings.cloudflareLogDesc":"Logs are recorded in storage/logs/cloudflared_tunnel.log","ui.settings.saveCloudflare":"Save Settings","ui.settings.downloadCloudflared":"Tunnel Program Download","ui.settings.pullSource":"Version Check Source","ui.settings.pullSourceDesc":"Select the source repository for version detection and updates.","ui.settings.pullSource.auto":"Auto Detect","ui.settings.pullSource.github":"github.com","ui.settings.pullSource.cnb":"Tencent cnb.cool","ui.settings.userDatabaseConfig":"Database Enhancement","ui.settings.userDatabaseDesc":"Default SQLite (maintenance-free, supports concurrent read/single write), suitable for personal and small-scale deployments, meeting most needs of individual users.
Recommended scenarios for switching to professional databases (PostgreSQL / MySQL):
  • Multiple users, multiple note libraries with extremely large scale
  • Huge number of notes or large attachment sizes
  • Frequently encountering concurrency-related errors
Professional databases are more reliable for concurrency and large-scale data processing, but will increase hardware requirements and operational costs (requires operational capabilities).
Note: When modifying the database, original data will not be migrated to the new database; you need to force resync the note library; settings must pass the connection test before changing the database type.
","ui.settings.databaseType":"Database Type","ui.settings.databaseType.sqlite":"SQLite (Embedded)","ui.settings.databaseType.mysql":"MySQL","ui.settings.databaseType.postgres":"PostgreSQL","ui.settings.databaseHost":"Database Address (Host)","ui.settings.databasePort":"Port (Port)","ui.settings.databaseUser":"Username (User)","ui.settings.databasePassword":"Password (Password)","ui.settings.databaseName":"Database Name (Database)","ui.settings.databaseMaxIdleConns":"Max Idle Connections (MaxIdle)","ui.settings.databaseMaxOpenConns":"Max Open Connections (MaxOpen)","ui.settings.databaseConnMaxLifetime":"Connection Max Lifetime (MaxLifetime)","ui.settings.databaseConnMaxIdleTime":"Connection Max Idle Time (MaxIdleTime)","ui.settings.databaseMaxWriteConcurrency":"Max Write Concurrency (MaxWriteConcurrency)","ui.settings.databaseSchema":"Database Schema (Postgres)","ui.settings.databaseSslMode":"SSL Mode","ui.settings.mysqlPermissionWarning":"Note: When using MySQL, the provided account must have CREATE DATABASE permissions for user information isolation.","ui.settings.postgresPermissionWarning":"Note: When using PostgreSQL, the provided account must have CREATE DATABASE permissions for user information isolation.","ui.settings.testConnection":"Test Connection","ui.settings.testSuccess":"Connection test successful","ui.settings.testFailed":"Connection test failed","ui.settings.testRequiredBeforeSave":"Please perform a connection test and ensure success before saving","ui.settingsBrowser.title":"Vault Configuration Files","ui.settingsBrowser.description":"Manage vault configuration information (e.g., vault config, plugins, themes, etc.)","ui.settingsBrowser.add":"Add Configuration","ui.settingsBrowser.edit":"Edit Configuration","ui.settingsBrowser.key":"Path","ui.settingsBrowser.value":"Size","ui.settingsBrowser.content":"Content","ui.settingsBrowser.keyRequired":"Path cannot be empty","ui.settingsBrowser.confirmDelete":'Are you sure you want to delete the configuration item "{{key}}"?',"ui.settingsBrowser.rename":"Rename Path","ui.settingsBrowser.newKey":"New Path","ui.settingsBrowser.renameSuccess":"Configuration item renamed successfully","ui.settingsBrowser.noSettings":"No configuration items yet","ui.obsidian.authTokenConfig":"Authorization Configuration","ui.obsidian.authTokenConfigTo":"One-click Authorization to Obsidian","ui.obsidian.oneClickImport":"One-click Authorization to Obsidian","ui.obsidian.copyConfigSuccess":"Authorization configuration copied, please return to the Obsidian plugin to paste the configuration","ui.obsidian.copyConfigError":"Non-HTTPS page, clipboard function unavailable, please manually copy the authorization configuration","ui.system.serviceInfo":"Service Information","ui.system.versionInfo":"Version Information","ui.system.repo":"Project Repository","ui.system.githubRepo":"GitHub Repository","ui.system.cnbMirror":"CNB Mirror","ui.system.currentVersion":"Current Version","ui.system.checkUpdate":"Check for Updates","ui.system.checkNow":"Check Now","ui.system.checking":"Checking...","ui.system.newVersionAvailable":"New Version Available","ui.system.alreadyLatest":"Already Latest Version","ui.system.viewRelease":"View Release","ui.system.upgradeNow":"Upgrade Now","ui.system.upgrading":"Upgrading...","ui.system.upgradeSuccess":"Upgrade triggered successfully, page will refresh shortly","ui.system.upgradeFailed":"Upgrade trigger failed","ui.system.upgradeRefreshTimeout":"Upgrade completed, but the service is not yet available. Please refresh the page manually.","ui.system.viewChangelog":"View Changelog","ui.system.getVersionError":"Failed to get version information","ui.system.getWebGuiConfigError":"Failed to get WebGui configuration:","ui.system.restartService":"Restart Service","ui.system.restartServiceConfirm":"Are you sure you want to restart the service immediately? Connections will be disconnected during the restart.","ui.system.manualGC":"Memory Reclamation","ui.system.manualGCConfirm":"Are you sure you want to perform manual memory reclamation (GC) immediately?","ui.system.manualGCSuccess":"Manual memory reclamation (GC) triggered successfully","ui.system.serverSystemInfo":"Server Information","ui.system.modelName":"Processor Model","ui.system.hostInfo":"System Information","ui.system.systemTime":"System Time","ui.system.runtimeInfo":"Service Information","ui.system.startTime":"Start Time","ui.system.serviceUptime":"Uptime","ui.system.physicalCores":"Cores (Logical/Physical)","ui.system.cpuLoad":"Average Load (1/5/15)","ui.system.totalMemory":"Total Memory","ui.system.usedMemory":"Used Memory","ui.system.memoryUsage":"Memory Usage","ui.system.os":"Operating System","ui.system.kernelVersion":"Kernel Version","ui.system.uptime":"Uptime","ui.system.goVersion":"Runtime Version","ui.system.goroutines":"Number of Goroutines","ui.system.heapMemory":"Heap Memory / Total Usage","ui.system.numGc":"GC Count","ui.system.createdAt":"Created At","ui.system.updatedAt":"Updated At","ui.system.websocketClients":"Online Clients","ui.system.wsNickname":"Nickname","ui.system.wsClientName":"Client","ui.system.wsClientType":"Type","ui.system.wsRemoteAddr":"Address","ui.system.wsStartTime":"Connection Time","ui.system.wsTraceId":"Trace ID","ui.system.wsNoClients":"No online clients","ui.storage.management":"Storage Configuration","ui.storage.add":"Add Storage","ui.storage.edit":"Edit Storage","ui.storage.noStorage":"No storage configuration","ui.storage.type":"Type","ui.storage.storageType.oss":"Aliyun OSS","ui.storage.storageType.s3":"AWS S3","ui.storage.storageType.r2":"Cloudflare R2","ui.storage.storageType.minio":"MinIO","ui.storage.storageType.localfs":"Local Storage","ui.storage.storageType.webdav":"WebDAV","ui.storage.selectType":"Select Storage Type","ui.storage.endpoint":"Endpoint","ui.storage.region":"Region (S3 Region)","ui.storage.accountId":"Account ID (R2 Account ID)","ui.storage.bucketName":"Bucket","ui.storage.accessKeyId":"Access Key ID","ui.storage.accessKeySecret":"Access Key Secret","ui.storage.webdavUrl":"WebDAV URL","ui.storage.webdavUser":"Username","ui.storage.webdavPassword":"Password","ui.storage.customPath":"Custom Path","ui.storage.accessUrlPrefix":"Access URL Prefix","ui.storage.placeholder.endpoint.oss":"oss-cn-hangzhou.aliyuncs.com","ui.storage.placeholder.endpoint.minio":"http://192.168.1.100:9000","ui.storage.placeholder.region":"us-east-1","ui.storage.placeholder.accountId":"your-account-id","ui.storage.placeholder.bucketName":"my-bucket","ui.storage.placeholder.accessKeyId":"","ui.storage.placeholder.accessKeySecret":"","ui.storage.placeholder.webdavUrl":"http://192.168.1.100:5244/dav","ui.storage.placeholder.webdavUser":"admin","ui.storage.placeholder.webdavPassword":"","ui.storage.placeholder.customPath":"data/obsidian","ui.storage.placeholder.accessUrlPrefix":"http://192.168.1.100:5244","ui.storage.help.endpoint.oss":"Aliyun OSS Endpoint address, excluding the Bucket name","ui.storage.help.endpoint.minio":"MinIO service address, must include protocol prefix and port","ui.storage.help.region":"Region where the S3 bucket is located","ui.storage.help.accountId":"Cloudflare Account ID, can be found in the right sidebar of the console","ui.storage.help.webdavUrl":"Must include protocol prefix (http:// or https://) and full path","ui.storage.help.webdavUser":"Login username for the WebDAV service","ui.storage.help.webdavPassword":"Login password for the WebDAV service","ui.storage.help.customPath":"Subdirectory path for file storage, no leading or trailing slashes required","ui.storage.help.accessUrlPrefix":"Prefix address for generating file access links, for display purposes only","ui.storage.confirmDelete":"Are you sure you want to delete this storage configuration?","ui.storage.validate.title":"Test Connection","ui.storage.validate.loading":"Testing...","ui.backup.management":"Task Management","ui.backup.add":"Add Task","ui.backup.edit":"Edit Task","ui.backup.noBackup":"No backup or sync tasks yet","ui.backup.selectType":"Please select a backup type","ui.backup.confirmDelete":"Are you sure you want to delete this backup task?","ui.backup.vault":"Vault","ui.backup.selectVault":"Select Vault","ui.backup.type":"Backup Type","ui.backup.backupType.full":"Full Backup","ui.backup.backupType.incremental":"Incremental Backup","ui.backup.backupType.sync":"Mirror Sync","ui.backup.cronStrategy":"Schedule Strategy","ui.backup.strategy.daily":"Daily Backup (00:00)","ui.backup.strategy.weekly":"Weekly Backup (Monday 00:00)","ui.backup.strategy.monthly":"Monthly Backup (1st 00:00)","ui.backup.strategy.custom":"Custom Cron","ui.backup.cronExpression":"Cron Expression","ui.backup.retentionDays":"Retention Days","ui.backup.fileCountUnit":"files","ui.backup.retentionDays.sync":"History retention days (0: do not clean history, -1: do not retain history)","ui.backup.retentionDays.backup":"Backup package retention days (0: do not clean backups, -1: do not retain historical backups)","ui.backup.includeVaultName.label":"Include Vault Name","ui.backup.includeVaultName.tooltip":"Off: {customPath}/notes/xxx.md\nOn: {customPath}/{vaultName}/notes/xxx.md","ui.backup.noAvailableStorage":"No available storage configuration","ui.backup.addStorageTip":"Please add and enable a storage backend in Storage Management first","ui.backup.storages":"Storage Selection","ui.backup.validation.vaultRequired":"Please select a vault","ui.backup.validation.typeRequired":"Please select a backup type","ui.backup.validation.strategyRequired":"Please select a schedule strategy","ui.backup.validation.storageRequired":"Please select at least one storage","ui.backup.validation.retentionDaysMin":"Retention days must be ≥ -1","ui.backup.validation.cronExpressionRequired":"Cron expression cannot be empty under custom strategy","ui.backup.lastRunTime":"Last Run","ui.backup.nextRunTime":"Next Run","ui.backup.executeNow":"Execute Now","ui.backup.executeSuccess":"Manual backup trigger successful","ui.backup.status.0":"Waiting","ui.backup.status.1":"In Progress","ui.backup.status.2":"Success","ui.backup.status.3":"Failed","ui.backup.status.4":"Cancelled","ui.backup.status.5":"No Updates","ui.backup.history.title":"Task History","ui.backup.history.startTime":"Task Execution Time","ui.backup.history.storage":"Storage","ui.backup.history.status":"Status","ui.backup.history.backupStats":"Backup Statistics","ui.backup.history.syncStats":"Sync Statistics","ui.backup.history.backupFile":"Backup File","ui.backup.history.message":"Message","ui.backup.history.copyError":"Copy Error Message","ui.backup.history.noData":"No task records yet","ui.git.title":"Git Automation Management","ui.git.config":"Git Repository Configuration","ui.git.repoUrl":"Git Repository URL (SSH URLs not supported)","ui.git.status":"Git Status Overview","ui.git.history":"Automated Commit History","ui.git.comingSoon":"Git automation features coming soon...","ui.git.configDesc":"Configure your Git repository URL, branch, and authentication details.","ui.git.statusDesc":"Real-time monitoring of the current repository's commit status and sync progress.","ui.git.historyDesc":"View recent automated commit records.","ui.git.retentionDays":"History Retention Days","ui.git.retentionDaysDesc":"0: Do not clean history, -1: Do not retain history","ui.git.addConfig":"Add Git Sync Configuration","ui.git.editConfig":"Edit Git Sync Configuration","ui.git.loading":"Loading configuration...","ui.git.noConfig":"No Git sync configurations yet","ui.git.addFirst":"Add your first repository now","ui.git.lastCommit":"Last Commit","ui.git.checkTime":"Last Check","ui.git.neverRun":"Never executed","ui.git.execute.title":"Trigger Sync Immediately","ui.git.clean.title":"Clean Workspace","ui.git.clean.confirm":"Are you sure you want to clean this Git workspace? This will delete the local clone and reinitialize it.","ui.git.delete.confirm":"Are you sure you want to delete this Git configuration? This action cannot be undone.","ui.git.form.branch":"Branch Name","ui.git.form.delay":"Auto-sync Delay (seconds)","ui.git.history.title":"Commit History","ui.git.history.configId":"Config ID","ui.git.history.startTime":"Commit Time","ui.git.history.endTime":"End Time","ui.git.history.status":"Status","ui.git.history.message":"Message","ui.git.history.noData":"No commit records yet","ui.git.history.duration":"Commit Duration","ui.git.history.vault":"Vault","ui.git.status.0":"Idle","ui.git.status.1":"Checking","ui.git.status.2":"Success","ui.git.status.3":"Failed","ui.git.status.4":"Stopped","ui.git.validate.title":"Check Connection","ui.git.validate.loading":"Checking...","ui.validation.git.vaultRequired":"Please select a vault","ui.validation.git.repoUrlRequired":"Please enter the repository URL","ui.validation.git.repoUrlInvalid":"Please enter a valid URL","ui.validation.git.branchRequired":"Please enter the branch name","ui.validation.git.delayMin":"Delay time cannot be negative","ui.validation.git.retentionDaysMin":"Retention days must be ≥ -1","ui.validation.storage.typeRequired":"Please select a storage type","ui.validation.storage.accessUrlPrefixRequired":"Access URL prefix cannot be empty","ui.validation.vault.nameRequired":"Vault name cannot be empty","api.git.list.error":"Failed to fetch Git configuration list","api.git.save.success":"Configuration saved successfully","api.git.save.error":"Failed to save configuration","api.git.delete.success":"Configuration deleted successfully","api.git.delete.error":"Failed to delete configuration","api.git.execute.success":"Sync task triggered","api.git.execute.error":"Failed to trigger sync","api.git.clean.success":"Workspace cleaned successfully","api.git.clean.error":"Failed to clean workspace","api.git.history.error":"Failed to fetch history records","api.git.validate.success":"Git repository connection test successful","api.git.validate.error":"Git repository connection test failed","api.backup.configList.error":"Failed to fetch backup configuration list","api.backup.delete.success":"Deletion successful","api.backup.delete.error":"Failed to delete backup configuration","api.backup.save.success":"Save successful","api.backup.save.error":"Failed to save backup configuration","api.backup.execute.success":"Manual trigger successful","api.backup.execute.error":"Failed to trigger backup","api.backup.history.error":"Failed to retrieve backup history","api.system.restart.success":"Service restart triggered","api.system.restart.error":"Failed to request service restart","api.system.gc.success":"Garbage collection triggered","api.system.gc.error":"Failed to request garbage collection","api.storage.list.error":"Failed to retrieve storage configuration list","api.storage.delete.success":"Deletion successful","api.storage.delete.error":"Failed to delete storage configuration","api.storage.save.error":"Failed to save storage configuration","api.storage.types.error":"Failed to retrieve storage types","api.storage.validate.success":"Storage connection test successful","api.storage.validate.error":"Storage connection test failed","error.storage.webdav.unauthorized":"WebDAV authentication failed, please check username and password","error.storage.webdav.forbidden":"Insufficient WebDAV permissions, please check account write permissions","error.storage.webdav.notFound":"WebDAV path does not exist, please check custom path configuration","error.storage.webdav.methodNotAllowed":"WebDAV method not allowed, please confirm WebDAV is enabled on the server","error.storage.webdav.unreachable":"WebDAV address is unreachable, please check the address in storage configuration","error.storage.webdav.connectionRefused":"WebDAV connection refused, please confirm if the service is running","error.storage.webdav.timeout":"WebDAV connection timed out, please check network or service status","error.storage.webdav.generic":"WebDAV operation failed, please check storage configuration","error.storage.s3.noSuchBucket":"S3 bucket does not exist, please check bucket name configuration","error.storage.s3.accessDenied":"S3 access denied, please check Access Key permissions","error.storage.s3.unreachable":"S3 endpoint is unreachable, please check Endpoint address","error.storage.s3.timeout":"S3 connection timed out, please check network or endpoint configuration","error.storage.oss.noSuchBucket":"OSS bucket does not exist, please check Bucket name","error.storage.oss.accessDenied":"OSS access denied, please check AccessKey permissions","error.storage.oss.unreachable":"OSS endpoint is unreachable, please check Endpoint address","error.storage.local.noPermission":"No write permission for local storage","error.storage.local.createDirFailed":"Failed to create local storage directory","error.storage.local.permissionDenied":"Local storage permission denied","error.backup.partialFailure":"Partial file sync failed, please check detailed error information","error.backup.uploadFailed":"Backup file upload failed","error.backup.openFileFailed":"Failed to open backup file","error.backup.vaultNotExist":"Vault does not exist, please check configuration","error.network.unreachable":"Target address is unreachable, please check network configuration","error.network.connectionRefused":"Connection refused, please confirm if the target service is running","error.network.timeout":"Connection timed out, please check network status","ui.support.title":"Support this project","ui.support.supportRequest":"If this project has helped you and you want it to continue development, please support us in the following ways. Thank you for your support of open source software!","ui.support.listTitle":"Support List","ui.support.noData":"No records yet","ui.support.item":"Item","ui.support.amount":"Amount","ui.support.time":"Time","ui.support.message":"Message","ui.support.thanks":"Thank you everyone for supporting Fast Note Sync! The support list will be updated irregularly","ui.support.sortDefault":"Default (Amount)","ui.support.sortTime":"By Time","ui.support.sort":"Sort","ui.support.buyMeACoffee":"Buy Me a Coffee","ui.support.wechatReward":"WeChat Reward Support","ui.share.invalidLink":"Invalid share link. Missing ID or Token.","ui.share.errorTitle":"Share Error","ui.share.noteNotFound":"Shared note not found.","ui.share.poweredByPrefix":"Powered by ","ui.share.poweredBySuffix":" ","ui.share.version":"Version","ui.share.tabActive":"Sharing","ui.share.noShares":"No share records yet","ui.share.viewShare":"View Share Page","ui.share.cancelShare":"Cancel Share","ui.share.cancelConfirm":"Are you sure you want to cancel the share for this note? The share link will become invalid immediately after cancellation.","ui.share.shareNotFound":"Share record not found","ui.share.title":"Share Note","ui.share.checking":"Checking share status...","ui.share.create":"Enable Share","ui.share.creating":"Enabling...","ui.share.success":"Share successful","ui.share.shortLinkCreate":"Generate Short Link","ui.share.link":"Share Link","ui.share.shortLink":"Short Link","ui.share.copy":"Copy Link","ui.share.copySuccess":"Link copied to clipboard","ui.share.shortLinkCopy":"Copy Short Link","ui.share.cancelSuccess":"Share cancelled","ui.share.buttonCreating":"Enabling...","ui.share.passwordRequired":"Password Required","ui.share.passwordHint":"This share is password protected. Please enter the password to view the content.","ui.share.passwordPlaceholder":"Enter password...","ui.share.preview":"Content Preview","ui.syncLog.title":"Vault Update Logs","ui.syncLog.vault":"Vault","ui.syncLog.type":"Type","ui.syncLog.action":"Action","ui.syncLog.path":"File Path","ui.syncLog.size":"Size","ui.syncLog.client":"Client","ui.syncLog.status":"Status","ui.syncLog.message":"Details","ui.syncLog.time":"Record Time","ui.syncLog.changedFields":"Changed Fields","ui.syncLog.noLogs":"No Sync Records","ui.syncLog.noLogsDescription":"No update records found","ui.syncLog.description":"Trace all sync change records for notes, attachments, folders, and configuration files in vaults","ui.syncLog.allVaults":"All Vaults","ui.syncLog.allTypes":"All Types","ui.syncLog.allActions":"All Actions","ui.syncLog.resetFilters":"Reset Filters","ui.syncLog.statusSuccess":"Success","ui.syncLog.statusFailed":"Failed","ui.syncLog.type.note":"Note","ui.syncLog.type.file":"Attachment","ui.syncLog.type.setting":"Configuration","ui.syncLog.type.folder":"Folder","ui.syncLog.action.create":"Create","ui.syncLog.action.modify":"Modify","ui.syncLog.action.soft_delete":"Soft Delete","ui.syncLog.action.delete":"Permanent Delete","ui.syncLog.action.rename":"Rename","ui.syncLog.action.restore":"Restore"};export{e as default}; ================================================ FILE: frontend/assets/eye-DrvrOb4o.js ================================================ import{c as a}from"./font-loader-CIrh3KnA.js"; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const e=a("EyeOff",[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",key:"ct8e1f"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242",key:"151rxh"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",key:"13bj9a"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]),c=a("Eye",[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */export{c as E,e as a}; ================================================ FILE: frontend/assets/file-manager-Bz0QGSbU.js ================================================ import{c as e,u as s,r as t,j as a,B as r,X as l,a0 as n,a7 as o,I as i,V as c,C as d,t as m}from"./font-loader-CIrh3KnA.js";import{A as h,m as u,E as x,P as p,u as f,C as g,f as j}from"./main-BIi-kGYY.js";import{u as b,a as v,R as w,F as N,C as y}from"./canvas-viewer-Cxwbo1vR.js";import{S as C,a as k,b as H,c as P,d as S}from"./select-CJF_alSt.js";import{C as z}from"./checkbox-DhTHgmeh.js";import{T as M}from"./tooltip-Dr-qRlmI.js";import{F,f as V}from"./format-CdHm7RWL.js";import{F as R,I as A}from"./image-BFJJNQpe.js";import{D}from"./download-CKtDCbjj.js";import{S as B}from"./search-DdihTHF8.js";import{R as E}from"./refresh-cw-BxIJAPy3.js";import{C as I}from"./clock-C9LPHszx.js";import{C as T}from"./markdown-editor-CX5kQlgI.js";import{A as L,D as K}from"./database-eyf5nvY6.js";import{T as $}from"./trash-2-ad7PiUnC.js";import{T as O}from"./text-cursor-input-Bphfsfyn.js";import"./index-JfsWWBj_.js";import"./zap-CLLhzk_y.js";import"./pencil-DqQhr35g.js"; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const W=e("Music",[["path",{d:"M9 18V5l12-2v13",key:"1jmyc2"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["circle",{cx:"18",cy:"16",r:"3",key:"1hluhg"}]]),_=e("Video",[["path",{d:"m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5",key:"ftymec"}],["rect",{x:"2",y:"6",width:"14",height:"12",rx:"2",key:"158x01"}]]); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */function G({file:e,url:o,onClose:i}){var c;const{t:d}=s(),m=(null==(c=e.path.split(".").pop())?void 0:c.toLowerCase())||"",f=["jpg","jpeg","png","gif","svg","webp","bmp"].includes(m),g=["mp3","wav","flac","ogg","m4a"].includes(m),j=["mp4","webm","mkv","avi","mov"].includes(m),b="pdf"===m,v=["js","ts","jsx","tsx","py","sh","bat","go","css","html","json","c","cpp","rs","php"].includes(m),w=e.path.split("/").pop()||e.path,N=t.useRef(null),[y,C]=t.useState(!0);t.useEffect(()=>{C(!0)},[o]),t.useEffect(()=>{const e=localStorage.getItem("preview-volume");e&&N.current&&(N.current.volume=parseFloat(e))},[o]);const k=e=>{localStorage.setItem("preview-volume",e.currentTarget.volume.toString())},H=()=>{C(!1)};return a.jsx(h,{children:a.jsxs(u.div,{initial:{opacity:0,y:50,scale:.9},animate:{opacity:1,y:0,scale:1},exit:{opacity:0,y:50,scale:.9},className:"fixed bottom-6 right-6 z-100 w-[320px] sm:w-100 bg-card border border-border rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[80vh]",children:[a.jsxs("div",{className:"flex items-center justify-between p-3 border-b border-border bg-muted/50",children:[a.jsxs("div",{className:"flex flex-col min-w-0",children:[a.jsx("span",{className:"text-xs font-medium text-muted-foreground uppercase tracking-wider",children:d(f?"ui.file.imagePreview":g?"ui.file.audioPreview":j?"ui.file.videoPreview":b?"ui.file.pdfPreview":v?"ui.file.codePreview":"ui.file.detail")}),a.jsx("h3",{className:"text-sm font-semibold truncate pr-2",title:w,children:w})]}),a.jsxs("div",{className:"flex items-center gap-1 shrink-0",children:[a.jsx(r,{variant:"ghost",size:"icon",className:"h-8 w-8 rounded-xl","aria-label":d("ui.file.openInNewWindow"),onClick:()=>window.open(o,"_blank"),children:a.jsx(x,{className:"h-4 w-4"})}),a.jsx(r,{variant:"ghost",size:"icon",className:"h-8 w-8 rounded-xl hover:bg-destructive/10 hover:text-destructive","aria-label":d("ui.common.close"),onClick:i,children:a.jsx(l,{className:"h-4 w-4"})})]})]}),a.jsxs("div",{className:"relative p-4 flex items-center justify-center bg-black/5 min-h-50 overflow-hidden text-center",children:[a.jsx(h,{children:y&&a.jsx(u.div,{initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},className:"absolute inset-0 z-10 flex flex-col items-center justify-center bg-card/80 backdrop-blur-sm pointer-events-none",children:a.jsx(n,{className:"h-8 w-8 animate-spin text-primary mb-2"})})}),f&&a.jsx("img",{src:o,alt:w,className:"max-w-full max-h-[70vh] object-contain rounded-lg shadow-sm transition-opacity duration-300 "+(y?"opacity-0":"opacity-100"),onLoad:H,onError:H},o),g&&a.jsxs("div",{className:"w-full py-8",children:[a.jsx("div",{className:"mb-4 flex justify-center",children:a.jsx("div",{className:"w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center text-primary animate-pulse",children:a.jsx("svg",{className:"w-8 h-8",fill:"currentColor",viewBox:"0 0 24 24",children:a.jsx("path",{d:"M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"})})})}),a.jsx("audio",{ref:N,src:o,controls:!0,autoPlay:!0,className:"w-full",onVolumeChange:k,onLoadedMetadata:H,onCanPlay:H,onError:H})]},o),j&&a.jsx("video",{ref:N,src:o,controls:!0,autoPlay:!0,className:"max-w-full max-h-[70vh] object-contain rounded-lg shadow-sm transition-opacity duration-300 "+(y?"opacity-0":"opacity-100"),onVolumeChange:k,onLoadedMetadata:H,onCanPlay:H,onError:H},o),!f&&!g&&!j&&a.jsxs("div",{className:"flex flex-col items-center gap-4 py-6",children:[a.jsx("div",{className:"w-20 h-20 rounded-2xl bg-primary/5 flex items-center justify-center text-primary/60 border border-primary/10",children:b?a.jsx(F,{className:"w-10 h-10"}):v?a.jsx(R,{className:"w-10 h-10"}):a.jsx(p,{className:"w-10 h-10"})}),a.jsxs("div",{className:"text-center",children:[a.jsx("p",{className:"text-sm text-muted-foreground",children:d("ui.file.unsupportedPreview")}),a.jsx(r,{variant:"link",className:"text-primary mt-1 h-auto p-0",onClick:()=>window.open(o,"_blank"),children:d("ui.file.openInNewWindow")})]})]})]}),a.jsx("div",{className:"p-3 border-t border-border bg-muted/30 flex justify-end",children:a.jsxs(r,{variant:"default",size:"sm",className:"rounded-xl gap-2 text-xs",onClick:()=>window.open(o,"_blank"),children:[a.jsx(D,{className:"h-3.5 w-3.5"}),d("ui.file.browserDownload")]})})]})})}function q(e){if(0===e)return"0 B";const s=Math.floor(Math.log(e)/Math.log(1024));return Math.round(e/Math.pow(1024,s)*100)/100+" "+["B","KB","MB","GB"][s]}function U({vault:e,vaults:n,onVaultChange:m,isRecycle:h=!1,page:u,setPage:x,pageSize:j,setPageSize:y,searchKeyword:D,setSearchKeyword:K,currentPath:U,setCurrentPath:J,currentPathHash:Q,setCurrentPathHash:X,pathHashMap:Y,setPathHashMap:Z,onCanvasOpen:ee}){const{t:se}=s(),{handleFileList:te,handleDeleteFile:ae,handleRestoreFile:re,getRawFileUrl:le,handleFolderFiles:ne,handleFolderList:oe,handlePermanentDeleteFile:ie,handleClearFileRecycle:ce,handleRenameFile:de}=b(),{openConfirmDialog:me}=o(),[he,ue]=t.useState([]),[xe,pe]=t.useState(!1),[fe,ge]=t.useState(0),[je,be]=t.useState(D),[ve,we]=t.useState("mtime"),[Ne,ye]=t.useState("desc"),[Ce,ke]=t.useState(new Set),[He,Pe]=t.useState(null),[Se,ze]=t.useState("folder"),[Me,Fe]=t.useState([]),Ve=t.useRef(0),{trashType:Re,setModule:Ae}=f(),[De,Be]=t.useState(null),[Ee,Ie]=t.useState("");t.useEffect(()=>{const e=setTimeout(()=>{be(D)},300);return()=>clearTimeout(e)},[D]);const Te=(s=u,t=j,a=je)=>{const r=++Ve.current;pe(!0),"folder"!==Se||h?te(e,s,t,h,a,ve,Ne,e=>{var s;r===Ve.current&&(ue((null==e?void 0:e.list)||[]),ge((null==(s=null==e?void 0:e.pager)?void 0:s.totalRows)||0),pe(!1))}):oe(e,U,Q,a=>{r===Ve.current&&(Fe(a||[]),ne(e,U,Q,s,t,ve,Ne,e=>{var s;r===Ve.current&&(ue((null==e?void 0:e.list)||[]),ge((null==(s=null==e?void 0:e.pager)?void 0:s.totalRows)||0),pe(!1))}))})};t.useEffect(()=>{Te(u,j,je),ke(new Set)},[e,u,j,je,h,ve,Ne,Se,U]),t.useEffect(()=>{je&&ze("flat")},[je,U,Se,x]);const Le=e=>{e>=1&&e<=Math.ceil(fe/j)&&x(e)},Ke=e=>{if(!e.contentHash)return;const s=new Set(Ce);s.has(e.pathHash)?s.delete(e.pathHash):s.add(e.pathHash),ke(s)},$e=e=>{var s;const t=(null==(s=e.split(".").pop())?void 0:s.toLowerCase())||"";return["jpg","jpeg","png","gif","svg","webp","bmp"].includes(t)?a.jsx(A,{className:"h-5 w-5"}):"pdf"===t?a.jsx(F,{className:"h-5 w-5"}):["mp3","wav","flac","ogg","m4a"].includes(t)?a.jsx(W,{className:"h-5 w-5"}):["mp4","webm","mkv","avi","mov"].includes(t)?a.jsx(_,{className:"h-5 w-5"}):["js","ts","jsx","tsx","py","sh","bat","go","css","html","json","c","cpp","rs","php"].includes(t)?a.jsx(R,{className:"h-5 w-5"}):a.jsx(p,{className:"h-5 w-5"})},Oe=Math.ceil(fe/j);return a.jsxs("div",{className:"w-full flex flex-col space-y-4",children:[a.jsxs("div",{className:"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 py-1",children:[a.jsx("div",{className:"flex items-center gap-3",children:n&&m&&a.jsxs(C,{value:e,onValueChange:m,children:[a.jsx(k,{className:"w-auto min-w-45 rounded-xl",children:a.jsx(H,{placeholder:se("ui.common.selectVault")})}),a.jsx(P,{className:"rounded-xl",children:n.map(e=>a.jsx(S,{value:e.vault,className:"rounded-xl",children:e.vault},e.id))})]})}),a.jsx("div",{className:"flex flex-col gap-2 w-full sm:w-auto",children:a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsxs("div",{className:"relative flex-1 sm:w-64",children:[a.jsx(B,{className:"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none"}),a.jsx(i,{type:"text",placeholder:se("ui.file.searchPlaceholder"),className:"pl-9 pr-8 rounded-xl",value:D,onChange:e=>K(e.target.value)}),D&&a.jsx("button",{className:"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",onClick:()=>K(""),children:a.jsx(l,{className:"h-4 w-4"})})]}),a.jsx(r,{variant:"outline",size:"icon","aria-label":se("ui.common.refresh"),onClick:()=>Te(),disabled:xe,className:"rounded-xl shrink-0",children:a.jsx(E,{className:"h-4 w-4 "+(xe?"animate-spin":"")})})]})})]}),"folder"===Se&&!h&&U&&a.jsxs("div",{className:"flex items-center gap-2 px-1 text-sm text-muted-foreground overflow-x-auto whitespace-nowrap scrollbar-hide",children:[a.jsx("button",{className:"hover:text-primary transition-colors flex items-center",onClick:()=>{J(""),X(""),x(1)},children:e}),U.split("/").filter(Boolean).map((e,s,t)=>a.jsxs(c.Fragment,{children:[a.jsx(d,{className:"h-4 w-4 shrink-0"}),a.jsx("button",{className:"transition-colors "+(s===t.length-1?"text-foreground font-medium pointer-events-none":"hover:text-primary"),onClick:()=>{const e=t.slice(0,s+1).join("/");J(e),X(Y[e]||""),x(1)},children:e})]},`breadcrumb-${s}`))]}),!h&&a.jsxs("div",{className:"flex flex-wrap items-center gap-4 py-2 px-2 bg-muted/30 rounded-xl border border-border/50",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsxs("div",{className:"flex items-center h-8 rounded-lg border border-border overflow-hidden bg-background shadow-sm",children:[a.jsx("button",{className:"px-4 h-full text-xs font-medium transition-colors "+("folder"===Se?"bg-primary text-primary-foreground":"hover:bg-muted"),onClick:()=>{K(""),be(""),ze("folder")},children:se("ui.note.viewFolder")}),a.jsx("button",{className:"px-4 h-full text-xs font-medium transition-colors border-l border-border "+("flat"===Se?"bg-primary text-primary-foreground":"hover:bg-muted"),onClick:()=>ze("flat"),children:se("ui.note.viewFlat")})]}),a.jsxs("span",{className:"text-sm font-medium text-muted-foreground mr-2",children:[fe," ",se("ui.file.file")]})]}),a.jsxs("div",{className:"flex items-center h-8 rounded-xl border border-border overflow-hidden bg-background shadow-sm ml-auto",children:[a.jsxs("button",{className:"px-3 h-full text-xs flex items-center gap-1.5 transition-colors "+("mtime"===ve?"bg-accent text-accent-foreground":"hover:bg-muted"),onClick:()=>we("mtime"),children:[a.jsx(I,{className:"h-3.5 w-3.5"}),se("ui.note.sortByMtime")]}),a.jsxs("button",{className:"px-3 h-full text-xs flex items-center gap-1.5 transition-colors border-l border-border "+("ctime"===ve?"bg-accent text-accent-foreground":"hover:bg-muted"),onClick:()=>we("ctime"),children:[a.jsx(T,{className:"h-3.5 w-3.5"}),se("ui.note.sortByCtime")]}),a.jsxs("button",{className:"px-3 h-full text-xs flex items-center gap-1.5 transition-colors border-l border-border "+("path"===ve?"bg-accent text-accent-foreground":"hover:bg-muted"),onClick:()=>we("path"),children:[a.jsx(F,{className:"h-3.5 w-3.5"}),se("ui.note.sortByPath")]}),a.jsx(M,{content:se("desc"===Ne?"ui.note.sortDesc":"ui.note.sortAsc"),side:"top",delay:200,children:a.jsx("button",{className:"px-2.5 h-full text-xs flex items-center transition-colors border-l border-border hover:bg-muted",onClick:()=>ye("desc"===Ne?"asc":"desc"),children:"desc"===Ne?a.jsx(L,{className:"h-3.5 w-3.5"}):a.jsx(v,{className:"h-3.5 w-3.5"})})})]})]}),h&&a.jsxs("div",{className:"flex flex-wrap items-center gap-4 py-2 px-2 bg-muted/30 rounded-xl border border-border/50",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsxs("div",{className:"flex items-center h-8 rounded-lg border border-border overflow-hidden bg-background shadow-sm",children:[a.jsx("button",{className:"px-4 h-full text-xs font-medium transition-colors "+("notes"===Re?"bg-primary text-primary-foreground":"hover:bg-muted"),onClick:()=>Ae("trash","notes"),children:se("ui.note.note")}),a.jsx("button",{className:"px-4 h-full text-xs font-medium transition-colors border-l border-border "+("files"===Re?"bg-primary text-primary-foreground":"hover:bg-muted"),onClick:()=>Ae("trash","files"),children:se("ui.file.file")})]}),a.jsxs("span",{className:"text-sm font-medium text-muted-foreground mr-2",children:[fe," ",se("ui.nav.menuTrash"),se("ui.file.file")]}),he.length>0&&a.jsxs(r,{variant:"ghost",size:"sm",onClick:()=>{me(se("ui.file.clearRecycleConfirm"),"confirm",()=>{ce(e,()=>{Te()})})},className:"h-8 rounded-lg text-destructive hover:bg-destructive/10 hover:text-destructive",children:[a.jsx($,{className:"h-3.5 w-3.5 mr-1.5"}),se("ui.common.clear")]})]}),he.length>0&&a.jsxs("div",{className:"flex items-center gap-3 pl-4 border-l border-border/60",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(z,{id:"select-all",checked:he.filter(e=>e.contentHash).length>0&&Ce.size===he.filter(e=>e.contentHash).length,onCheckedChange:()=>{const e=he.filter(e=>e.contentHash);Ce.size===e.length&&e.length>0?ke(new Set):ke(new Set(e.map(e=>e.pathHash)))},className:"rounded-md"}),a.jsx("label",{htmlFor:"select-all",className:"text-xs font-medium cursor-pointer text-muted-foreground hover:text-foreground transition-colors",children:se("ui.common.selectAll")})]}),Ce.size>0&&a.jsxs("div",{className:"flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200",children:[a.jsx("span",{className:"text-xs text-primary font-semibold bg-primary/10 px-2 py-0.5 rounded-full",children:se("ui.file.selectedCount",{count:Ce.size})}),a.jsxs(r,{variant:"outline",size:"sm",onClick:()=>{if(0===Ce.size)return;const s=he.filter(e=>Ce.has(e.pathHash)&&e.contentHash);0!==s.length&&me(se("ui.file.batchRestoreConfirm",{count:s.length}),"confirm",async()=>{pe(!0);const t=s.length;try{for(let a=0;a{re(e,s[a].path,s[a].pathHash,t)}),new Promise(e=>setTimeout(e,3e4))])}finally{Pe(null),ke(new Set),Te()}})},disabled:!he.some(e=>Ce.has(e.pathHash)&&e.contentHash),className:"h-8 rounded-lg text-green-600 border-green-200 hover:bg-green-50 hover:text-green-700 hover:border-green-300 shadow-sm",children:[a.jsx(w,{className:"h-3.5 w-3.5 mr-1.5"}),se("ui.file.batchRestore")]}),a.jsxs(r,{variant:"outline",size:"sm",onClick:()=>{if(0===Ce.size)return;const s=he.filter(e=>Ce.has(e.pathHash));0!==s.length&&me(se("ui.common.batchPermanentDeleteConfirm",{count:s.length}),"confirm",async()=>{pe(!0);const t=s.length;try{for(let a=0;a{ie(e,s[a].path,s[a].pathHash,t)}),new Promise(e=>setTimeout(e,3e4))])}finally{Pe(null),ke(new Set),Te()}})},className:"h-8 rounded-lg text-destructive border-destructive/20 hover:bg-destructive/5 hover:text-destructive hover:border-destructive/40 shadow-sm",children:[a.jsx($,{className:"h-3.5 w-3.5 mr-1.5"}),se("ui.common.batchPermanentDelete")]})]})]}),a.jsxs("div",{className:"flex items-center h-8 rounded-xl border border-border overflow-hidden bg-background shadow-sm ml-auto",children:[a.jsxs("button",{className:"px-3 h-full text-xs flex items-center gap-1.5 transition-colors "+("mtime"===ve?"bg-accent text-accent-foreground":"hover:bg-muted"),onClick:()=>we("mtime"),children:[a.jsx(I,{className:"h-3.5 w-3.5"}),se("ui.note.sortByMtime")]}),a.jsxs("button",{className:"px-3 h-full text-xs flex items-center gap-1.5 transition-colors border-l border-border "+("ctime"===ve?"bg-accent text-accent-foreground":"hover:bg-muted"),onClick:()=>we("ctime"),children:[a.jsx(T,{className:"h-3.5 w-3.5"}),se("ui.note.sortByCtime")]}),a.jsxs("button",{className:"px-3 h-full text-xs flex items-center gap-1.5 transition-colors border-l border-border "+("path"===ve?"bg-accent text-accent-foreground":"hover:bg-muted"),onClick:()=>we("path"),children:[a.jsx(F,{className:"h-3.5 w-3.5"}),se("ui.note.sortByPath")]}),a.jsx(M,{content:se("desc"===Ne?"ui.note.sortDesc":"ui.note.sortAsc"),side:"top",delay:200,children:a.jsx("button",{className:"px-2.5 h-full text-xs flex items-center transition-colors border-l border-border hover:bg-muted",onClick:()=>ye("desc"===Ne?"asc":"desc"),children:"desc"===Ne?a.jsx(L,{className:"h-3.5 w-3.5"}):a.jsx(v,{className:"h-3.5 w-3.5"})})})]})]}),xe?a.jsxs("div",{className:"rounded-xl border border-border bg-card p-12 text-center text-muted-foreground",children:[a.jsx(E,{className:"h-6 w-6 animate-spin mx-auto mb-2"}),He?`${He.current} / ${He.total}`:se("ui.common.loading")]}):Array.isArray(he)&&0!==he.length||Array.isArray(Me)&&0!==Me.length&&"flat"!==Se?a.jsx("div",{className:"-mx-2 px-2",children:a.jsxs("div",{className:"grid grid-cols-1 gap-3 py-1",children:["folder"===Se&&!h&&Array.isArray(Me)&&Me.map(e=>a.jsx("article",{className:"rounded-xl border border-border bg-card p-4 cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary/30",onClick:()=>{Z({...Y,[e.path]:e.pathHash}),J(e.path),X(e.pathHash),x(1)},children:a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("div",{className:"flex items-start gap-3 min-w-0 flex-1",children:[a.jsx("span",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/10 text-blue-500 shrink-0",children:a.jsx(N,{className:"h-5 w-5 fill-current opacity-70"})}),a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h3",{className:"font-semibold text-card-foreground truncate",children:e.path.split("/").pop()}),a.jsxs("div",{className:"flex flex-wrap items-center gap-x-4 gap-y-1 mt-1.5 text-xs text-muted-foreground",children:[a.jsx(M,{content:se("ui.common.createdAt"),side:"top",delay:300,children:a.jsxs("span",{className:"hidden sm:flex items-center gap-1",children:[a.jsx(T,{className:"h-3.5 w-3.5"}),V(new Date(e.ctime),"yyyy-MM-dd HH:mm")]})}),a.jsx(M,{content:se("ui.common.updatedAt"),side:"top",delay:300,children:a.jsxs("span",{className:"flex items-center gap-1",children:[a.jsx(I,{className:"h-3.5 w-3.5"}),V(new Date(e.mtime),"yyyy-MM-dd HH:mm")]})})]})]})]}),a.jsx("div",{className:"shrink-0",children:a.jsx(d,{className:"h-5 w-5 text-muted-foreground"})})]})},`folder-${e.pathHash}`)),Array.isArray(he)&&he.map(s=>a.jsx("article",{className:"rounded-xl border border-border bg-card p-4 transition-all duration-200 hover:shadow-md hover:border-primary/30 cursor-pointer",onClick:()=>(s=>{var t;if(s.path.toLowerCase().endsWith(".canvas")&&ee)return void ee({path:s.path,pathHash:s.pathHash});let a=le(e,s.path,null==(t=s.pathHash)?void 0:t.toString());h&&(a+=(a.includes("?")?"&":"?")+"isRecycle=1"),Be(s),Ie(a)})(s),children:a.jsxs("div",{className:"flex items-center justify-between gap-4",children:[a.jsxs("div",{className:"flex items-start gap-3 min-w-0 flex-1",children:[h&&a.jsx("div",{className:"flex items-center self-center "+(s.contentHash?"":"opacity-30"),onClick:e=>((e,s)=>{e.stopPropagation(),Ke(s)})(e,s),children:a.jsx(z,{checked:Ce.has(s.pathHash),onClick:e=>e.stopPropagation(),onCheckedChange:()=>{!xe&&s.contentHash&&Ke(s)},disabled:!s.contentHash,className:"rounded-md"})}),a.jsx("span",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0",children:$e(s.path)}),a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h3",{className:"font-semibold text-card-foreground truncate",children:"folder"!==Se||h?s.path:s.path.split("/").pop()}),a.jsxs("div",{className:"flex flex-wrap items-center gap-x-4 gap-y-1 mt-1.5 text-xs text-muted-foreground",children:[a.jsx("span",{className:"flex items-center gap-1",children:q(s.size)}),a.jsx(M,{content:se("ui.common.createdAt"),side:"top",delay:300,children:a.jsxs("span",{className:"hidden sm:flex items-center gap-1",children:[a.jsx(T,{className:"h-3.5 w-3.5"}),V(new Date(s.ctime),"yyyy-MM-dd HH:mm")]})}),a.jsx(M,{content:se("ui.common.updatedAt"),side:"top",delay:300,children:a.jsxs("span",{className:"flex items-center gap-1",children:[a.jsx(I,{className:"h-3.5 w-3.5"}),V(new Date(s.mtime),"yyyy-MM-dd HH:mm")]})})]})]})]}),a.jsxs("div",{className:"flex items-center gap-1 shrink-0",children:[!h&&a.jsxs(a.Fragment,{children:[a.jsx(M,{content:se("ui.common.rename"),side:"top",delay:200,children:a.jsx(r,{variant:"ghost",size:"icon",className:"h-8 w-8 rounded-xl text-muted-foreground hover:text-blue-500",onClick:t=>((s,t)=>{s.stopPropagation();const r=t.path.split("/").pop()||"",l=r.lastIndexOf("."),n=-1!==l?r.substring(l):"",o=-1!==l?r.substring(0,l):r;let c=o;me(se("ui.file.renameFile"),"confirm",()=>{if(!c||c===o)return;const s=c.endsWith(n)?c:c+n,a=t.path.includes("/")?t.path.substring(0,t.path.lastIndexOf("/")+1):"";de({vault:e,oldPath:t.path,path:a+s,oldPathHash:t.pathHash},()=>{Te()})},a.jsx("div",{className:"pt-2",children:a.jsx(i,{autoFocus:!0,defaultValue:o,placeholder:se("ui.file.renameFilePlaceholder"),onChange:e=>{c=e.target.value}})}))})(t,s),children:a.jsx(O,{className:"h-4 w-4"})})}),a.jsx(M,{content:se("ui.common.delete"),side:"top",delay:200,children:a.jsx(r,{variant:"ghost",size:"icon",className:"h-8 w-8 rounded-xl text-muted-foreground hover:text-destructive",onClick:t=>((s,t)=>{s.stopPropagation(),me(se("ui.file.deleteFileConfirm",{title:t.path}),"confirm",()=>{ae(e,t.path,t.pathHash,()=>{Te()})})})(t,s),children:a.jsx($,{className:"h-4 w-4"})})})]}),h&&a.jsxs(a.Fragment,{children:[a.jsx(M,{content:se("ui.common.restore"),side:"top",delay:200,children:a.jsx(r,{variant:"ghost",size:"icon",disabled:!s.contentHash,className:"h-8 w-8 rounded-xl text-muted-foreground hover:text-green-600",onClick:t=>((s,t)=>{s.stopPropagation(),t.contentHash&&me(se("ui.file.restoreFileConfirm",{title:t.path}),"confirm",()=>{re(e,t.path,t.pathHash,()=>{Te()})})})(t,s),children:a.jsx(w,{className:"h-4 w-4"})})}),a.jsx(M,{content:se("ui.common.permanentDelete"),side:"top",delay:200,children:a.jsx(r,{variant:"ghost",size:"icon",className:"h-8 w-8 rounded-xl text-muted-foreground hover:text-destructive",onClick:t=>((s,t)=>{s.stopPropagation(),me(se("ui.file.permanentDeleteConfirm",{title:t.path}),"confirm",()=>{ie(e,t.path,t.pathHash,()=>{Te()})})})(t,s),children:a.jsx($,{className:"h-4 w-4"})})})]})]})]})},`file-${s.pathHash}`))]})}):a.jsx("div",{className:"rounded-xl border border-border bg-card p-12 text-center text-muted-foreground",children:se("ui.file.noFiles")}),he.length>0&&a.jsxs("div",{className:"flex flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-4 pt-2 shrink-0",children:[a.jsxs("div",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:[a.jsxs("span",{children:[se("ui.common.of")," ",fe," ",se("ui.file.results")]}),a.jsxs(C,{value:j.toString(),onValueChange:e=>{const s=parseInt(e);y(s),x(1)},children:[a.jsx(k,{className:"h-8 w-25 rounded-xl",children:a.jsx(H,{placeholder:j.toString()})}),a.jsx(P,{className:"rounded-xl",children:[10,20,50,100].map(e=>a.jsxs(S,{value:e.toString(),className:"rounded-xl",children:[e," ",se("ui.common.perPage")]},e))})]})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsxs(r,{variant:"outline",size:"sm",onClick:()=>Le(u-1),disabled:1===u||xe,className:"rounded-xl",children:[a.jsx(g,{className:"h-4 w-4"}),se("ui.common.previous")]}),a.jsxs("span",{className:"text-sm font-medium px-2",children:[u," / ",Oe]}),a.jsxs(r,{variant:"outline",size:"sm",onClick:()=>Le(u+1),disabled:u===Oe||xe,className:"rounded-xl",children:[se("ui.common.next"),a.jsx(d,{className:"h-4 w-4"})]})]})]}),De&&a.jsx(G,{file:De,url:Ee,onClose:()=>{Be(null),Ie("")}},Ee)]})}function J({vault:e,onVaultChange:l,onNavigateToVaults:n,isRecycle:o=!1}){const{t:i}=s(),[c,d]=t.useState([]),h=t.useRef(!1),[u,x]=t.useState(1),[p,f]=t.useState(()=>{const e=localStorage.getItem("filePageSize");return e?parseInt(e,10):10}),[g,b]=t.useState(""),[v,w]=t.useState(""),[N,C]=t.useState(""),[k,H]=t.useState({}),[P,S]=t.useState(null);t.useEffect(()=>{localStorage.setItem("filePageSize",p.toString())},[p]);const{handleVaultList:z}=j();t.useEffect(()=>{let e=!0;return(async()=>{try{await z(s=>{e&&d(s)})}catch(s){if(!e)return;m.error(s instanceof Error?s.message:String(s)),d([])}finally{e&&(h.current=!0)}})(),()=>{e=!1}},[z]),t.useEffect(()=>{x(1),w(""),C(""),H({}),S(null)},[e]);const M=t.useCallback(e=>{S(e)},[]),F=t.useCallback(()=>{S(null)},[]);return h.current&&0===c.length?a.jsxs("div",{className:"rounded-xl border border-border bg-card p-12 flex flex-col items-center justify-center",children:[a.jsx(K,{className:"h-16 w-16 text-muted-foreground/30 mb-4"}),a.jsx("h3",{className:"text-lg font-semibold text-foreground mb-2",children:i("ui.file.noVaults")}),a.jsx("p",{className:"text-muted-foreground mb-6 text-center",children:i("ui.file.createVaultFirst")}),a.jsx(r,{onClick:()=>{n&&n()},className:"rounded-xl",children:i("ui.note.goToVaultManagement")})]}):P?a.jsx(y,{vault:e,note:P,onBack:F,isRecycle:o}):a.jsx(U,{vault:e,vaults:c,onVaultChange:l,isRecycle:o,page:u,setPage:x,pageSize:p,setPageSize:f,searchKeyword:g,setSearchKeyword:b,currentPath:v,setCurrentPath:w,currentPathHash:N,setCurrentPathHash:C,pathHashMap:k,setPathHashMap:H,onCanvasOpen:M})}export{J as FileManager}; ================================================ FILE: frontend/assets/file-type-DbD_pFnN.js ================================================ import{c as a}from"./font-loader-CIrh3KnA.js"; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const e=a("FileType",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M9 13v-1h6v1",key:"1bb014"}],["path",{d:"M12 12v6",key:"3ahymv"}],["path",{d:"M11 18h2",key:"12mj7e"}]]);export{e as F}; ================================================ FILE: frontend/assets/font-loader-B-ynJ_1p.css ================================================ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-950:oklch(28.6% .066 53.813);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-500:oklch(69.6% .17 162.48);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-700:oklch(50% .134 242.749);--color-sky-900:oklch(39.1% .09 240.876);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-violet-300:oklch(81.1% .111 293.571);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-pink-500:oklch(65.6% .241 354.308);--color-rose-500:oklch(64.5% .246 16.439);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-950:oklch(13% .028 261.692);--color-zinc-500:oklch(55.2% .016 285.938);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-2xl:1rem;--radius-3xl:1.5rem;--shadow-2xs:var(--shadow-2xs);--shadow-xs:var(--shadow-xs);--shadow-sm:var(--shadow-sm);--shadow-md:var(--shadow-md);--shadow-lg:var(--shadow-lg);--shadow-xl:var(--shadow-xl);--shadow-2xl:var(--shadow-2xl);--ease-out:cubic-bezier(0,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0,0,.2,1)infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--shadow:var(--shadow)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);color:var(--foreground);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;position:relative}#root{position:relative}body:before,body:after{content:"";filter:blur(60px);opacity:.5;z-index:0;pointer-events:none;border-radius:9999px;position:fixed}html.dark body:before,.dark body:before,html.dark body:after,.dark body:after{opacity:.35}.custom-shadow{box-shadow:0 18px 40px #0f172a1a}html.dark .custom-shadow,.dark .custom-shadow{box-shadow:0 18px 40px #00000040}html{touch-action:manipulation;scroll-behavior:smooth;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}body{padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom);padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right);overscroll-behavior:none}button,[role=button]{cursor:pointer}button,[role=button],input,select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent}button:not([role=checkbox]):not([data-state]),input:not([type=checkbox]):not([type=radio]),select,textarea{min-height:36px}button[data-size=icon-sm],button[data-size=icon-lg]{width:36px;min-width:36px;height:36px}@media(pointer:coarse){.min-touch-target{min-width:44px;min-height:44px}}input,textarea{font-size:16px}@media(max-width:768px){body:before{filter:blur(50px);opacity:.4;width:250px;height:250px;top:-8%;left:-8%}body:after{filter:blur(40px);opacity:.35;width:180px;height:180px;bottom:3%;right:-5%}}@media(max-width:480px){body:before{opacity:.3;width:180px;height:180px}body:after{opacity:.25;width:120px;height:120px}}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.-top-1{top:calc(var(--spacing)*-1)}.top-0{top:calc(var(--spacing)*0)}.top-1{top:calc(var(--spacing)*1)}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing)*2)}.top-4{top:calc(var(--spacing)*4)}.top-\[50\%\]{top:50%}.-right-1{right:calc(var(--spacing)*-1)}.-right-1\.5{right:calc(var(--spacing)*-1.5)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.right-6{right:calc(var(--spacing)*6)}.right-\[-4px\]{right:-4px}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-1{bottom:calc(var(--spacing)*1)}.bottom-2{bottom:calc(var(--spacing)*2)}.bottom-6{bottom:calc(var(--spacing)*6)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.left-3{left:calc(var(--spacing)*3)}.left-\[-4px\]{left:-4px}.left-\[25px\]{left:25px}.left-\[50\%\]{left:50%}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-100{z-index:100}.z-\[9999\]{z-index:9999}.col-span-2{grid-column:span 2/span 2}.col-span-full{grid-column:1/-1}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.-mx-2{margin-inline:calc(var(--spacing)*-2)}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-1\.5{margin-inline:calc(var(--spacing)*1.5)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-auto{margin-inline:auto}.-my-0\.5{margin-block:calc(var(--spacing)*-.5)}.my-1{margin-block:calc(var(--spacing)*1)}.my-1\.5{margin-block:calc(var(--spacing)*1.5)}.my-3{margin-block:calc(var(--spacing)*3)}.my-4{margin-block:calc(var(--spacing)*4)}.my-6{margin-block:calc(var(--spacing)*6)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-7{margin-top:calc(var(--spacing)*7)}.mt-8{margin-top:calc(var(--spacing)*8)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-1\.5{margin-right:calc(var(--spacing)*1.5)}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-3{margin-right:calc(var(--spacing)*3)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-0\.5{margin-left:calc(var(--spacing)*.5)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-6{margin-left:calc(var(--spacing)*6)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-3\/5{width:60%;height:60%}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.size-8{width:calc(var(--spacing)*8);height:calc(var(--spacing)*8)}.size-9{width:calc(var(--spacing)*9);height:calc(var(--spacing)*9)}.size-10{width:calc(var(--spacing)*10);height:calc(var(--spacing)*10)}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-4\.5{height:calc(var(--spacing)*4.5)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-16{height:calc(var(--spacing)*16)}.h-20{height:calc(var(--spacing)*20)}.h-48{height:calc(var(--spacing)*48)}.h-64{height:calc(var(--spacing)*64)}.h-\[1px\]{height:1px}.h-\[220px\]{height:220px}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-auto{height:auto}.h-dvh{height:100dvh}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-40{max-height:calc(var(--spacing)*40)}.max-h-48{max-height:calc(var(--spacing)*48)}.max-h-60{max-height:calc(var(--spacing)*60)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[70vh\]{max-height:70vh}.max-h-\[80vh\]{max-height:80vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[300px\]{max-height:300px}.max-h-\[500px\]{max-height:500px}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-30{min-height:calc(var(--spacing)*30)}.min-h-50{min-height:calc(var(--spacing)*50)}.min-h-\[28px\]{min-height:28px}.min-h-\[80px\]{min-height:80px}.min-h-\[300px\]{min-height:300px}.min-h-\[450px\]{min-height:450px}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-1{width:calc(var(--spacing)*1)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-4\.5{width:calc(var(--spacing)*4.5)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-20{width:calc(var(--spacing)*20)}.w-24{width:calc(var(--spacing)*24)}.w-25{width:calc(var(--spacing)*25)}.w-40{width:calc(var(--spacing)*40)}.w-48{width:calc(var(--spacing)*48)}.w-56{width:calc(var(--spacing)*56)}.w-\[44px\]{width:44px}.w-\[80px\]{width:80px}.w-\[90vw\]{width:90vw}.w-\[95vw\]{width:95vw}.w-\[100px\]{width:100px}.w-\[110px\]{width:110px}.w-\[120px\]{width:120px}.w-\[150px\]{width:150px}.w-\[180px\]{width:180px}.w-\[320px\]{width:320px}.w-\[calc\(100\%-0\.75rem\)\]{width:calc(100% - .75rem)}.w-\[calc\(100vw-2rem\)\]{width:calc(100vw - 2rem)}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-20{max-width:calc(var(--spacing)*20)}.max-w-37\.5{max-width:calc(var(--spacing)*37.5)}.max-w-65{max-width:calc(var(--spacing)*65)}.max-w-75{max-width:calc(var(--spacing)*75)}.max-w-225{max-width:calc(var(--spacing)*225)}.max-w-\[150px\]{max-width:150px}.max-w-\[200px\]{max-width:200px}.max-w-\[250px\]{max-width:250px}.max-w-\[300px\]{max-width:300px}.max-w-\[calc\(100vw-3rem\)\]{max-width:calc(100vw - 3rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-7\.5{min-width:calc(var(--spacing)*7.5)}.min-w-20{min-width:calc(var(--spacing)*20)}.min-w-30{min-width:calc(var(--spacing)*30)}.min-w-32{min-width:calc(var(--spacing)*32)}.min-w-45{min-width:calc(var(--spacing)*45)}.min-w-\[3\.5rem\]{min-width:3.5rem}.min-w-\[8rem\]{min-width:8rem}.min-w-\[110px\]{min-width:110px}.min-w-\[120px\]{min-width:120px}.min-w-\[140px\]{min-width:140px}.min-w-\[180px\]{min-width:180px}.min-w-\[200px\]{min-width:200px}.min-w-\[250px\]{min-width:250px}.min-w-\[300px\]{min-width:300px}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.\[transform-origin\:center\]{transform-origin:50%}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[1px\]{--tw-translate-y:1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-200{--tw-scale-x:200%;--tw-scale-y:200%;--tw-scale-z:200%;scale:var(--tw-scale-x)var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.touch-none{touch-action:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[80px_1fr_80px\]{grid-template-columns:80px 1fr 80px}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:calc(var(--spacing)*0)}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing)*2)}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-x-6{column-gap:calc(var(--spacing)*6)}:where(.space-x-0\.5>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*.5)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:calc(var(--spacing)*1)}.gap-y-2{row-gap:calc(var(--spacing)*2)}.gap-y-3{row-gap:calc(var(--spacing)*3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border\/20>:not(:last-child)){border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){:where(.divide-border\/20>:not(:last-child)){border-color:color-mix(in oklab,var(--border)20%,transparent)}}.self-center{align-self:center}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-none{border-radius:0}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-r-lg{border-top-right-radius:var(--radius);border-bottom-right-radius:var(--radius)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-background{border-color:var(--background)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-400{border-color:var(--color-blue-400)}.border-border,.border-border\/10{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/10{border-color:color-mix(in oklab,var(--border)10%,transparent)}}.border-border\/30{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/30{border-color:color-mix(in oklab,var(--border)30%,transparent)}}.border-border\/40{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/40{border-color:color-mix(in oklab,var(--border)40%,transparent)}}.border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/50{border-color:color-mix(in oklab,var(--border)50%,transparent)}}.border-border\/60{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/60{border-color:color-mix(in oklab,var(--border)60%,transparent)}}.border-border\/70{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/70{border-color:color-mix(in oklab,var(--border)70%,transparent)}}.border-cyan-400{border-color:var(--color-cyan-400)}.border-destructive\/10{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/10{border-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.border-destructive\/20{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/20{border-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.border-emerald-500\/20{border-color:#00bb7f33}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/20{border-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.border-gray-400{border-color:var(--color-gray-400)}.border-green-200{border-color:var(--color-green-200)}.border-green-200\/50{border-color:#b9f8cf80}@supports (color:color-mix(in lab,red,red)){.border-green-200\/50{border-color:color-mix(in oklab,var(--color-green-200)50%,transparent)}}.border-green-300{border-color:var(--color-green-300)}.border-green-400{border-color:var(--color-green-400)}.border-green-500\/10{border-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.border-green-500\/10{border-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.border-input{border-color:var(--input)}.border-muted-foreground\/20{border-color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.border-muted-foreground\/20{border-color:color-mix(in oklab,var(--muted-foreground)20%,transparent)}}.border-orange-400{border-color:var(--color-orange-400)}.border-orange-500\/10{border-color:#fe6e001a}@supports (color:color-mix(in lab,red,red)){.border-orange-500\/10{border-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.border-primary,.border-primary\/10{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/10{border-color:color-mix(in oklab,var(--primary)10%,transparent)}}.border-primary\/20{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/20{border-color:color-mix(in oklab,var(--primary)20%,transparent)}}.border-primary\/50{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/50{border-color:color-mix(in oklab,var(--primary)50%,transparent)}}.border-purple-400{border-color:var(--color-purple-400)}.border-red-200\/50{border-color:#ffcaca80}@supports (color:color-mix(in lab,red,red)){.border-red-200\/50{border-color:color-mix(in oklab,var(--color-red-200)50%,transparent)}}.border-red-400{border-color:var(--color-red-400)}.border-rose-500\/20{border-color:#ff235733}@supports (color:color-mix(in lab,red,red)){.border-rose-500\/20{border-color:color-mix(in oklab,var(--color-rose-500)20%,transparent)}}.border-sidebar-border{border-color:var(--sidebar-border)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-teal-400{border-color:var(--color-teal-400)}.border-transparent{border-color:#0000}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.border-yellow-400{border-color:var(--color-yellow-400)}.border-x-transparent{border-inline-color:#0000}.border-y-border{border-block-color:var(--border)}.border-y-transparent{border-block-color:#0000}.border-t-transparent{border-top-color:#0000}.border-t-white{border-top-color:var(--color-white)}.border-r-border,.border-r-border\/30{border-right-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-r-border\/30{border-right-color:color-mix(in oklab,var(--border)30%,transparent)}}.border-r-transparent{border-right-color:#0000}.border-l-blue-600{border-left-color:var(--color-blue-600)}.border-l-green-600{border-left-color:var(--color-green-600)}.border-l-muted{border-left-color:var(--muted)}.border-l-orange-500{border-left-color:var(--color-orange-500)}.border-l-transparent{border-left-color:#0000}.bg-\[\#1E88E5\]{background-color:#1e88e5}.bg-\[\#7C4DFF\]{background-color:#7c4dff}.bg-\[\#08b94e\]{background-color:#08b94e}.bg-\[\#FF8A33\]{background-color:#ff8a33}.bg-accent{background-color:var(--accent)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500)10%,transparent)}}.bg-background,.bg-background\/50{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){.bg-background\/50{background-color:color-mix(in oklab,var(--background)50%,transparent)}}.bg-background\/80{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){.bg-background\/80{background-color:color-mix(in oklab,var(--background)80%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/5{background-color:#0000000d}@supports (color:color-mix(in lab,red,red)){.bg-black\/5{background-color:color-mix(in oklab,var(--color-black)5%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab,red,red)){.bg-black\/80{background-color:color-mix(in oklab,var(--color-black)80%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.bg-border\/50{background-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.bg-border\/50{background-color:color-mix(in oklab,var(--border)50%,transparent)}}.bg-border\/60{background-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.bg-border\/60{background-color:color-mix(in oklab,var(--border)60%,transparent)}}.bg-card,.bg-card\/10{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/10{background-color:color-mix(in oklab,var(--card)10%,transparent)}}.bg-card\/50{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/50{background-color:color-mix(in oklab,var(--card)50%,transparent)}}.bg-card\/80{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/80{background-color:color-mix(in oklab,var(--card)80%,transparent)}}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-destructive,.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/5{background-color:color-mix(in oklab,var(--destructive)5%,transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/10{background-color:color-mix(in oklab,var(--color-emerald-500)10%,transparent)}}.bg-gray-50{background-color:var(--color-gray-50)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500\/5{background-color:#00c7580d}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/5{background-color:color-mix(in oklab,var(--color-green-500)5%,transparent)}}.bg-green-500\/10{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/10{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.bg-muted,.bg-muted\/20{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/20{background-color:color-mix(in oklab,var(--muted)20%,transparent)}}.bg-muted\/30{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/30{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.bg-muted\/40{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/40{background-color:color-mix(in oklab,var(--muted)40%,transparent)}}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.bg-muted\/60{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/60{background-color:color-mix(in oklab,var(--muted)60%,transparent)}}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-500\/5{background-color:#fe6e000d}@supports (color:color-mix(in lab,red,red)){.bg-orange-500\/5{background-color:color-mix(in oklab,var(--color-orange-500)5%,transparent)}}.bg-orange-500\/10{background-color:#fe6e001a}@supports (color:color-mix(in lab,red,red)){.bg-orange-500\/10{background-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.bg-popover,.bg-popover\/95{background-color:var(--popover)}@supports (color:color-mix(in lab,red,red)){.bg-popover\/95{background-color:color-mix(in oklab,var(--popover)95%,transparent)}}.bg-primary,.bg-primary\/5{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/5{background-color:color-mix(in oklab,var(--primary)5%,transparent)}}.bg-primary\/10{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-500{background-color:var(--color-purple-500)}.bg-purple-500\/10{background-color:#ac4bff1a}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/10{background-color:color-mix(in oklab,var(--color-purple-500)10%,transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-ring{background-color:var(--ring)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-500\/10{background-color:#ff23571a}@supports (color:color-mix(in lab,red,red)){.bg-rose-500\/10{background-color:color-mix(in oklab,var(--color-rose-500)10%,transparent)}}.bg-secondary,.bg-secondary\/20{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.bg-secondary\/20{background-color:color-mix(in oklab,var(--secondary)20%,transparent)}}.bg-secondary\/50{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.bg-secondary\/50{background-color:color-mix(in oklab,var(--secondary)50%,transparent)}}.bg-sidebar{background-color:var(--sidebar)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-teal-50{background-color:var(--color-teal-50)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab,red,red)){.bg-white\/80{background-color:color-mix(in oklab,var(--color-white)80%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-gradient-to-l{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-muted\/60{--tw-gradient-from:var(--muted)}@supports (color:color-mix(in lab,red,red)){.from-muted\/60{--tw-gradient-from:color-mix(in oklab,var(--muted)60%,transparent)}}.from-muted\/60{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.fill-current{fill:currentColor}.fill-red-500\/10{fill:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.fill-red-500\/10{fill:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.stroke-\[1\.5\]{stroke-width:1.5px}.object-contain{object-fit:contain}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-2\.5{padding:calc(var(--spacing)*2.5)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.p-24{padding:calc(var(--spacing)*24)}.px-0\.5{padding-inline:calc(var(--spacing)*.5)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.py-12{padding-block:calc(var(--spacing)*12)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-0\.5{padding-top:calc(var(--spacing)*.5)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-1\.5{padding-top:calc(var(--spacing)*1.5)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-7{padding-right:calc(var(--spacing)*7)}.pr-8{padding-right:calc(var(--spacing)*8)}.pr-10{padding-right:calc(var(--spacing)*10)}.pr-14{padding-right:calc(var(--spacing)*14)}.pb-0\.5{padding-bottom:calc(var(--spacing)*.5)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-18{padding-bottom:calc(var(--spacing)*18)}.pb-24{padding-bottom:calc(var(--spacing)*24)}.pb-\[env\(safe-area-inset-bottom\)\]{padding-bottom:env(safe-area-inset-bottom)}.pl-0{padding-left:calc(var(--spacing)*0)}.pl-1{padding-left:calc(var(--spacing)*1)}.pl-1\.5{padding-left:calc(var(--spacing)*1.5)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-6{padding-left:calc(var(--spacing)*6)}.pl-8{padding-left:calc(var(--spacing)*8)}.pl-9{padding-left:calc(var(--spacing)*9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.9em\]{font-size:.9em}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.leading-7{--tw-leading:calc(var(--spacing)*7);line-height:calc(var(--spacing)*7)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-nowrap{text-wrap:nowrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#3DDC84\]{color:#3ddc84}.text-\[\#0078D4\]{color:#0078d4}.text-\[\#4285F4\]{color:#4285f4}.text-\[\#555555\]{color:#555}.text-accent-foreground{color:var(--accent-foreground)}.text-amber-500{color:var(--color-amber-500)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-blue-500\/70{color:#3080ffb3}@supports (color:color-mix(in lab,red,red)){.text-blue-500\/70{color:color-mix(in oklab,var(--color-blue-500)70%,transparent)}}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-card-foreground{color:var(--card-foreground)}.text-current{color:currentColor}.text-cyan-400{color:var(--color-cyan-400)}.text-destructive{color:var(--destructive)}.text-destructive-foreground{color:var(--destructive-foreground)}.text-destructive\/70{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/70{color:color-mix(in oklab,var(--destructive)70%,transparent)}}.text-destructive\/80{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/80{color:color-mix(in oklab,var(--destructive)80%,transparent)}}.text-destructive\/90{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/90{color:color-mix(in oklab,var(--destructive)90%,transparent)}}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-500\/70{color:#00bb7fb3}@supports (color:color-mix(in lab,red,red)){.text-emerald-500\/70{color:color-mix(in oklab,var(--color-emerald-500)70%,transparent)}}.text-foreground,.text-foreground\/80{color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.text-foreground\/80{color:color-mix(in oklab,var(--foreground)80%,transparent)}}.text-foreground\/90{color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.text-foreground\/90{color:color-mix(in oklab,var(--foreground)90%,transparent)}}.text-gray-400{color:var(--color-gray-400)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-600\/80{color:#00a544cc}@supports (color:color-mix(in lab,red,red)){.text-green-600\/80{color:color-mix(in oklab,var(--color-green-600)80%,transparent)}}.text-green-700{color:var(--color-green-700)}.text-green-900{color:var(--color-green-900)}.text-indigo-500\/70{color:#625fffb3}@supports (color:color-mix(in lab,red,red)){.text-indigo-500\/70{color:color-mix(in oklab,var(--color-indigo-500)70%,transparent)}}.text-muted-foreground,.text-muted-foreground\/30{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/30{color:color-mix(in oklab,var(--muted-foreground)30%,transparent)}}.text-muted-foreground\/40{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/40{color:color-mix(in oklab,var(--muted-foreground)40%,transparent)}}.text-muted-foreground\/50{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/50{color:color-mix(in oklab,var(--muted-foreground)50%,transparent)}}.text-muted-foreground\/60{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/60{color:color-mix(in oklab,var(--muted-foreground)60%,transparent)}}.text-muted-foreground\/70{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/70{color:color-mix(in oklab,var(--muted-foreground)70%,transparent)}}.text-muted-foreground\/80{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/80{color:color-mix(in oklab,var(--muted-foreground)80%,transparent)}}.text-orange-400{color:var(--color-orange-400)}.text-orange-500{color:var(--color-orange-500)}.text-orange-500\/70{color:#fe6e00b3}@supports (color:color-mix(in lab,red,red)){.text-orange-500\/70{color:color-mix(in oklab,var(--color-orange-500)70%,transparent)}}.text-orange-600{color:var(--color-orange-600)}.text-orange-600\/80{color:#f05100cc}@supports (color:color-mix(in lab,red,red)){.text-orange-600\/80{color:color-mix(in oklab,var(--color-orange-600)80%,transparent)}}.text-pink-500\/70{color:#f6339ab3}@supports (color:color-mix(in lab,red,red)){.text-pink-500\/70{color:color-mix(in oklab,var(--color-pink-500)70%,transparent)}}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-primary\/60{color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.text-primary\/60{color:color-mix(in oklab,var(--primary)60%,transparent)}}.text-primary\/70{color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.text-primary\/70{color:color-mix(in oklab,var(--primary)70%,transparent)}}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-500\/70{color:#ac4bffb3}@supports (color:color-mix(in lab,red,red)){.text-purple-500\/70{color:color-mix(in oklab,var(--color-purple-500)70%,transparent)}}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-900{color:var(--color-red-900)}.text-rose-500{color:var(--color-rose-500)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-sidebar-foreground{color:var(--sidebar-foreground)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-700{color:var(--color-slate-700)}.text-teal-400{color:var(--color-teal-400)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-red-400{-webkit-text-decoration-color:var(--color-red-400);text-decoration-color:var(--color-red-400)}.underline-offset-4{text-underline-offset:4px}.accent-primary{accent-color:var(--primary)}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:var(--shadow-2xl);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:var(--shadow-lg);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:var(--shadow-md);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:var(--shadow-xl);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:var(--shadow-xs);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-primary\/20{--tw-shadow-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.shadow-primary\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--primary)20%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-background{--tw-ring-color:var(--background)}.ring-ring,.ring-ring\/30{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.ring-ring\/30{--tw-ring-color:color-mix(in oklab,var(--ring)30%,transparent)}}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.fade-in-0{--tw-enter-opacity:0}.fill-mode-forwards{--tw-animation-fill-mode:forwards;animation-fill-mode:forwards}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}.zoom-in-95{--tw-enter-scale:.95}.\[animation-direction\:alternate\]{animation-direction:alternate}.fade-in{--tw-enter-opacity:0}.running{animation-play-state:running}.slide-in-from-bottom-4{--tw-enter-translate-y:calc(4*var(--spacing))}.slide-in-from-left-2{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.slide-in-from-top-2{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.slide-in-from-top-4{--tw-enter-translate-y:calc(4*var(--spacing)*-1)}.zoom-in{--tw-enter-scale:0}.group-focus-within\:text-primary:is(:where(.group):focus-within *){color:var(--primary)}@media(hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:border-primary\/20:is(:where(.group):hover *){border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.group-hover\:border-primary\/20:is(:where(.group):hover *){border-color:color-mix(in oklab,var(--primary)20%,transparent)}}.group-hover\:bg-primary\/10:is(:where(.group):hover *){background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-primary\/10:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--primary)}.group-hover\:text-slate-400:is(:where(.group):hover *){color:var(--color-slate-400)}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/stat\:text-primary:is(:where(.group\/stat):hover *){color:var(--primary)}}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-70:is(:where(.peer):disabled~*){opacity:.7}.selection\:bg-blue-100 ::selection{background-color:var(--color-blue-100)}.selection\:bg-blue-100::selection{background-color:var(--color-blue-100)}.selection\:text-blue-900 ::selection{color:var(--color-blue-900)}.selection\:text-blue-900::selection{color:var(--color-blue-900)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.first\:mt-0:first-child{margin-top:calc(var(--spacing)*0)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing)*0)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-primary\/20:focus-within{--tw-ring-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.focus-within\:ring-primary\/20:focus-within{--tw-ring-color:color-mix(in oklab,var(--primary)20%,transparent)}}@media(hover:hover){.hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.hover\:border-destructive\/40:hover{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:border-destructive\/40:hover{border-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.hover\:border-destructive\/50:hover{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:border-destructive\/50:hover{border-color:color-mix(in oklab,var(--destructive)50%,transparent)}}.hover\:border-green-300:hover{border-color:var(--color-green-300)}.hover\:border-green-500\/30:hover{border-color:#00c7584d}@supports (color:color-mix(in lab,red,red)){.hover\:border-green-500\/30:hover{border-color:color-mix(in oklab,var(--color-green-500)30%,transparent)}}.hover\:border-orange-500\/30:hover{border-color:#fe6e004d}@supports (color:color-mix(in lab,red,red)){.hover\:border-orange-500\/30:hover{border-color:color-mix(in oklab,var(--color-orange-500)30%,transparent)}}.hover\:border-primary\/20:hover{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:border-primary\/20:hover{border-color:color-mix(in oklab,var(--primary)20%,transparent)}}.hover\:border-primary\/30:hover{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:border-primary\/30:hover{border-color:color-mix(in oklab,var(--primary)30%,transparent)}}.hover\:border-primary\/50:hover{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab,var(--primary)50%,transparent)}}.hover\:bg-accent:hover,.hover\:bg-accent\/30:hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/30:hover{background-color:color-mix(in oklab,var(--accent)30%,transparent)}}.hover\:bg-accent\/50:hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/50:hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.hover\:bg-accent\/80:hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/80:hover{background-color:color-mix(in oklab,var(--accent)80%,transparent)}}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-destructive\/5:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/5:hover{background-color:color-mix(in oklab,var(--destructive)5%,transparent)}}.hover\:bg-destructive\/10:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/10:hover{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.hover\:bg-destructive\/80:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/80:hover{background-color:color-mix(in oklab,var(--destructive)80%,transparent)}}.hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-500\/10:hover{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/10:hover{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.hover\:bg-muted:hover,.hover\:bg-muted\/30:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/30:hover{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.hover\:bg-muted\/80:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab,var(--muted)80%,transparent)}}.hover\:bg-orange-500\/10:hover{background-color:#fe6e001a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-orange-500\/10:hover{background-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.hover\:bg-primary\/5:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/5:hover{background-color:color-mix(in oklab,var(--primary)5%,transparent)}}.hover\:bg-primary\/10:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/10:hover{background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.hover\:bg-primary\/80:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab,var(--primary)80%,transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.hover\:bg-sky-900:hover{background-color:var(--color-sky-900)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-destructive:hover{color:var(--destructive)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-green-600:hover{color:var(--color-green-600)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-muted-foreground:hover{color:var(--muted-foreground)}.hover\:text-orange-500:hover{color:var(--color-orange-500)}.hover\:text-primary:hover{color:var(--primary)}.hover\:text-purple-600:hover{color:var(--color-purple-600)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow:var(--shadow-md);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-primary\/30:hover{--tw-shadow-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:shadow-primary\/30:hover{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--primary)30%,transparent)var(--tw-shadow-alpha),transparent)}}.hover\:ring-ring\/50:hover{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.hover\:ring-ring\/50:hover{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}}.focus\:border-primary\/50:focus{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.focus\:border-primary\/50:focus{border-color:color-mix(in oklab,var(--primary)50%,transparent)}}.focus\:bg-accent:focus,.focus\:bg-accent\/80:focus{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.focus\:bg-accent\/80:focus{background-color:color-mix(in oklab,var(--accent)80%,transparent)}}.focus\:bg-destructive\/10:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.focus\:bg-destructive\/10:focus{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:text-destructive:focus{color:var(--destructive)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-primary\/20:focus{--tw-ring-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab,var(--primary)20%,transparent)}}.focus\:ring-primary\/50:focus{--tw-ring-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.focus\:ring-primary\/50:focus{--tw-ring-color:color-mix(in oklab,var(--primary)50%,transparent)}}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-0:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-primary:focus-visible{--tw-ring-color:var(--primary)}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.active\:cursor-grabbing:active{cursor:grabbing}.active\:bg-accent:active{background-color:var(--accent)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media(hover:hover){.disabled\:hover\:bg-transparent:disabled:hover{background-color:#0000}}.has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--spacing)*2.5)}.has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--spacing)*3)}.has-\[\>svg\]\:px-4:has(>svg){padding-inline:calc(var(--spacing)*4)}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.data-\[state\=active\]\:bg-card[data-state=active]{background-color:var(--card)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:text-primary[data-state=active]{color:var(--primary)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--primary)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50% }.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48% }.data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:var(--muted-foreground)}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50% }.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48% }.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}@media(min-width:40rem){.sm\:mt-0{margin-top:calc(var(--spacing)*0)}.sm\:mt-6{margin-top:calc(var(--spacing)*6)}.sm\:mr-2{margin-right:calc(var(--spacing)*2)}.sm\:mb-4{margin-bottom:calc(var(--spacing)*4)}.sm\:mb-6{margin-bottom:calc(var(--spacing)*6)}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:inline-flex{display:inline-flex}.sm\:h-3\.5{height:calc(var(--spacing)*3.5)}.sm\:h-4{height:calc(var(--spacing)*4)}.sm\:h-5{height:calc(var(--spacing)*5)}.sm\:h-7{height:calc(var(--spacing)*7)}.sm\:h-8{height:calc(var(--spacing)*8)}.sm\:h-10{height:calc(var(--spacing)*10)}.sm\:h-auto{height:auto}.sm\:max-h-64{max-height:calc(var(--spacing)*64)}.sm\:max-h-\[90vh\]{max-height:90vh}.sm\:w-3\.5{width:calc(var(--spacing)*3.5)}.sm\:w-4{width:calc(var(--spacing)*4)}.sm\:w-5{width:calc(var(--spacing)*5)}.sm\:w-7{width:calc(var(--spacing)*7)}.sm\:w-8{width:calc(var(--spacing)*8)}.sm\:w-10{width:calc(var(--spacing)*10)}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:w-100{width:calc(var(--spacing)*100)}.sm\:w-auto{width:auto}.sm\:max-w-\[300px\]{max-width:300px}.sm\:max-w-md{max-width:var(--container-md)}.sm\:max-w-none{max-width:none}.sm\:scale-175{--tw-scale-x:175%;--tw-scale-y:175%;--tw-scale-z:175%;scale:var(--tw-scale-x)var(--tw-scale-y)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:calc(var(--spacing)*0)}.sm\:gap-2{gap:calc(var(--spacing)*2)}.sm\:gap-3{gap:calc(var(--spacing)*3)}.sm\:gap-4{gap:calc(var(--spacing)*4)}:where(.sm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:rounded-xl{border-radius:calc(var(--radius) + 4px)}.sm\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.sm\:p-4{padding:calc(var(--spacing)*4)}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:px-2{padding-inline:calc(var(--spacing)*2)}.sm\:px-3{padding-inline:calc(var(--spacing)*3)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:py-4{padding-block:calc(var(--spacing)*4)}.sm\:pt-6{padding-top:calc(var(--spacing)*6)}.sm\:pl-4{padding-left:calc(var(--spacing)*4)}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:48rem){.md\:relative{position:relative}.md\:bottom-auto{bottom:auto}.md\:left-auto{left:auto}.md\:col-span-2{grid-column:span 2/span 2}.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline-flex{display:inline-flex}.md\:h-4{height:calc(var(--spacing)*4)}.md\:h-screen{height:100vh}.md\:max-h-\[calc\(100vh-6rem\)\]{max-height:calc(100vh - 6rem)}.md\:w-4{width:calc(var(--spacing)*4)}.md\:max-w-none{max-width:none}.md\:translate-x-0{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-col{flex-direction:column}.md\:gap-1{gap:calc(var(--spacing)*1)}.md\:overflow-hidden{overflow:hidden}.md\:overflow-y-auto{overflow-y:auto}.md\:p-2{padding:calc(var(--spacing)*2)}.md\:p-5{padding:calc(var(--spacing)*5)}.md\:p-6{padding:calc(var(--spacing)*6)}.md\:px-6{padding-inline:calc(var(--spacing)*6)}.md\:pt-6{padding-top:calc(var(--spacing)*6)}.md\:pb-4{padding-bottom:calc(var(--spacing)*4)}.md\:pb-10{padding-bottom:calc(var(--spacing)*10)}.md\:pl-4{padding-left:calc(var(--spacing)*4)}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media(min-width:64rem){.lg\:flex{display:flex}.lg\:w-64{width:calc(var(--spacing)*64)}.lg\:w-auto{width:auto}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:p-8{padding:calc(var(--spacing)*8)}}.dark\:border-l-blue-500:is(.dark *,html.dark *){border-left-color:var(--color-blue-500)}.dark\:border-l-green-500:is(.dark *,html.dark *){border-left-color:var(--color-green-500)}.dark\:bg-blue-900\/30:is(.dark *,html.dark *){background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-900\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark\:bg-blue-950\/30:is(.dark *,html.dark *){background-color:#1624564d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-blue-950)30%,transparent)}}.dark\:bg-cyan-950\/30:is(.dark *,html.dark *){background-color:#0533454d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-cyan-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-cyan-950)30%,transparent)}}.dark\:bg-destructive\/20:is(.dark *,html.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/20:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.dark\:bg-gray-950\/30:is(.dark *,html.dark *){background-color:#0307124d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-gray-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-gray-950)30%,transparent)}}.dark\:bg-green-500\/20:is(.dark *,html.dark *){background-color:#00c75833}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-500\/20:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-green-500)20%,transparent)}}.dark\:bg-green-900\/30:is(.dark *,html.dark *){background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-900\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark\:bg-green-950\/30:is(.dark *,html.dark *){background-color:#032e154d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-green-950)30%,transparent)}}.dark\:bg-orange-900\/30:is(.dark *,html.dark *){background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-orange-900\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-orange-900)30%,transparent)}}.dark\:bg-orange-950\/30:is(.dark *,html.dark *){background-color:#4413064d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-orange-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-orange-950)30%,transparent)}}.dark\:bg-purple-950\/30:is(.dark *,html.dark *){background-color:#3c03664d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-purple-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-purple-950)30%,transparent)}}.dark\:bg-red-950\/30:is(.dark *,html.dark *){background-color:#4608094d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-red-950)30%,transparent)}}.dark\:bg-teal-950\/30:is(.dark *,html.dark *){background-color:#022f2e4d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-teal-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-teal-950)30%,transparent)}}.dark\:bg-yellow-950\/30:is(.dark *,html.dark *){background-color:#4320044d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-yellow-950\/30:is(.dark *,html.dark *){background-color:color-mix(in oklab,var(--color-yellow-950)30%,transparent)}}.dark\:text-blue-300:is(.dark *,html.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:is(.dark *,html.dark *){color:var(--color-blue-400)}.dark\:text-green-400:is(.dark *,html.dark *){color:var(--color-green-400)}.dark\:text-orange-400:is(.dark *,html.dark *){color:var(--color-orange-400)}.\[\&_\.cm-editor\]\:h-full .cm-editor,.\[\&_\.cm-gutters\]\:h-full .cm-gutters{height:100%}.\[\&_\.cm-scroller\]\:overflow-auto .cm-scroller{overflow:auto}.\[\&_\.hljs-attr\]\:text-amber-300 .hljs-attr{color:var(--color-amber-300)}.\[\&_\.hljs-attr\]\:text-amber-700 .hljs-attr{color:var(--color-amber-700)}.\[\&_\.hljs-built_in\]\:text-purple-700 .hljs-built in{color:var(--color-purple-700)}.\[\&_\.hljs-built_in\]\:text-violet-300 .hljs-built in{color:var(--color-violet-300)}.\[\&_\.hljs-comment\]\:text-zinc-500 .hljs-comment{color:var(--color-zinc-500)}.\[\&_\.hljs-keyword\]\:text-blue-600 .hljs-keyword{color:var(--color-blue-600)}.\[\&_\.hljs-keyword\]\:text-sky-300 .hljs-keyword{color:var(--color-sky-300)}.\[\&_\.hljs-literal\]\:text-blue-600 .hljs-literal{color:var(--color-blue-600)}.\[\&_\.hljs-literal\]\:text-sky-300 .hljs-literal{color:var(--color-sky-300)}.\[\&_\.hljs-name\]\:text-emerald-300 .hljs-name{color:var(--color-emerald-300)}.\[\&_\.hljs-name\]\:text-green-700 .hljs-name{color:var(--color-green-700)}.\[\&_\.hljs-number\]\:text-fuchsia-300 .hljs-number{color:var(--color-fuchsia-300)}.\[\&_\.hljs-number\]\:text-purple-700 .hljs-number{color:var(--color-purple-700)}.\[\&_\.hljs-quote\]\:text-zinc-500 .hljs-quote{color:var(--color-zinc-500)}.\[\&_\.hljs-section\]\:text-emerald-300 .hljs-section{color:var(--color-emerald-300)}.\[\&_\.hljs-section\]\:text-green-700 .hljs-section{color:var(--color-green-700)}.\[\&_\.hljs-selector-tag\]\:text-blue-600 .hljs-selector-tag{color:var(--color-blue-600)}.\[\&_\.hljs-selector-tag\]\:text-sky-300 .hljs-selector-tag{color:var(--color-sky-300)}.\[\&_\.hljs-string\]\:text-amber-300 .hljs-string{color:var(--color-amber-300)}.\[\&_\.hljs-string\]\:text-amber-700 .hljs-string{color:var(--color-amber-700)}.\[\&_\.hljs-template-tag\]\:text-amber-300 .hljs-template-tag{color:var(--color-amber-300)}.\[\&_\.hljs-template-tag\]\:text-amber-700 .hljs-template-tag{color:var(--color-amber-700)}.\[\&_\.hljs-title\]\:text-emerald-300 .hljs-title{color:var(--color-emerald-300)}.\[\&_\.hljs-title\]\:text-green-700 .hljs-title{color:var(--color-green-700)}.\[\&_\.hljs-type\]\:text-purple-700 .hljs-type{color:var(--color-purple-700)}.\[\&_\.hljs-type\]\:text-violet-300 .hljs-type{color:var(--color-violet-300)}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing)*0)}.\[\&\>span\]\:line-clamp-1>span{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}[data-sonner-toaster]{z-index:100000!important}:root,:root[data-color-scheme=default]{--background:#fff;--foreground:#34322d;--card:#fff;--card-foreground:#34322d;--popover:#fff;--popover-foreground:#34322d;--primary:#34322d;--primary-foreground:#fff;--secondary:#f3f3f3;--secondary-foreground:#34322d;--muted:#f3f3f3;--muted-foreground:#858481;--accent:#f3f3f3;--accent-foreground:#34322d;--destructive:oklch(57.87% .15 34.8321);--destructive-foreground:oklch(97.96% .0101 32.5158);--border:#0000001f;--input:#0000001f;--ring:#34322d;--chart-1:#34322d;--chart-2:#5c5a56;--chart-3:#858481;--chart-4:#adacaa;--chart-5:#d6d5d4;--sidebar:#fbfbfb;--sidebar-foreground:#34322d;--sidebar-primary:#34322d;--sidebar-primary-foreground:#fff;--sidebar-accent:#f3f3f3;--sidebar-accent-foreground:#34322d;--sidebar-border:#00000014;--sidebar-ring:#34322d;--glow-1:#34322d1a;--glow-2:#8584811a;--radius:.5rem;--shadow-x:0;--shadow-y:1px;--shadow-blur:3px;--shadow-spread:0px;--shadow-opacity:.1;--shadow-color:#000;--shadow-2xs:0 1px 3px 0px #0000000d;--shadow-xs:0 1px 3px 0px #0000000d;--shadow-sm:0 1px 3px 0px #0000001a,0 1px 2px -1px #0000001a;--shadow:0 1px 3px 0px #0000001a,0 1px 2px -1px #0000001a;--shadow-md:0 1px 3px 0px #0000001a,0 2px 4px -1px #0000001a;--shadow-lg:0 1px 3px 0px #0000001a,0 4px 6px -1px #0000001a;--shadow-xl:0 1px 3px 0px #0000001a,0 8px 10px -1px #0000001a;--shadow-2xl:0 1px 3px 0px #00000040;--tracking-normal:0em;--spacing:.25rem}:root[data-color-scheme=green]{--background:oklch(93.31% .0081 98.8844);--foreground:oklch(35.14% .025 84.4589);--card:oklch(98.91% .0017 325.59);--card-foreground:oklch(35.14% .025 84.4589);--popover:oklch(97.96% .0057 84.5661);--popover-foreground:oklch(35.14% .025 84.4589);--primary:oklch(20% 0 0);--primary-foreground:oklch(98% 0 0);--secondary:oklch(92.04% .0149 98.297);--secondary-foreground:oklch(40.06% .0305 86.8158);--muted:oklch(93.99% .0124 91.5213);--muted-foreground:oklch(51.92% .0198 84.5869);--accent:oklch(68% .1103 148.083);--accent-foreground:oklch(97.92% .0091 150.693);--destructive:oklch(57.87% .15 34.8321);--destructive-foreground:oklch(97.96% .0101 32.5158);--border:oklch(87.95% .0142 88.6926);--input:oklch(87.95% .0142 88.6926);--ring:oklch(20% 0 0);--chart-1:oklch(68.09% .1201 145.043);--chart-2:oklch(61.95% .11 147.856);--chart-3:oklch(56.05% .1006 149.757);--chart-4:oklch(50.06% .0903 151.802);--chart-5:oklch(43.88% .0792 155.555);--sidebar:oklch(98.91% .0017 325.59);--sidebar-foreground:oklch(35.14% .025 84.4589);--sidebar-primary:oklch(88.16% .0425 82.469);--sidebar-primary-foreground:oklch(0% 0 0);--sidebar-accent:oklch(96.89% .0079 73.7443);--sidebar-accent-foreground:oklch(97.92% .0091 150.693);--sidebar-border:oklch(87.95% .0142 88.6926);--sidebar-ring:oklch(62.05% .1199 144.861);--glow-1:oklch(50% 0 0);--glow-2:oklch(70% 0 0)}:root[data-color-scheme=blue]{--background:oklch(98.46% .0017 247.84);--foreground:oklch(20.8% .034 264.665);--card:oklch(100% 0 0);--card-foreground:oklch(20.8% .034 264.665);--popover:oklch(100% 0 0);--popover-foreground:oklch(20.8% .034 264.665);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(93.2% .032 255.585);--secondary-foreground:oklch(20.8% .034 264.665);--muted:oklch(96% .002 247.84);--muted-foreground:oklch(55.2% .014 255.6);--accent:oklch(93.2% .032 255.585);--accent-foreground:oklch(48.8% .243 264.376);--destructive:oklch(57.87% .15 34.8321);--destructive-foreground:oklch(97.96% .0101 32.5158);--border:oklch(89.8% .005 255.6);--input:oklch(89.8% .005 255.6);--ring:oklch(54.6% .245 262.881);--chart-1:oklch(54.6% .245 262.881);--chart-2:oklch(59.6% .2 262.881);--chart-3:oklch(64.6% .15 262.881);--chart-4:oklch(69.6% .1 262.881);--chart-5:oklch(74.6% .05 262.881);--sidebar:oklch(98.46% .0017 247.84);--sidebar-foreground:oklch(20.8% .034 264.665);--sidebar-primary:oklch(54.6% .245 262.881);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(93.2% .032 255.585);--sidebar-accent-foreground:oklch(48.8% .243 264.376);--sidebar-border:oklch(89.8% .005 255.6);--sidebar-ring:oklch(54.6% .245 262.881);--glow-1:oklch(54.6% .245 262.881);--glow-2:oklch(60% .2 280)}:root[data-color-scheme=sky-blue]{--background:oklch(95% .012 220);--foreground:oklch(30% .03 230);--card:oklch(98.5% .008 220);--card-foreground:oklch(30% .03 230);--popover:oklch(98% .01 220);--popover-foreground:oklch(30% .03 230);--primary:oklch(72% .08 220);--primary-foreground:oklch(98% .01 220);--secondary:oklch(92% .02 220);--secondary-foreground:oklch(35% .035 230);--muted:oklch(94% .015 220);--muted-foreground:oklch(50% .025 230);--accent:oklch(75% .09 210);--accent-foreground:oklch(98% .01 220);--border:oklch(86% .02 220);--input:oklch(86% .02 220);--ring:oklch(72% .08 220);--chart-1:oklch(75% .09 220);--chart-2:oklch(68% .08 225);--chart-3:oklch(61% .07 230);--chart-4:oklch(54% .06 235);--chart-5:oklch(47% .05 240);--sidebar:oklch(98.5% .008 220);--sidebar-foreground:oklch(30% .03 230);--sidebar-primary:oklch(85% .05 220);--sidebar-primary-foreground:oklch(15% .02 230);--sidebar-accent:oklch(96% .012 220);--sidebar-accent-foreground:oklch(98% .01 220);--sidebar-border:oklch(86% .02 220);--sidebar-ring:oklch(72% .08 220);--glow-1:oklch(72% .08 220);--glow-2:oklch(75% .09 200)}:root[data-color-scheme=purple]{--background:oklch(95% .015 290);--foreground:oklch(30% .035 280);--card:oklch(98.5% .01 290);--card-foreground:oklch(30% .035 280);--popover:oklch(98% .012 290);--popover-foreground:oklch(30% .035 280);--primary:oklch(65% .15 280);--primary-foreground:oklch(98% .01 290);--secondary:oklch(92% .025 290);--secondary-foreground:oklch(35% .04 280);--muted:oklch(94% .018 290);--muted-foreground:oklch(50% .03 280);--accent:oklch(70% .14 275);--accent-foreground:oklch(98% .01 290);--border:oklch(86% .025 290);--input:oklch(86% .025 290);--ring:oklch(65% .15 280);--chart-1:oklch(70% .14 280);--chart-2:oklch(63% .13 285);--chart-3:oklch(56% .12 290);--chart-4:oklch(49% .11 295);--chart-5:oklch(42% .1 300);--sidebar:oklch(98.5% .01 290);--sidebar-foreground:oklch(30% .035 280);--sidebar-primary:oklch(85% .06 290);--sidebar-primary-foreground:oklch(15% .025 280);--sidebar-accent:oklch(96% .015 290);--sidebar-accent-foreground:oklch(98% .01 290);--sidebar-border:oklch(86% .025 290);--sidebar-ring:oklch(65% .15 280);--glow-1:oklch(65% .15 280);--glow-2:oklch(70% .12 300)}:root[data-color-scheme=orange]{--background:oklch(95% .015 60);--foreground:oklch(30% .035 50);--card:oklch(98.5% .01 60);--card-foreground:oklch(30% .035 50);--popover:oklch(98% .012 60);--popover-foreground:oklch(30% .035 50);--primary:oklch(70% .15 50);--primary-foreground:oklch(98% .01 60);--secondary:oklch(92% .025 60);--secondary-foreground:oklch(35% .04 50);--muted:oklch(94% .018 60);--muted-foreground:oklch(50% .03 50);--accent:oklch(75% .14 45);--accent-foreground:oklch(98% .01 60);--border:oklch(86% .025 60);--input:oklch(86% .025 60);--ring:oklch(70% .15 50);--chart-1:oklch(75% .14 50);--chart-2:oklch(68% .13 55);--chart-3:oklch(61% .12 60);--chart-4:oklch(54% .11 65);--chart-5:oklch(47% .1 70);--sidebar:oklch(98.5% .01 60);--sidebar-foreground:oklch(30% .035 50);--sidebar-primary:oklch(85% .06 60);--sidebar-primary-foreground:oklch(15% .025 50);--sidebar-accent:oklch(96% .015 60);--sidebar-accent-foreground:oklch(98% .01 60);--sidebar-border:oklch(86% .025 60);--sidebar-ring:oklch(70% .15 50);--glow-1:oklch(70% .15 50);--glow-2:oklch(75% .12 35)}:root[data-color-scheme=rose]{--background:oklch(95% .015 10);--foreground:oklch(30% .035 5);--card:oklch(98.5% .01 10);--card-foreground:oklch(30% .035 5);--popover:oklch(98% .012 10);--popover-foreground:oklch(30% .035 5);--primary:oklch(65% .15 10);--primary-foreground:oklch(98% .01 10);--secondary:oklch(92% .025 10);--secondary-foreground:oklch(35% .04 5);--muted:oklch(94% .018 10);--muted-foreground:oklch(50% .03 5);--accent:oklch(70% .14 5);--accent-foreground:oklch(98% .01 10);--border:oklch(86% .025 10);--input:oklch(86% .025 10);--ring:oklch(65% .15 10);--chart-1:oklch(70% .14 10);--chart-2:oklch(63% .13 15);--chart-3:oklch(56% .12 20);--chart-4:oklch(49% .11 25);--chart-5:oklch(42% .1 30);--sidebar:oklch(98.5% .01 10);--sidebar-foreground:oklch(30% .035 5);--sidebar-primary:oklch(85% .06 10);--sidebar-primary-foreground:oklch(15% .025 5);--sidebar-accent:oklch(96% .015 10);--sidebar-accent-foreground:oklch(98% .01 10);--sidebar-border:oklch(86% .025 10);--sidebar-ring:oklch(65% .15 10);--glow-1:oklch(65% .15 10);--glow-2:oklch(70% .12 350)}:root[data-color-scheme=teal]{--background:oklch(95% .015 175);--foreground:oklch(30% .035 180);--card:oklch(98.5% .01 175);--card-foreground:oklch(30% .035 180);--popover:oklch(98% .012 175);--popover-foreground:oklch(30% .035 180);--primary:oklch(70% .12 175);--primary-foreground:oklch(98% .01 175);--secondary:oklch(92% .025 175);--secondary-foreground:oklch(35% .04 180);--muted:oklch(94% .018 175);--muted-foreground:oklch(50% .03 180);--accent:oklch(75% .11 170);--accent-foreground:oklch(98% .01 175);--border:oklch(86% .025 175);--input:oklch(86% .025 175);--ring:oklch(70% .12 175);--chart-1:oklch(75% .11 175);--chart-2:oklch(68% .1 180);--chart-3:oklch(61% .09 185);--chart-4:oklch(54% .08 190);--chart-5:oklch(47% .07 195);--sidebar:oklch(98.5% .01 175);--sidebar-foreground:oklch(30% .035 180);--sidebar-primary:oklch(85% .06 175);--sidebar-primary-foreground:oklch(15% .025 180);--sidebar-accent:oklch(96% .015 175);--sidebar-accent-foreground:oklch(98% .01 175);--sidebar-border:oklch(86% .025 175);--sidebar-ring:oklch(70% .12 175);--glow-1:oklch(70% .12 175);--glow-2:oklch(75% .1 190)}html.dark[data-color-scheme=default],.dark[data-color-scheme=default]{--background:#1a1a1a;--foreground:#ededed;--card:#1a1a1a;--card-foreground:#ededed;--popover:#1a1a1a;--popover-foreground:#ededed;--primary:#ededed;--primary-foreground:#1a1a1a;--secondary:#2a2a2a;--secondary-foreground:#ededed;--muted:#2a2a2a;--muted-foreground:#a1a1a1;--accent:#2a2a2a;--accent-foreground:#ededed;--destructive:oklch(57.87% .15 34.8321);--destructive-foreground:oklch(94.96% .0101 32.5162);--border:#ffffff1f;--input:#ffffff1f;--ring:#ededed;--chart-1:#ededed;--chart-2:#c7c7c7;--chart-3:#a1a1a1;--chart-4:#7b7b7b;--chart-5:#555;--sidebar:#1f1f1f;--sidebar-foreground:#ededed;--sidebar-primary:#ededed;--sidebar-primary-foreground:#1a1a1a;--sidebar-accent:#2a2a2a;--sidebar-accent-foreground:#ededed;--sidebar-border:#ffffff14;--sidebar-ring:#ededed;--glow-1:#ededed1a;--glow-2:#a1a1a11a}html.dark,.dark,html.dark[data-color-scheme=green],.dark[data-color-scheme=green]{--background:oklch(28.09% .0264 153.404);--foreground:oklch(92% .0104 81.7947);--card:oklch(31.96% .0217 150.339);--card-foreground:oklch(92% .0104 81.7947);--popover:oklch(31.96% .0217 150.339);--popover-foreground:oklch(92% .0104 81.7947);--primary:oklch(69.9% .1292 144.873);--primary-foreground:oklch(22.14% .0312 149.621);--secondary:oklch(37.83% .02 147.987);--secondary-foreground:oklch(88.01% .0117 84.5809);--muted:oklch(34.03% .0192 151.125);--muted-foreground:oklch(68.01% .0151 88.7221);--accent:oklch(74.09% .1207 147.975);--accent-foreground:oklch(21.89% .0281 148.509);--destructive:oklch(57.87% .15 34.8321);--destructive-foreground:oklch(94.96% .0101 32.5162);--border:oklch(40.06% .021 145.17);--input:oklch(40.06% .021 145.17);--ring:oklch(69.9% .1292 144.873);--chart-1:oklch(76.03% .1309 144.994);--chart-2:oklch(70.08% .12 147.875);--chart-3:oklch(66.66% .1099 150);--chart-4:oklch(57.95% .0999 151.837);--chart-5:oklch(51.99% .0893 155.145);--sidebar:oklch(25.93% .0275 159.089);--sidebar-foreground:oklch(92% .0104 81.7947);--sidebar-primary:oklch(69.9% .1292 144.873);--sidebar-primary-foreground:oklch(22.14% .0312 149.621);--sidebar-accent:oklch(74.09% .1207 147.975);--sidebar-accent-foreground:oklch(21.89% .0281 148.509);--sidebar-border:oklch(40.06% .021 145.17);--sidebar-ring:oklch(69.9% .1292 144.873);--glow-1:oklch(40% .15 145);--glow-2:oklch(45% .12 160);--radius:.5rem;--shadow-x:0;--shadow-y:1px;--shadow-blur:3px;--shadow-spread:0px;--shadow-opacity:.1;--shadow-color:#000;--shadow-2xs:0 1px 3px 0px #0000000d;--shadow-xs:0 1px 3px 0px #0000000d;--shadow-sm:0 1px 3px 0px #0000001a,0 1px 2px -1px #0000001a;--shadow:0 1px 3px 0px #0000001a,0 1px 2px -1px #0000001a;--shadow-md:0 1px 3px 0px #0000001a,0 2px 4px -1px #0000001a;--shadow-lg:0 1px 3px 0px #0000001a,0 4px 6px -1px #0000001a;--shadow-xl:0 1px 3px 0px #0000001a,0 8px 10px -1px #0000001a;--shadow-2xl:0 1px 3px 0px #00000040}html.dark[data-color-scheme=blue],.dark[data-color-scheme=blue]{--background:oklch(18% .03 263);--foreground:oklch(92% .01 263);--card:oklch(20% .03 263);--card-foreground:oklch(92% .01 263);--popover:oklch(20% .03 263);--popover-foreground:oklch(92% .01 263);--primary:oklch(60% .2 263);--primary-foreground:oklch(18% .03 263);--secondary:oklch(25% .03 263);--secondary-foreground:oklch(90% .01 263);--muted:oklch(25% .03 263);--muted-foreground:oklch(70% .02 263);--accent:oklch(25% .03 263);--accent-foreground:oklch(90% .01 263);--destructive:oklch(57.87% .15 34.8321);--destructive-foreground:oklch(94.96% .0101 32.5162);--border:oklch(30% .03 263);--input:oklch(30% .03 263);--ring:oklch(60% .2 263);--chart-1:oklch(60% .2 263);--chart-2:oklch(55% .18 263);--chart-3:oklch(50% .16 263);--chart-4:oklch(45% .14 263);--chart-5:oklch(40% .12 263);--sidebar:oklch(18% .03 263);--sidebar-foreground:oklch(92% .01 263);--sidebar-primary:oklch(60% .2 263);--sidebar-primary-foreground:oklch(18% .03 263);--sidebar-accent:oklch(25% .03 263);--sidebar-accent-foreground:oklch(90% .01 263);--sidebar-border:oklch(30% .03 263);--sidebar-ring:oklch(60% .2 263);--glow-1:oklch(40% .2 263);--glow-2:oklch(45% .15 280)}html.dark[data-color-scheme=sky-blue],.dark[data-color-scheme=sky-blue]{--background:oklch(22% .03 220);--foreground:oklch(92% .015 220);--card:oklch(26% .025 220);--card-foreground:oklch(92% .015 220);--popover:oklch(26% .025 220);--popover-foreground:oklch(92% .015 220);--primary:oklch(72% .08 220);--primary-foreground:oklch(18% .02 220);--secondary:oklch(32% .03 220);--secondary-foreground:oklch(88% .015 220);--muted:oklch(28% .025 220);--muted-foreground:oklch(68% .02 220);--accent:oklch(75% .09 210);--accent-foreground:oklch(18% .02 220);--border:oklch(35% .03 220);--input:oklch(35% .03 220);--ring:oklch(72% .08 220);--chart-1:oklch(75% .09 220);--chart-2:oklch(68% .08 225);--chart-3:oklch(61% .07 230);--chart-4:oklch(54% .06 235);--chart-5:oklch(47% .05 240);--sidebar:oklch(20% .032 220);--sidebar-foreground:oklch(92% .015 220);--sidebar-primary:oklch(72% .08 220);--sidebar-primary-foreground:oklch(18% .02 220);--sidebar-accent:oklch(75% .09 210);--sidebar-accent-foreground:oklch(18% .02 220);--sidebar-border:oklch(35% .03 220);--sidebar-ring:oklch(72% .08 220);--glow-1:oklch(45% .1 220);--glow-2:oklch(50% .08 200)}html.dark[data-color-scheme=purple],.dark[data-color-scheme=purple]{--background:oklch(22% .035 280);--foreground:oklch(92% .018 290);--card:oklch(26% .03 280);--card-foreground:oklch(92% .018 290);--popover:oklch(26% .03 280);--popover-foreground:oklch(92% .018 290);--primary:oklch(65% .15 280);--primary-foreground:oklch(18% .025 280);--secondary:oklch(32% .035 280);--secondary-foreground:oklch(88% .018 290);--muted:oklch(28% .03 280);--muted-foreground:oklch(68% .025 290);--accent:oklch(70% .14 275);--accent-foreground:oklch(18% .025 280);--border:oklch(35% .035 280);--input:oklch(35% .035 280);--ring:oklch(65% .15 280);--chart-1:oklch(70% .14 280);--chart-2:oklch(63% .13 285);--chart-3:oklch(56% .12 290);--chart-4:oklch(49% .11 295);--chart-5:oklch(42% .1 300);--sidebar:oklch(20% .038 280);--sidebar-foreground:oklch(92% .018 290);--sidebar-primary:oklch(65% .15 280);--sidebar-primary-foreground:oklch(18% .025 280);--sidebar-accent:oklch(70% .14 275);--sidebar-accent-foreground:oklch(18% .025 280);--sidebar-border:oklch(35% .035 280);--sidebar-ring:oklch(65% .15 280);--glow-1:oklch(40% .18 280);--glow-2:oklch(45% .15 300)}html.dark[data-color-scheme=orange],.dark[data-color-scheme=orange]{--background:oklch(22% .035 50);--foreground:oklch(92% .018 60);--card:oklch(26% .03 50);--card-foreground:oklch(92% .018 60);--popover:oklch(26% .03 50);--popover-foreground:oklch(92% .018 60);--primary:oklch(70% .15 50);--primary-foreground:oklch(18% .025 50);--secondary:oklch(32% .035 50);--secondary-foreground:oklch(88% .018 60);--muted:oklch(28% .03 50);--muted-foreground:oklch(68% .025 60);--accent:oklch(75% .14 45);--accent-foreground:oklch(18% .025 50);--border:oklch(35% .035 50);--input:oklch(35% .035 50);--ring:oklch(70% .15 50);--chart-1:oklch(75% .14 50);--chart-2:oklch(68% .13 55);--chart-3:oklch(61% .12 60);--chart-4:oklch(54% .11 65);--chart-5:oklch(47% .1 70);--sidebar:oklch(20% .038 50);--sidebar-foreground:oklch(92% .018 60);--sidebar-primary:oklch(70% .15 50);--sidebar-primary-foreground:oklch(18% .025 50);--sidebar-accent:oklch(75% .14 45);--sidebar-accent-foreground:oklch(18% .025 50);--sidebar-border:oklch(35% .035 50);--sidebar-ring:oklch(70% .15 50);--glow-1:oklch(45% .18 50);--glow-2:oklch(50% .15 35)}html.dark[data-color-scheme=rose],.dark[data-color-scheme=rose]{--background:oklch(22% .035 5);--foreground:oklch(92% .018 10);--card:oklch(26% .03 5);--card-foreground:oklch(92% .018 10);--popover:oklch(26% .03 5);--popover-foreground:oklch(92% .018 10);--primary:oklch(65% .15 10);--primary-foreground:oklch(18% .025 5);--secondary:oklch(32% .035 5);--secondary-foreground:oklch(88% .018 10);--muted:oklch(28% .03 5);--muted-foreground:oklch(68% .025 10);--accent:oklch(70% .14 5);--accent-foreground:oklch(18% .025 5);--border:oklch(35% .035 5);--input:oklch(35% .035 5);--ring:oklch(65% .15 10);--chart-1:oklch(70% .14 10);--chart-2:oklch(63% .13 15);--chart-3:oklch(56% .12 20);--chart-4:oklch(49% .11 25);--chart-5:oklch(42% .1 30);--sidebar:oklch(20% .038 5);--sidebar-foreground:oklch(92% .018 10);--sidebar-primary:oklch(65% .15 10);--sidebar-primary-foreground:oklch(18% .025 5);--sidebar-accent:oklch(70% .14 5);--sidebar-accent-foreground:oklch(18% .025 5);--sidebar-border:oklch(35% .035 5);--sidebar-ring:oklch(65% .15 10);--glow-1:oklch(40% .18 10);--glow-2:oklch(45% .15 350)}html.dark[data-color-scheme=teal],.dark[data-color-scheme=teal]{--background:oklch(22% .035 175);--foreground:oklch(92% .018 175);--card:oklch(26% .03 175);--card-foreground:oklch(92% .018 175);--popover:oklch(26% .03 175);--popover-foreground:oklch(92% .018 175);--primary:oklch(70% .12 175);--primary-foreground:oklch(18% .025 175);--secondary:oklch(32% .035 175);--secondary-foreground:oklch(88% .018 175);--muted:oklch(28% .03 175);--muted-foreground:oklch(68% .025 175);--accent:oklch(75% .11 170);--accent-foreground:oklch(18% .025 175);--border:oklch(35% .035 175);--input:oklch(35% .035 175);--ring:oklch(70% .12 175);--chart-1:oklch(75% .11 175);--chart-2:oklch(68% .1 180);--chart-3:oklch(61% .09 185);--chart-4:oklch(54% .08 190);--chart-5:oklch(47% .07 195);--sidebar:oklch(20% .038 175);--sidebar-foreground:oklch(92% .018 175);--sidebar-primary:oklch(70% .12 175);--sidebar-primary-foreground:oklch(18% .025 175);--sidebar-accent:oklch(75% .11 170);--sidebar-accent-foreground:oklch(18% .025 175);--sidebar-border:oklch(35% .035 175);--sidebar-ring:oklch(70% .12 175);--glow-1:oklch(45% .15 175);--glow-2:oklch(50% .12 190)}@media(prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;transition-delay:0s!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}.auth-page-container{--default-bg: #ffffff;--default-text-primary: rgb(52, 50, 45);--default-text-secondary: rgb(133, 132, 129);--default-border: rgba(0, 0, 0, .12);--default-input-bg: #ffffff;--default-button-bg: rgb(137, 137, 136);--default-button-text: #ffffff;--default-accent: #000000;position:relative;min-height:100vh;min-height:100dvh;width:100%;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:var(--default-bg);color:var(--default-text-primary);overflow:hidden;background-image:radial-gradient(circle,#d1d1d1 1px,transparent 1px);background-size:20px 20px;background-position:center}@media(pointer:coarse){.auth-page-container{overflow-x:hidden;overflow-y:auto}}.auth-background-layer{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0}body:has(.auth-page-container):before,body:has(.auth-page-container):after{content:none!important;display:none!important}.auth-page-container.dark{--default-bg: #1a1a1a;--default-text-primary: #ededed;--default-text-secondary: #a1a1a1;background-image:radial-gradient(circle,#333 1px,transparent 1px)}.auth-card{position:relative;z-index:50;width:100%;max-width:360px;padding:0;margin:0 auto}.auth-logo-wrapper{display:flex;flex-direction:column;align-items:center;text-align:center}.auth-logo-box{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#fff;border:1px solid rgba(0,0,0,.08);box-shadow:0 4px 12px #0000000d;margin-bottom:1.5rem;color:var(--default-text-primary)}.auth-page-container.dark .auth-logo-box{background:#2a2a2a;border-color:#ffffff1a;color:#fff;box-shadow:0 4px 12px #0003}.auth-title{font-size:32px;font-weight:600;color:var(--default-text-primary);margin-bottom:.5rem;text-align:center;letter-spacing:-.02em}.auth-subtitle{font-size:14px;color:var(--default-text-secondary);text-align:center;margin-bottom:2rem}.auth-tabs-container{margin-bottom:1.5rem;display:flex;justify-content:center}.auth-tabs{display:flex;background:transparent;padding:0;border:none;gap:1rem}.auth-tab{padding:.5rem 1rem;font-size:14px;font-weight:500;color:var(--default-text-secondary);background:transparent;border:none;cursor:pointer;transition:color .2s}.auth-tab.active{color:var(--default-text-primary);background:transparent;box-shadow:none;border-bottom:2px solid var(--default-text-primary);border-radius:0}.auth-input{width:100%!important;height:40px!important;padding:4px 12px!important;font-size:14px!important;border-radius:8px!important;border:none!important;box-shadow:0 0 0 1px #0000001f!important;background:#fffc!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:var(--default-text-primary)!important;margin-bottom:1rem!important;transition:box-shadow .2s ease!important}.auth-page-container.dark .auth-input{background:#2a2a2acc!important;box-shadow:0 0 0 1px #ffffff1f!important;color:#fff!important}.auth-input:focus{outline:none!important;box-shadow:0 0 0 2px #0003!important}.auth-input:-webkit-autofill,.auth-input:-webkit-autofill:hover,.auth-input:-webkit-autofill:focus,.auth-input:-webkit-autofill:active{-webkit-text-fill-color:#000000!important}.auth-page-container.dark .auth-input:-webkit-autofill,.auth-page-container.dark .auth-input:-webkit-autofill:hover,.auth-page-container.dark .auth-input:-webkit-autofill:focus,.auth-page-container.dark .auth-input:-webkit-autofill:active{-webkit-text-fill-color:#a1a1a1!important;-webkit-box-shadow:0 0 0 30px rgba(42,42,42,.8) inset!important}.auth-button-primary{width:100%;height:40px;display:flex;align-items:center;justify-content:center;gap:.5rem;background-color:var(--default-text-primary);color:#fff;font-size:14px;font-weight:500;border-radius:8px;border:none;cursor:pointer;transition:opacity .2s;margin-top:1rem}.auth-page-container.dark .auth-button-primary{background-color:#8a8a8a;color:#000}.auth-button-primary:hover{opacity:.9}.auth-button-primary:disabled{background-color:#898988;cursor:not-allowed}.auth-footer-wrapper{margin-top:auto;padding-top:2rem;padding-bottom:2rem;width:100%;display:flex;justify-content:center}.auth-brand-footer{text-align:center;font-size:12px;color:var(--default-text-secondary);opacity:.7;max-width:400px;line-height:1.5;white-space:pre-line}.auth-brand-footer a{text-decoration:none;color:inherit;transition:color .2s}.auth-brand-footer a:hover{color:var(--default-text-primary)}.auth-floating-actions{position:absolute;top:1.5rem;right:1.5rem;z-index:100;display:flex;gap:.5rem}.auth-floating-switcher{width:32px!important;height:32px!important;min-width:32px!important;min-height:32px!important;padding:0!important;border-radius:50%!important;border:1px solid rgba(0,0,0,.08)!important;background:#fff;box-shadow:0 2px 4px #0000000d;color:var(--default-text-secondary);display:flex!important;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;aspect-ratio:1 / 1}.auth-floating-switcher:hover{background:#fcfcfc;border-color:#00000026;color:var(--default-text-primary);transform:translateY(-1px);box-shadow:0 4px 6px #00000014}.auth-page-container.dark .auth-floating-switcher{background:#2a2a2a;border-color:#ffffff1a;color:#a1a1a1}.auth-page-container.dark .auth-floating-switcher:hover{background:#333;color:#fff} ================================================ FILE: frontend/assets/font-loader-CIrh3KnA.js ================================================ function _mergeNamespaces(A,t){for(var e=0;ea[t]})}}return Object.freeze(Object.defineProperty(A,Symbol.toStringTag,{value:"Module"}))}function getDefaultExportFromCjs(A){return A&&A.__esModule&&Object.prototype.hasOwnProperty.call(A,"default")?A.default:A}!function(){const A=document.createElement("link").relList;if(!(A&&A.supports&&A.supports("modulepreload"))){for(const A of document.querySelectorAll('link[rel="modulepreload"]'))t(A);new MutationObserver(A=>{for(const e of A)if("childList"===e.type)for(const A of e.addedNodes)"LINK"===A.tagName&&"modulepreload"===A.rel&&t(A)}).observe(document,{childList:!0,subtree:!0})}function t(A){if(A.ep)return;A.ep=!0;const t=function(A){const t={};return A.integrity&&(t.integrity=A.integrity),A.referrerPolicy&&(t.referrerPolicy=A.referrerPolicy),"use-credentials"===A.crossOrigin?t.credentials="include":"anonymous"===A.crossOrigin?t.credentials="omit":t.credentials="same-origin",t}(A);fetch(A.href,t)}}();var jsxRuntime={exports:{}},reactJsxRuntime_production={},hasRequiredReactJsxRuntime_production,hasRequiredJsxRuntime;function requireReactJsxRuntime_production(){if(hasRequiredReactJsxRuntime_production)return reactJsxRuntime_production;hasRequiredReactJsxRuntime_production=1;var A=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function e(t,e,a){var r=null;if(void 0!==a&&(r=""+a),void 0!==e.key&&(r=""+e.key),"key"in e)for(var n in a={},e)"key"!==n&&(a[n]=e[n]);else a=e;return e=a.ref,{$$typeof:A,type:t,key:r,ref:void 0!==e?e:null,props:a}}return reactJsxRuntime_production.Fragment=t,reactJsxRuntime_production.jsx=e,reactJsxRuntime_production.jsxs=e,reactJsxRuntime_production}function requireJsxRuntime(){return hasRequiredJsxRuntime||(hasRequiredJsxRuntime=1,jsxRuntime.exports=requireReactJsxRuntime_production()),jsxRuntime.exports}var jsxRuntimeExports=requireJsxRuntime();const scriptRel="modulepreload",assetsURL=function(A){return"/"+A},seen={},__vitePreload=function(A,t,e){let a=Promise.resolve();if(t&&t.length>0){let A=function(A){return Promise.all(A.map(A=>Promise.resolve(A).then(A=>({status:"fulfilled",value:A}),A=>({status:"rejected",reason:A}))))};document.getElementsByTagName("link");const e=document.querySelector("meta[property=csp-nonce]"),r=(null==e?void 0:e.nonce)||(null==e?void 0:e.getAttribute("nonce"));a=A(t.map(A=>{if((A=assetsURL(A))in seen)return;seen[A]=!0;const t=A.endsWith(".css"),e=t?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${A}"]${e}`))return;const a=document.createElement("link");return a.rel=t?"stylesheet":scriptRel,t||(a.as="script"),a.crossOrigin="",a.href=A,r&&a.setAttribute("nonce",r),document.head.appendChild(a),t?new Promise((t,e)=>{a.addEventListener("load",t),a.addEventListener("error",()=>e(new Error(`Unable to preload CSS for ${A}`)))}):void 0}))}function r(A){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=A,window.dispatchEvent(t),!t.defaultPrevented)throw A}return a.then(t=>{for(const A of t||[])"rejected"===A.status&&r(A.reason);return A().catch(r)})};var react={exports:{}},react_production={},hasRequiredReact_production,hasRequiredReact;function requireReact_production(){if(hasRequiredReact_production)return react_production;hasRequiredReact_production=1;var A=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),e=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),n=Symbol.for("react.consumer"),l=Symbol.for("react.context"),i=Symbol.for("react.forward_ref"),o=Symbol.for("react.suspense"),s=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),c=Symbol.for("react.activity"),u=Symbol.iterator;var d={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,S={};function f(A,t,e){this.props=A,this.context=t,this.refs=S,this.updater=e||d}function m(){}function L(A,t,e){this.props=A,this.context=t,this.refs=S,this.updater=e||d}f.prototype.isReactComponent={},f.prototype.setState=function(A,t){if("object"!=typeof A&&"function"!=typeof A&&null!=A)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,A,t,"setState")},f.prototype.forceUpdate=function(A){this.updater.enqueueForceUpdate(this,A,"forceUpdate")},m.prototype=f.prototype;var W=L.prototype=new m;W.constructor=L,h(W,f.prototype),W.isPureReactComponent=!0;var g=Array.isArray;function y(){}var v={H:null,A:null,T:null,S:null},b=Object.prototype.hasOwnProperty;function x(t,e,a){var r=a.ref;return{$$typeof:A,type:t,key:e,ref:void 0!==r?r:null,props:a}}function E(t){return"object"==typeof t&&null!==t&&t.$$typeof===A}var k=/\/+/g;function w(A,t){return"object"==typeof A&&null!==A&&null!=A.key?(e=""+A.key,a={"=":"=0",":":"=2"},"$"+e.replace(/[=:]/g,function(A){return a[A]})):t.toString(36);var e,a}function C(e,a,r,n,l){var i=typeof e;"undefined"!==i&&"boolean"!==i||(e=null);var o,s,c=!1;if(null===e)c=!0;else switch(i){case"bigint":case"string":case"number":c=!0;break;case"object":switch(e.$$typeof){case A:case t:c=!0;break;case p:return C((c=e._init)(e._payload),a,r,n,l)}}if(c)return l=l(e),c=""===n?"."+w(e,0):n,g(l)?(r="",null!=c&&(r=c.replace(k,"$&/")+"/"),C(l,a,r,"",function(A){return A})):null!=l&&(E(l)&&(o=l,s=r+(null==l.key||e&&e.key===l.key?"":(""+l.key).replace(k,"$&/")+"/")+c,l=x(o.type,s,o.props)),a.push(l)),1;c=0;var d,h=""===n?".":n+":";if(g(e))for(var S=0;S{var r,n,l,i;const o=[e,{code:t,...a||{}}];if(null==(n=null==(r=null==A?void 0:A.services)?void 0:r.logger)?void 0:n.forward)return A.services.logger.forward(o,"warn","react-i18next::",!0);isString$1(o[0])&&(o[0]=`react-i18next:: ${o[0]}`),(null==(i=null==(l=null==A?void 0:A.services)?void 0:l.logger)?void 0:i.warn)?A.services.logger.warn(...o):null==console||console.warn},alreadyWarned={},warnOnce=(A,t,e,a)=>{isString$1(e)&&alreadyWarned[e]||(isString$1(e)&&(alreadyWarned[e]=new Date),warn(A,t,e,a))},loadedClb=(A,t)=>()=>{if(A.isInitialized)t();else{const e=()=>{setTimeout(()=>{A.off("initialized",e)},0),t()};A.on("initialized",e)}},loadNamespaces=(A,t,e)=>{A.loadNamespaces(t,loadedClb(A,e))},loadLanguages=(A,t,e,a)=>{if(isString$1(e)&&(e=[e]),A.options.preload&&A.options.preload.indexOf(t)>-1)return loadNamespaces(A,e,a);e.forEach(t=>{A.options.ns.indexOf(t)<0&&A.options.ns.push(t)}),A.loadLanguages(t,loadedClb(A,a))},hasLoadedNamespace=(A,t,e={})=>t.languages&&t.languages.length?t.hasLoadedNamespace(A,{lng:e.lng,precheck:(t,a)=>{if(e.bindI18n&&e.bindI18n.indexOf("languageChanging")>-1&&t.services.backendConnector.backend&&t.isLanguageChangingTo&&!a(t.isLanguageChangingTo,A))return!1}}):(warnOnce(t,"NO_LANGUAGES","i18n.languages were undefined or empty",{languages:t.languages}),!0),isString$1=A=>"string"==typeof A,isObject=A=>"object"==typeof A&&null!==A,matchHtmlEntity=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,htmlEntities={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},unescapeHtmlEntity=A=>htmlEntities[A],unescape=A=>A.replace(matchHtmlEntity,unescapeHtmlEntity);let defaultOptions$1={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:unescape};const setDefaults=(A={})=>{defaultOptions$1={...defaultOptions$1,...A}},getDefaults=()=>defaultOptions$1;let i18nInstance;const setI18n=A=>{i18nInstance=A},getI18n=()=>i18nInstance,initReactI18next={type:"3rdParty",init(A){setDefaults(A.options.react),setI18n(A)}},I18nContext=reactExports.createContext();class ReportNamespaces{constructor(){this.usedNamespaces={}}addUsedNamespaces(A){A.forEach(A=>{this.usedNamespaces[A]||(this.usedNamespaces[A]=!0)})}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const usePrevious=(A,t)=>{const e=reactExports.useRef();return reactExports.useEffect(()=>{e.current=A},[A,t]),e.current},alwaysNewT=(A,t,e,a)=>A.getFixedT(t,e,a),useMemoizedT=(A,t,e,a)=>reactExports.useCallback(alwaysNewT(A,t,e,a),[A,t,e,a]),useTranslation=(A,t={})=>{var e,a,r,n;const{i18n:l}=t,{i18n:i,defaultNS:o}=reactExports.useContext(I18nContext)||{},s=l||i||getI18n();if(s&&!s.reportNamespaces&&(s.reportNamespaces=new ReportNamespaces),!s){warnOnce(s,"NO_I18NEXT_INSTANCE","useTranslation: You will need to pass in an i18next instance by using initReactI18next");const A=(A,t)=>isString$1(t)?t:isObject(t)&&isString$1(t.defaultValue)?t.defaultValue:Array.isArray(A)?A[A.length-1]:A,t=[A,{},!1];return t.t=A,t.i18n={},t.ready=!1,t}(null==(e=s.options.react)?void 0:e.wait)&&warnOnce(s,"DEPRECATED_OPTION","useTranslation: It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const p={...getDefaults(),...s.options.react,...t},{useSuspense:c,keyPrefix:u}=p;let d=o||(null==(a=s.options)?void 0:a.defaultNS);d=isString$1(d)?[d]:d||["translation"],null==(n=(r=s.reportNamespaces).addUsedNamespaces)||n.call(r,d);const h=(s.isInitialized||s.initializedStoreOnce)&&d.every(A=>hasLoadedNamespace(A,s,p)),S=useMemoizedT(s,t.lng||null,"fallback"===p.nsMode?d:d[0],u),f=()=>S,m=()=>alwaysNewT(s,t.lng||null,"fallback"===p.nsMode?d:d[0],u),[L,W]=reactExports.useState(f);let g=d.join();t.lng&&(g=`${t.lng}${g}`);const y=usePrevious(g),v=reactExports.useRef(!0);reactExports.useEffect(()=>{const{bindI18n:A,bindI18nStore:e}=p;v.current=!0,h||c||(t.lng?loadLanguages(s,t.lng,d,()=>{v.current&&W(m)}):loadNamespaces(s,d,()=>{v.current&&W(m)})),h&&y&&y!==g&&v.current&&W(m);const a=()=>{v.current&&W(m)};return A&&(null==s||s.on(A,a)),e&&(null==s||s.store.on(e,a)),()=>{v.current=!1,s&&A&&(null==A||A.split(" ").forEach(A=>s.off(A,a))),e&&s&&e.split(" ").forEach(A=>s.store.off(A,a))}},[s,g]),reactExports.useEffect(()=>{v.current&&h&&W(f)},[s,u,h]);const b=[L,s,h];if(b.t=L,b.i18n=s,b.ready=h,h)return b;if(!h&&!c)return b;throw new Promise(A=>{t.lng?loadLanguages(s,t.lng,d,()=>A()):loadNamespaces(s,d,()=>A())})},changeLang=async(A,t="lang")=>{localStorage.setItem(t,A),await ensureResourceLoaded(A),await instance.changeLanguage(A)};function getBrowserLang(A){if(A)return localStorage.getItem(A)||navigator.language||"en";const t="undefined"!=typeof window&&(window.location.pathname.toLowerCase().includes("share")||window.location.search.toLowerCase().includes("share")),e=t?"share-lang":"lang";let a=localStorage.getItem(e);!a&&t&&(a=localStorage.getItem("lang")),a||(a=navigator.language);const r=null==a?void 0:a.toString();return r?r.includes(",")?r.split(",")[0]:r:"en"}const utils=Object.freeze(Object.defineProperty({__proto__:null,changeLang:changeLang,getBrowserLang:getBrowserLang},Symbol.toStringTag,{value:"Module"})),isString=A=>"string"==typeof A,defer=()=>{let A,t;const e=new Promise((e,a)=>{A=e,t=a});return e.resolve=A,e.reject=t,e},makeString=A=>null==A?"":""+A,copy=(A,t,e)=>{A.forEach(A=>{t[A]&&(e[A]=t[A])})},lastOfPathSeparatorRegExp=/###/g,cleanKey=A=>A&&A.indexOf("###")>-1?A.replace(lastOfPathSeparatorRegExp,"."):A,canNotTraverseDeeper=A=>!A||isString(A),getLastOfPath=(A,t,e)=>{const a=isString(t)?t.split("."):t;let r=0;for(;r{const{obj:a,k:r}=getLastOfPath(A,t,Object);if(void 0!==a||1===t.length)return void(a[r]=e);let n=t[t.length-1],l=t.slice(0,t.length-1),i=getLastOfPath(A,l,Object);for(;void 0===i.obj&&l.length;)n=`${l[l.length-1]}.${n}`,l=l.slice(0,l.length-1),i=getLastOfPath(A,l,Object),(null==i?void 0:i.obj)&&void 0!==i.obj[`${i.k}.${n}`]&&(i.obj=void 0);i.obj[`${i.k}.${n}`]=e},pushPath=(A,t,e,a)=>{const{obj:r,k:n}=getLastOfPath(A,t,Object);r[n]=r[n]||[],r[n].push(e)},getPath=(A,t)=>{const{obj:e,k:a}=getLastOfPath(A,t);if(e&&Object.prototype.hasOwnProperty.call(e,a))return e[a]},getPathWithDefaults=(A,t,e)=>{const a=getPath(A,e);return void 0!==a?a:getPath(t,e)},deepExtend=(A,t,e)=>{for(const a in t)"__proto__"!==a&&"constructor"!==a&&(a in A?isString(A[a])||A[a]instanceof String||isString(t[a])||t[a]instanceof String?e&&(A[a]=t[a]):deepExtend(A[a],t[a],e):A[a]=t[a]);return A},regexEscape=A=>A.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&");var _entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};const escape=A=>isString(A)?A.replace(/[&<>"'\/]/g,A=>_entityMap[A]):A;class RegExpCache{constructor(A){this.capacity=A,this.regExpMap=new Map,this.regExpQueue=[]}getRegExp(A){const t=this.regExpMap.get(A);if(void 0!==t)return t;const e=new RegExp(A);return this.regExpQueue.length===this.capacity&&this.regExpMap.delete(this.regExpQueue.shift()),this.regExpMap.set(A,e),this.regExpQueue.push(A),e}}const chars=[" ",",","?","!",";"],looksLikeObjectPathRegExpCache=new RegExpCache(20),looksLikeObjectPath=(A,t,e)=>{t=t||"",e=e||"";const a=chars.filter(A=>t.indexOf(A)<0&&e.indexOf(A)<0);if(0===a.length)return!0;const r=looksLikeObjectPathRegExpCache.getRegExp(`(${a.map(A=>"?"===A?"\\?":A).join("|")})`);let n=!r.test(A);if(!n){const t=A.indexOf(e);t>0&&!r.test(A.substring(0,t))&&(n=!0)}return n},deepFind=function(A,t){let e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:".";if(!A)return;if(A[t]){if(!Object.prototype.hasOwnProperty.call(A,t))return;return A[t]}const a=t.split(e);let r=A;for(let n=0;n-1&&lnull==A?void 0:A.replace("_","-"),consoleLogger={type:"logger",log(A){this.output("log",A)},warn(A){this.output("warn",A)},error(A){this.output("error",A)},output(A,t){var e,a;null==(a=null==(e=null==console?void 0:console[A])?void 0:e.apply)||a.call(e,console,t)}};class Logger{constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.init(A,t)}init(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.prefix=t.prefix||"i18next:",this.logger=A||consoleLogger,this.options=t,this.debug=t.debug}log(){for(var A=arguments.length,t=new Array(A),e=0;e{this.observers[A]||(this.observers[A]=new Map);const e=this.observers[A].get(t)||0;this.observers[A].set(t,e+1)}),this}off(A,t){this.observers[A]&&(t?this.observers[A].delete(t):delete this.observers[A])}emit(A){for(var t=arguments.length,e=new Array(t>1?t-1:0),a=1;a{let[t,a]=A;for(let r=0;r{let[a,r]=t;for(let n=0;n1&&void 0!==arguments[1]?arguments[1]:{ns:["translation"],defaultNS:"translation"};super(),this.data=A||{},this.options=t,void 0===this.options.keySeparator&&(this.options.keySeparator="."),void 0===this.options.ignoreJSONStructure&&(this.options.ignoreJSONStructure=!0)}addNamespaces(A){this.options.ns.indexOf(A)<0&&this.options.ns.push(A)}removeNamespaces(A){const t=this.options.ns.indexOf(A);t>-1&&this.options.ns.splice(t,1)}getResource(A,t,e){var a,r;let n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const l=void 0!==n.keySeparator?n.keySeparator:this.options.keySeparator,i=void 0!==n.ignoreJSONStructure?n.ignoreJSONStructure:this.options.ignoreJSONStructure;let o;A.indexOf(".")>-1?o=A.split("."):(o=[A,t],e&&(Array.isArray(e)?o.push(...e):isString(e)&&l?o.push(...e.split(l)):o.push(e)));const s=getPath(this.data,o);return!s&&!t&&!e&&A.indexOf(".")>-1&&(A=o[0],t=o[1],e=o.slice(2).join(".")),!s&&i&&isString(e)?deepFind(null==(r=null==(a=this.data)?void 0:a[A])?void 0:r[t],e,l):s}addResource(A,t,e,a){let r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{silent:!1};const n=void 0!==r.keySeparator?r.keySeparator:this.options.keySeparator;let l=[A,t];e&&(l=l.concat(n?e.split(n):e)),A.indexOf(".")>-1&&(l=A.split("."),a=t,t=l[1]),this.addNamespaces(t),setPath(this.data,l,a),r.silent||this.emit("added",A,t,e,a)}addResources(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{silent:!1};for(const r in e)(isString(e[r])||Array.isArray(e[r]))&&this.addResource(A,t,r,e[r],{silent:!0});a.silent||this.emit("added",A,t,e)}addResourceBundle(A,t,e,a,r){let n=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{silent:!1,skipCopy:!1},l=[A,t];A.indexOf(".")>-1&&(l=A.split("."),a=e,e=t,t=l[1]),this.addNamespaces(t);let i=getPath(this.data,l)||{};n.skipCopy||(e=JSON.parse(JSON.stringify(e))),a?deepExtend(i,e,r):i={...i,...e},setPath(this.data,l,i),n.silent||this.emit("added",A,t,e)}removeResourceBundle(A,t){this.hasResourceBundle(A,t)&&delete this.data[A][t],this.removeNamespaces(t),this.emit("removed",A,t)}hasResourceBundle(A,t){return void 0!==this.getResource(A,t)}getResourceBundle(A,t){return t||(t=this.options.defaultNS),this.getResource(A,t)}getDataByLanguage(A){return this.data[A]}hasLanguageSomeTranslations(A){const t=this.getDataByLanguage(A);return!!(t&&Object.keys(t)||[]).find(A=>t[A]&&Object.keys(t[A]).length>0)}toJSON(){return this.data}}var postProcessor={processors:{},addPostProcessor(A){this.processors[A.name]=A},handle(A,t,e,a,r){return A.forEach(A=>{var n;t=(null==(n=this.processors[A])?void 0:n.process(t,e,a,r))??t}),t}};const checkedLoadedFor={},shouldHandleAsObject=A=>!isString(A)&&"boolean"!=typeof A&&"number"!=typeof A;class Translator extends EventEmitter{constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};super(),copy(["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],A,this),this.options=t,void 0===this.options.keySeparator&&(this.options.keySeparator="."),this.logger=baseLogger.create("translator")}changeLanguage(A){A&&(this.language=A)}exists(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}};if(null==A)return!1;const e=this.resolve(A,t);return void 0!==(null==e?void 0:e.res)}extractFromKey(A,t){let e=void 0!==t.nsSeparator?t.nsSeparator:this.options.nsSeparator;void 0===e&&(e=":");const a=void 0!==t.keySeparator?t.keySeparator:this.options.keySeparator;let r=t.ns||this.options.defaultNS||[];const n=e&&A.indexOf(e)>-1,l=!(this.options.userDefinedKeySeparator||t.keySeparator||this.options.userDefinedNsSeparator||t.nsSeparator||looksLikeObjectPath(A,e,a));if(n&&!l){const t=A.match(this.interpolator.nestingRegexp);if(t&&t.length>0)return{key:A,namespaces:isString(r)?[r]:r};const n=A.split(e);(e!==a||e===a&&this.options.ns.indexOf(n[0])>-1)&&(r=n.shift()),A=n.join(a)}return{key:A,namespaces:isString(r)?[r]:r}}translate(A,t,e){if("object"!=typeof t&&this.options.overloadTranslationOptionHandler&&(t=this.options.overloadTranslationOptionHandler(arguments)),"object"==typeof t&&(t={...t}),t||(t={}),null==A)return"";Array.isArray(A)||(A=[String(A)]);const a=void 0!==t.returnDetails?t.returnDetails:this.options.returnDetails,r=void 0!==t.keySeparator?t.keySeparator:this.options.keySeparator,{key:n,namespaces:l}=this.extractFromKey(A[A.length-1],t),i=l[l.length-1],o=t.lng||this.language,s=t.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if("cimode"===(null==o?void 0:o.toLowerCase())){if(s){const A=t.nsSeparator||this.options.nsSeparator;return a?{res:`${i}${A}${n}`,usedKey:n,exactUsedKey:n,usedLng:o,usedNS:i,usedParams:this.getUsedParamsDetails(t)}:`${i}${A}${n}`}return a?{res:n,usedKey:n,exactUsedKey:n,usedLng:o,usedNS:i,usedParams:this.getUsedParamsDetails(t)}:n}const p=this.resolve(A,t);let c=null==p?void 0:p.res;const u=(null==p?void 0:p.usedKey)||n,d=(null==p?void 0:p.exactUsedKey)||n,h=void 0!==t.joinArrays?t.joinArrays:this.options.joinArrays,S=!this.i18nFormat||this.i18nFormat.handleAsObject,f=void 0!==t.count&&!isString(t.count),m=Translator.hasDefaultValue(t),L=f?this.pluralResolver.getSuffix(o,t.count,t):"",W=t.ordinal&&f?this.pluralResolver.getSuffix(o,t.count,{ordinal:!1}):"",g=f&&!t.ordinal&&0===t.count,y=g&&t[`defaultValue${this.options.pluralSeparator}zero`]||t[`defaultValue${L}`]||t[`defaultValue${W}`]||t.defaultValue;let v=c;S&&!c&&m&&(v=y);const b=shouldHandleAsObject(v),x=Object.prototype.toString.apply(v);if(!(S&&v&&b&&["[object Number]","[object Function]","[object RegExp]"].indexOf(x)<0)||isString(h)&&Array.isArray(v))if(S&&isString(h)&&Array.isArray(c))c=c.join(h),c&&(c=this.extendTranslation(c,A,t,e));else{let a=!1,l=!1;!this.isValidLookup(c)&&m&&(a=!0,c=y),this.isValidLookup(c)||(l=!0,c=n);const s=(t.missingKeyNoValueFallbackToKey||this.options.missingKeyNoValueFallbackToKey)&&l?void 0:c,u=m&&y!==c&&this.options.updateMissing;if(l||a||u){if(this.logger.log(u?"updateKey":"missingKey",o,i,n,u?y:c),r){const A=this.resolve(n,{...t,keySeparator:!1});A&&A.res&&this.logger.warn("Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.")}let A=[];const e=this.languageUtils.getFallbackCodes(this.options.fallbackLng,t.lng||this.language);if("fallback"===this.options.saveMissingTo&&e&&e[0])for(let t=0;t{var r;const n=m&&a!==c?a:s;this.options.missingKeyHandler?this.options.missingKeyHandler(A,i,e,n,u,t):(null==(r=this.backendConnector)?void 0:r.saveMissing)&&this.backendConnector.saveMissing(A,i,e,n,u,t),this.emit("missingKey",A,i,e,c)};this.options.saveMissing&&(this.options.saveMissingPlurals&&f?A.forEach(A=>{const e=this.pluralResolver.getSuffixes(A,t);g&&t[`defaultValue${this.options.pluralSeparator}zero`]&&e.indexOf(`${this.options.pluralSeparator}zero`)<0&&e.push(`${this.options.pluralSeparator}zero`),e.forEach(e=>{a([A],n+e,t[`defaultValue${e}`]||y)})}):a(A,n,y))}c=this.extendTranslation(c,A,t,p,e),l&&c===n&&this.options.appendNamespaceToMissingKey&&(c=`${i}:${n}`),(l||a)&&this.options.parseMissingKeyHandler&&(c=this.options.parseMissingKeyHandler(this.options.appendNamespaceToMissingKey?`${i}:${n}`:n,a?c:void 0))}else{if(!t.returnObjects&&!this.options.returnObjects){this.options.returnedObjectHandler||this.logger.warn("accessing an object - but returnObjects options is not enabled!");const A=this.options.returnedObjectHandler?this.options.returnedObjectHandler(u,v,{...t,ns:l}):`key '${n} (${this.language})' returned an object instead of string.`;return a?(p.res=A,p.usedParams=this.getUsedParamsDetails(t),p):A}if(r){const A=Array.isArray(v),e=A?[]:{},a=A?d:u;for(const n in v)if(Object.prototype.hasOwnProperty.call(v,n)){const A=`${a}${r}${n}`;e[n]=m&&!c?this.translate(A,{...t,defaultValue:shouldHandleAsObject(y)?y[n]:void 0,joinArrays:!1,ns:l}):this.translate(A,{...t,joinArrays:!1,ns:l}),e[n]===A&&(e[n]=v[n])}c=e}}return a?(p.res=c,p.usedParams=this.getUsedParamsDetails(t),p):c}extendTranslation(A,t,e,a,r){var n,l,i=this;if(null==(n=this.i18nFormat)?void 0:n.parse)A=this.i18nFormat.parse(A,{...this.options.interpolation.defaultVariables,...e},e.lng||this.language||a.usedLng,a.usedNS,a.usedKey,{resolved:a});else if(!e.skipInterpolation){e.interpolation&&this.interpolator.init({...e,interpolation:{...this.options.interpolation,...e.interpolation}});const n=isString(A)&&(void 0!==(null==(l=null==e?void 0:e.interpolation)?void 0:l.skipOnVariables)?e.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables);let o;if(n){const t=A.match(this.interpolator.nestingRegexp);o=t&&t.length}let s=e.replace&&!isString(e.replace)?e.replace:e;if(this.options.interpolation.defaultVariables&&(s={...this.options.interpolation.defaultVariables,...s}),A=this.interpolator.interpolate(A,s,e.lng||this.language||a.usedLng,e),n){const t=A.match(this.interpolator.nestingRegexp);o<(t&&t.length)&&(e.nest=!1)}!e.lng&&a&&a.res&&(e.lng=this.language||a.usedLng),!1!==e.nest&&(A=this.interpolator.nest(A,function(){for(var A=arguments.length,a=new Array(A),n=0;n1&&void 0!==arguments[1]?arguments[1]:{};return isString(A)&&(A=[A]),A.forEach(A=>{if(this.isValidLookup(t))return;const i=this.extractFromKey(A,l),o=i.key;e=o;let s=i.namespaces;this.options.fallbackNS&&(s=s.concat(this.options.fallbackNS));const p=void 0!==l.count&&!isString(l.count),c=p&&!l.ordinal&&0===l.count,u=void 0!==l.context&&(isString(l.context)||"number"==typeof l.context)&&""!==l.context,d=l.lngs?l.lngs:this.languageUtils.toResolveHierarchy(l.lng||this.language,l.fallbackLng);s.forEach(A=>{var i,s;this.isValidLookup(t)||(n=A,checkedLoadedFor[`${d[0]}-${A}`]||!(null==(i=this.utils)?void 0:i.hasLoadedNamespace)||(null==(s=this.utils)?void 0:s.hasLoadedNamespace(n))||(checkedLoadedFor[`${d[0]}-${A}`]=!0,this.logger.warn(`key "${e}" for languages "${d.join(", ")}" won't get resolved as namespace "${n}" was not yet loaded`,"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),d.forEach(e=>{var n;if(this.isValidLookup(t))return;r=e;const i=[o];if(null==(n=this.i18nFormat)?void 0:n.addLookupKeys)this.i18nFormat.addLookupKeys(i,o,e,A,l);else{let A;p&&(A=this.pluralResolver.getSuffix(e,l.count,l));const t=`${this.options.pluralSeparator}zero`,a=`${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;if(p&&(i.push(o+A),l.ordinal&&0===A.indexOf(a)&&i.push(o+A.replace(a,this.options.pluralSeparator)),c&&i.push(o+t)),u){const e=`${o}${this.options.contextSeparator}${l.context}`;i.push(e),p&&(i.push(e+A),l.ordinal&&0===A.indexOf(a)&&i.push(e+A.replace(a,this.options.pluralSeparator)),c&&i.push(e+t))}}let s;for(;s=i.pop();)this.isValidLookup(t)||(a=s,t=this.getResource(e,A,s,l))}))})}),{res:t,usedKey:e,exactUsedKey:a,usedLng:r,usedNS:n}}isValidLookup(A){return!(void 0===A||!this.options.returnNull&&null===A||!this.options.returnEmptyString&&""===A)}getResource(A,t,e){var a;let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return(null==(a=this.i18nFormat)?void 0:a.getResource)?this.i18nFormat.getResource(A,t,e,r):this.resourceStore.getResource(A,t,e,r)}getUsedParamsDetails(){let A=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const t=["defaultValue","ordinal","context","replace","lng","lngs","fallbackLng","ns","keySeparator","nsSeparator","returnObjects","returnDetails","joinArrays","postProcess","interpolation"],e=A.replace&&!isString(A.replace);let a=e?A.replace:A;if(e&&void 0!==A.count&&(a.count=A.count),this.options.interpolation.defaultVariables&&(a={...this.options.interpolation.defaultVariables,...a}),!e){a={...a};for(const A of t)delete a[A]}return a}static hasDefaultValue(A){const t="defaultValue";for(const e in A)if(Object.prototype.hasOwnProperty.call(A,e)&&t===e.substring(0,12)&&void 0!==A[e])return!0;return!1}}class LanguageUtil{constructor(A){this.options=A,this.supportedLngs=this.options.supportedLngs||!1,this.logger=baseLogger.create("languageUtils")}getScriptPartFromCode(A){if(!(A=getCleanedCode(A))||A.indexOf("-")<0)return null;const t=A.split("-");return 2===t.length?null:(t.pop(),"x"===t[t.length-1].toLowerCase()?null:this.formatLanguageCode(t.join("-")))}getLanguagePartFromCode(A){if(!(A=getCleanedCode(A))||A.indexOf("-")<0)return A;const t=A.split("-");return this.formatLanguageCode(t[0])}formatLanguageCode(A){if(isString(A)&&A.indexOf("-")>-1){let e;try{e=Intl.getCanonicalLocales(A)[0]}catch(t){}return e&&this.options.lowerCaseLng&&(e=e.toLowerCase()),e||(this.options.lowerCaseLng?A.toLowerCase():A)}return this.options.cleanCode||this.options.lowerCaseLng?A.toLowerCase():A}isSupportedCode(A){return("languageOnly"===this.options.load||this.options.nonExplicitSupportedLngs)&&(A=this.getLanguagePartFromCode(A)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(A)>-1}getBestMatchFromCodes(A){if(!A)return null;let t;return A.forEach(A=>{if(t)return;const e=this.formatLanguageCode(A);this.options.supportedLngs&&!this.isSupportedCode(e)||(t=e)}),!t&&this.options.supportedLngs&&A.forEach(A=>{if(t)return;const e=this.getLanguagePartFromCode(A);if(this.isSupportedCode(e))return t=e;t=this.options.supportedLngs.find(A=>A===e?A:A.indexOf("-")<0&&e.indexOf("-")<0?void 0:A.indexOf("-")>0&&e.indexOf("-")<0&&A.substring(0,A.indexOf("-"))===e||0===A.indexOf(e)&&e.length>1?A:void 0)}),t||(t=this.getFallbackCodes(this.options.fallbackLng)[0]),t}getFallbackCodes(A,t){if(!A)return[];if("function"==typeof A&&(A=A(t)),isString(A)&&(A=[A]),Array.isArray(A))return A;if(!t)return A.default||[];let e=A[t];return e||(e=A[this.getScriptPartFromCode(t)]),e||(e=A[this.formatLanguageCode(t)]),e||(e=A[this.getLanguagePartFromCode(t)]),e||(e=A.default),e||[]}toResolveHierarchy(A,t){const e=this.getFallbackCodes(t||this.options.fallbackLng||[],A),a=[],r=A=>{A&&(this.isSupportedCode(A)?a.push(A):this.logger.warn(`rejecting language code not found in supportedLngs: ${A}`))};return isString(A)&&(A.indexOf("-")>-1||A.indexOf("_")>-1)?("languageOnly"!==this.options.load&&r(this.formatLanguageCode(A)),"languageOnly"!==this.options.load&&"currentOnly"!==this.options.load&&r(this.getScriptPartFromCode(A)),"currentOnly"!==this.options.load&&r(this.getLanguagePartFromCode(A))):isString(A)&&r(this.formatLanguageCode(A)),e.forEach(A=>{a.indexOf(A)<0&&r(this.formatLanguageCode(A))}),a}}const suffixesOrder={zero:0,one:1,two:2,few:3,many:4,other:5},dummyRule={select:A=>1===A?"one":"other",resolvedOptions:()=>({pluralCategories:["one","other"]})};class PluralResolver{constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.languageUtils=A,this.options=t,this.logger=baseLogger.create("pluralResolver"),this.pluralRulesCache={}}addRule(A,t){this.rules[A]=t}clearCache(){this.pluralRulesCache={}}getRule(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const e=getCleanedCode("dev"===A?"en":A),a=t.ordinal?"ordinal":"cardinal",r=JSON.stringify({cleanedCode:e,type:a});if(r in this.pluralRulesCache)return this.pluralRulesCache[r];let n;try{n=new Intl.PluralRules(e,{type:a})}catch(l){if(!Intl)return this.logger.error("No Intl support, please use an Intl polyfill!"),dummyRule;if(!A.match(/-|_/))return dummyRule;const e=this.languageUtils.getLanguagePartFromCode(A);n=this.getRule(e,t)}return this.pluralRulesCache[r]=n,n}needsPlural(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},e=this.getRule(A,t);return e||(e=this.getRule("dev",t)),(null==e?void 0:e.resolvedOptions().pluralCategories.length)>1}getPluralFormsOfKey(A,t){let e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.getSuffixes(A,e).map(A=>`${t}${A}`)}getSuffixes(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},e=this.getRule(A,t);return e||(e=this.getRule("dev",t)),e?e.resolvedOptions().pluralCategories.sort((A,t)=>suffixesOrder[A]-suffixesOrder[t]).map(A=>`${this.options.prepend}${t.ordinal?`ordinal${this.options.prepend}`:""}${A}`):[]}getSuffix(A,t){let e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const a=this.getRule(A,e);return a?`${this.options.prepend}${e.ordinal?`ordinal${this.options.prepend}`:""}${a.select(t)}`:(this.logger.warn(`no plural rule found for: ${A}`),this.getSuffix("dev",t,e))}}const deepFindWithDefaults=function(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:".",r=!(arguments.length>4&&void 0!==arguments[4])||arguments[4],n=getPathWithDefaults(A,t,e);return!n&&r&&isString(e)&&(n=deepFind(A,e,a),void 0===n&&(n=deepFind(t,e,a))),n},regexSafe=A=>A.replace(/\$/g,"$$$$");class Interpolator{constructor(){var A;let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.logger=baseLogger.create("interpolator"),this.options=t,this.format=(null==(A=null==t?void 0:t.interpolation)?void 0:A.format)||(A=>A),this.init(t)}init(){let A=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};A.interpolation||(A.interpolation={escapeValue:!0});const{escape:t,escapeValue:e,useRawValueToEscape:a,prefix:r,prefixEscaped:n,suffix:l,suffixEscaped:i,formatSeparator:o,unescapeSuffix:s,unescapePrefix:p,nestingPrefix:c,nestingPrefixEscaped:u,nestingSuffix:d,nestingSuffixEscaped:h,nestingOptionsSeparator:S,maxReplaces:f,alwaysFormat:m}=A.interpolation;this.escape=void 0!==t?t:escape,this.escapeValue=void 0===e||e,this.useRawValueToEscape=void 0!==a&&a,this.prefix=r?regexEscape(r):n||"{{",this.suffix=l?regexEscape(l):i||"}}",this.formatSeparator=o||",",this.unescapePrefix=s?"":p||"-",this.unescapeSuffix=this.unescapePrefix?"":s||"",this.nestingPrefix=c?regexEscape(c):u||regexEscape("$t("),this.nestingSuffix=d?regexEscape(d):h||regexEscape(")"),this.nestingOptionsSeparator=S||",",this.maxReplaces=f||1e3,this.alwaysFormat=void 0!==m&&m,this.resetRegExp()}reset(){this.options&&this.init(this.options)}resetRegExp(){const A=(A,t)=>(null==A?void 0:A.source)===t?(A.lastIndex=0,A):new RegExp(t,"g");this.regexp=A(this.regexp,`${this.prefix}(.+?)${this.suffix}`),this.regexpUnescape=A(this.regexpUnescape,`${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`),this.nestingRegexp=A(this.nestingRegexp,`${this.nestingPrefix}(.+?)${this.nestingSuffix}`)}interpolate(A,t,e,a){var r;let n,l,i;const o=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{},s=A=>{if(A.indexOf(this.formatSeparator)<0){const r=deepFindWithDefaults(t,o,A,this.options.keySeparator,this.options.ignoreJSONStructure);return this.alwaysFormat?this.format(r,void 0,e,{...a,...t,interpolationkey:A}):r}const r=A.split(this.formatSeparator),n=r.shift().trim(),l=r.join(this.formatSeparator).trim();return this.format(deepFindWithDefaults(t,o,n,this.options.keySeparator,this.options.ignoreJSONStructure),l,e,{...a,...t,interpolationkey:n})};this.resetRegExp();const p=(null==a?void 0:a.missingInterpolationHandler)||this.options.missingInterpolationHandler,c=void 0!==(null==(r=null==a?void 0:a.interpolation)?void 0:r.skipOnVariables)?a.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:A=>regexSafe(A)},{regex:this.regexp,safeValue:A=>this.escapeValue?regexSafe(this.escape(A)):regexSafe(A)}].forEach(t=>{for(i=0;n=t.regex.exec(A);){const e=n[1].trim();if(l=s(e),void 0===l)if("function"==typeof p){const t=p(A,n,a);l=isString(t)?t:""}else if(a&&Object.prototype.hasOwnProperty.call(a,e))l="";else{if(c){l=n[0];continue}this.logger.warn(`missed to pass in variable ${e} for interpolating ${A}`),l=""}else isString(l)||this.useRawValueToEscape||(l=makeString(l));const r=t.safeValue(l);if(A=A.replace(n[0],r),c?(t.regex.lastIndex+=l.length,t.regex.lastIndex-=n[0].length):t.regex.lastIndex=0,i++,i>=this.maxReplaces)break}}),A}nest(A,t){let e,a,r,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const l=(A,t)=>{const e=this.nestingOptionsSeparator;if(A.indexOf(e)<0)return A;const a=A.split(new RegExp(`${e}[ ]*{`));let n=`{${a[1]}`;A=a[0],n=this.interpolate(n,r);const l=n.match(/'/g),i=n.match(/"/g);(((null==l?void 0:l.length)??0)%2==0&&!i||i.length%2!=0)&&(n=n.replace(/'/g,'"'));try{r=JSON.parse(n),t&&(r={...t,...r})}catch(o){return this.logger.warn(`failed parsing options string in nesting for key ${A}`,o),`${A}${e}${n}`}return r.defaultValue&&r.defaultValue.indexOf(this.prefix)>-1&&delete r.defaultValue,A};for(;e=this.nestingRegexp.exec(A);){let i=[];r={...n},r=r.replace&&!isString(r.replace)?r.replace:r,r.applyPostProcessor=!1,delete r.defaultValue;let o=!1;if(-1!==e[0].indexOf(this.formatSeparator)&&!/{.*}/.test(e[1])){const A=e[1].split(this.formatSeparator).map(A=>A.trim());e[1]=A.shift(),i=A,o=!0}if(a=t(l.call(this,e[1].trim(),r),r),a&&e[0]===A&&!isString(a))return a;isString(a)||(a=makeString(a)),a||(this.logger.warn(`missed to resolve ${e[1]} for nesting ${A}`),a=""),o&&(a=i.reduce((A,t)=>this.format(A,t,n.lng,{...n,interpolationkey:e[1].trim()}),a.trim())),A=A.replace(e[0],a),this.regexp.lastIndex=0}return A}}const parseFormatStr=A=>{let t=A.toLowerCase().trim();const e={};if(A.indexOf("(")>-1){const a=A.split("(");t=a[0].toLowerCase().trim();const r=a[1].substring(0,a[1].length-1);if("currency"===t&&r.indexOf(":")<0)e.currency||(e.currency=r.trim());else if("relativetime"===t&&r.indexOf(":")<0)e.range||(e.range=r.trim());else{r.split(";").forEach(A=>{if(A){const[t,...a]=A.split(":"),r=a.join(":").trim().replace(/^'+|'+$/g,""),n=t.trim();e[n]||(e[n]=r),"false"===r&&(e[n]=!1),"true"===r&&(e[n]=!0),isNaN(r)||(e[n]=parseInt(r,10))}})}}return{formatName:t,formatOptions:e}},createCachedFormatter=A=>{const t={};return(e,a,r)=>{let n=r;r&&r.interpolationkey&&r.formatParams&&r.formatParams[r.interpolationkey]&&r[r.interpolationkey]&&(n={...n,[r.interpolationkey]:void 0});const l=a+JSON.stringify(n);let i=t[l];return i||(i=A(getCleanedCode(a),r),t[l]=i),i(e)}};class Formatter{constructor(){let A=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.logger=baseLogger.create("formatter"),this.options=A,this.formats={number:createCachedFormatter((A,t)=>{const e=new Intl.NumberFormat(A,{...t});return A=>e.format(A)}),currency:createCachedFormatter((A,t)=>{const e=new Intl.NumberFormat(A,{...t,style:"currency"});return A=>e.format(A)}),datetime:createCachedFormatter((A,t)=>{const e=new Intl.DateTimeFormat(A,{...t});return A=>e.format(A)}),relativetime:createCachedFormatter((A,t)=>{const e=new Intl.RelativeTimeFormat(A,{...t});return A=>e.format(A,t.range||"day")}),list:createCachedFormatter((A,t)=>{const e=new Intl.ListFormat(A,{...t});return A=>e.format(A)})},this.init(A)}init(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}};this.formatSeparator=t.interpolation.formatSeparator||","}add(A,t){this.formats[A.toLowerCase().trim()]=t}addCached(A,t){this.formats[A.toLowerCase().trim()]=createCachedFormatter(t)}format(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const r=t.split(this.formatSeparator);if(r.length>1&&r[0].indexOf("(")>1&&r[0].indexOf(")")<0&&r.find(A=>A.indexOf(")")>-1)){const A=r.findIndex(A=>A.indexOf(")")>-1);r[0]=[r[0],...r.splice(1,A)].join(this.formatSeparator)}return r.reduce((A,t)=>{var r;const{formatName:n,formatOptions:l}=parseFormatStr(t);if(this.formats[n]){let t=A;try{const i=(null==(r=null==a?void 0:a.formatParams)?void 0:r[a.interpolationkey])||{},o=i.locale||i.lng||a.locale||a.lng||e;t=this.formats[n](A,o,{...l,...a,...i})}catch(i){this.logger.warn(i)}return t}return this.logger.warn(`there was no format function for ${n}`),A},A)}}const removePending=(A,t)=>{void 0!==A.pending[t]&&(delete A.pending[t],A.pendingCount--)};class Connector extends EventEmitter{constructor(A,t,e){var a,r;let n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};super(),this.backend=A,this.store=t,this.services=e,this.languageUtils=e.languageUtils,this.options=n,this.logger=baseLogger.create("backendConnector"),this.waitingReads=[],this.maxParallelReads=n.maxParallelReads||10,this.readingCalls=0,this.maxRetries=n.maxRetries>=0?n.maxRetries:5,this.retryTimeout=n.retryTimeout>=1?n.retryTimeout:350,this.state={},this.queue=[],null==(r=null==(a=this.backend)?void 0:a.init)||r.call(a,e,n.backend,n)}queueLoad(A,t,e,a){const r={},n={},l={},i={};return A.forEach(A=>{let a=!0;t.forEach(t=>{const l=`${A}|${t}`;!e.reload&&this.store.hasResourceBundle(A,t)?this.state[l]=2:this.state[l]<0||(1===this.state[l]?void 0===n[l]&&(n[l]=!0):(this.state[l]=1,a=!1,void 0===n[l]&&(n[l]=!0),void 0===r[l]&&(r[l]=!0),void 0===i[t]&&(i[t]=!0)))}),a||(l[A]=!0)}),(Object.keys(r).length||Object.keys(n).length)&&this.queue.push({pending:n,pendingCount:Object.keys(n).length,loaded:{},errors:[],callback:a}),{toLoad:Object.keys(r),pending:Object.keys(n),toLoadLanguages:Object.keys(l),toLoadNamespaces:Object.keys(i)}}loaded(A,t,e){const a=A.split("|"),r=a[0],n=a[1];t&&this.emit("failedLoading",r,n,t),!t&&e&&this.store.addResourceBundle(r,n,e,void 0,void 0,{skipCopy:!0}),this.state[A]=t?-1:2,t&&e&&(this.state[A]=0);const l={};this.queue.forEach(e=>{pushPath(e.loaded,[r],n),removePending(e,A),t&&e.errors.push(t),0!==e.pendingCount||e.done||(Object.keys(e.loaded).forEach(A=>{l[A]||(l[A]={});const t=e.loaded[A];t.length&&t.forEach(t=>{void 0===l[A][t]&&(l[A][t]=!0)})}),e.done=!0,e.errors.length?e.callback(e.errors):e.callback())}),this.emit("loaded",l),this.queue=this.queue.filter(A=>!A.done)}read(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:this.retryTimeout,n=arguments.length>5?arguments[5]:void 0;if(!A.length)return n(null,{});if(this.readingCalls>=this.maxParallelReads)return void this.waitingReads.push({lng:A,ns:t,fcName:e,tried:a,wait:r,callback:n});this.readingCalls++;const l=(l,i)=>{if(this.readingCalls--,this.waitingReads.length>0){const A=this.waitingReads.shift();this.read(A.lng,A.ns,A.fcName,A.tried,A.wait,A.callback)}l&&i&&a{this.read.call(this,A,t,e,a+1,2*r,n)},r):n(l,i)},i=this.backend[e].bind(this.backend);if(2!==i.length)return i(A,t,l);try{const e=i(A,t);e&&"function"==typeof e.then?e.then(A=>l(null,A)).catch(l):l(null,e)}catch(o){l(o)}}prepareLoading(A,t){let e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},a=arguments.length>3?arguments[3]:void 0;if(!this.backend)return this.logger.warn("No backend was added via i18next.use. Will not load resources."),a&&a();isString(A)&&(A=this.languageUtils.toResolveHierarchy(A)),isString(t)&&(t=[t]);const r=this.queueLoad(A,t,e,a);if(!r.toLoad.length)return r.pending.length||a(),null;r.toLoad.forEach(A=>{this.loadOne(A)})}load(A,t,e){this.prepareLoading(A,t,{},e)}reload(A,t,e){this.prepareLoading(A,t,{reload:!0},e)}loadOne(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";const e=A.split("|"),a=e[0],r=e[1];this.read(a,r,"read",void 0,void 0,(e,n)=>{e&&this.logger.warn(`${t}loading namespace ${r} for language ${a} failed`,e),!e&&n&&this.logger.log(`${t}loaded namespace ${r} for language ${a}`,n),this.loaded(A,e,n)})}saveMissing(A,t,e,a,r){var n,l,i,o,s;let p=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{},c=arguments.length>6&&void 0!==arguments[6]?arguments[6]:()=>{};if(!(null==(l=null==(n=this.services)?void 0:n.utils)?void 0:l.hasLoadedNamespace)||(null==(o=null==(i=this.services)?void 0:i.utils)?void 0:o.hasLoadedNamespace(t))){if(null!=e&&""!==e){if(null==(s=this.backend)?void 0:s.create){const n={...p,isUpdate:r},l=this.backend.create.bind(this.backend);if(l.length<6)try{let r;r=5===l.length?l(A,t,e,a,n):l(A,t,e,a),r&&"function"==typeof r.then?r.then(A=>c(null,A)).catch(c):c(null,r)}catch(u){c(u)}else l(A,t,e,a,c,n)}A&&A[0]&&this.store.addResource(A[0],t,e,a)}}else this.logger.warn(`did not save key "${e}" as the namespace "${t}" was not yet loaded`,"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")}}const get=()=>({debug:!1,initAsync:!0,ns:["translation"],defaultNS:["translation"],fallbackLng:["dev"],fallbackNS:!1,supportedLngs:!1,nonExplicitSupportedLngs:!1,load:"all",preload:!1,simplifyPluralSuffix:!0,keySeparator:".",nsSeparator:":",pluralSeparator:"_",contextSeparator:"_",partialBundledLanguages:!1,saveMissing:!1,updateMissing:!1,saveMissingTo:"fallback",saveMissingPlurals:!0,missingKeyHandler:!1,missingInterpolationHandler:!1,postProcess:!1,postProcessPassResolved:!1,returnNull:!1,returnEmptyString:!0,returnObjects:!1,joinArrays:!1,returnedObjectHandler:!1,parseMissingKeyHandler:!1,appendNamespaceToMissingKey:!1,appendNamespaceToCIMode:!1,overloadTranslationOptionHandler:A=>{let t={};if("object"==typeof A[1]&&(t=A[1]),isString(A[1])&&(t.defaultValue=A[1]),isString(A[2])&&(t.tDescription=A[2]),"object"==typeof A[2]||"object"==typeof A[3]){const e=A[3]||A[2];Object.keys(e).forEach(A=>{t[A]=e[A]})}return t},interpolation:{escapeValue:!0,format:A=>A,prefix:"{{",suffix:"}}",formatSeparator:",",unescapePrefix:"-",nestingPrefix:"$t(",nestingSuffix:")",nestingOptionsSeparator:",",maxReplaces:1e3,skipOnVariables:!0}}),transformOptions=A=>{var t,e;return isString(A.ns)&&(A.ns=[A.ns]),isString(A.fallbackLng)&&(A.fallbackLng=[A.fallbackLng]),isString(A.fallbackNS)&&(A.fallbackNS=[A.fallbackNS]),(null==(e=null==(t=A.supportedLngs)?void 0:t.indexOf)?void 0:e.call(t,"cimode"))<0&&(A.supportedLngs=A.supportedLngs.concat(["cimode"])),"boolean"==typeof A.initImmediate&&(A.initAsync=A.initImmediate),A},noop$1=()=>{},bindMemberFunctions=A=>{Object.getOwnPropertyNames(Object.getPrototypeOf(A)).forEach(t=>{"function"==typeof A[t]&&(A[t]=A[t].bind(A))})};class I18n extends EventEmitter{constructor(){let A=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;if(super(),this.options=transformOptions(A),this.services={},this.logger=baseLogger,this.modules={external:[]},bindMemberFunctions(this),t&&!this.isInitialized&&!A.isClone){if(!this.options.initAsync)return this.init(A,t),this;setTimeout(()=>{this.init(A,t)},0)}}init(){var A=this;let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=arguments.length>1?arguments[1]:void 0;this.isInitializing=!0,"function"==typeof t&&(e=t,t={}),null==t.defaultNS&&t.ns&&(isString(t.ns)?t.defaultNS=t.ns:t.ns.indexOf("translation")<0&&(t.defaultNS=t.ns[0]));const a=get();this.options={...a,...this.options,...transformOptions(t)},this.options.interpolation={...a.interpolation,...this.options.interpolation},void 0!==t.keySeparator&&(this.options.userDefinedKeySeparator=t.keySeparator),void 0!==t.nsSeparator&&(this.options.userDefinedNsSeparator=t.nsSeparator);const r=A=>A?"function"==typeof A?new A:A:null;if(!this.options.isClone){let t;this.modules.logger?baseLogger.init(r(this.modules.logger),this.options):baseLogger.init(null,this.options),t=this.modules.formatter?this.modules.formatter:Formatter;const e=new LanguageUtil(this.options);this.store=new ResourceStore(this.options.resources,this.options);const n=this.services;n.logger=baseLogger,n.resourceStore=this.store,n.languageUtils=e,n.pluralResolver=new PluralResolver(e,{prepend:this.options.pluralSeparator,simplifyPluralSuffix:this.options.simplifyPluralSuffix}),!t||this.options.interpolation.format&&this.options.interpolation.format!==a.interpolation.format||(n.formatter=r(t),n.formatter.init(n,this.options),this.options.interpolation.format=n.formatter.format.bind(n.formatter)),n.interpolator=new Interpolator(this.options),n.utils={hasLoadedNamespace:this.hasLoadedNamespace.bind(this)},n.backendConnector=new Connector(r(this.modules.backend),n.resourceStore,n,this.options),n.backendConnector.on("*",function(t){for(var e=arguments.length,a=new Array(e>1?e-1:0),r=1;r1?e-1:0),r=1;r{A.init&&A.init(this)})}if(this.format=this.options.interpolation.format,e||(e=noop$1),this.options.fallbackLng&&!this.services.languageDetector&&!this.options.lng){const A=this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);A.length>0&&"dev"!==A[0]&&(this.options.lng=A[0])}this.services.languageDetector||this.options.lng||this.logger.warn("init: no languageDetector is used and no lng is defined");["getResource","hasResourceBundle","getResourceBundle","getDataByLanguage"].forEach(t=>{this[t]=function(){return A.store[t](...arguments)}});["addResource","addResources","addResourceBundle","removeResourceBundle"].forEach(t=>{this[t]=function(){return A.store[t](...arguments),A}});const n=defer(),l=()=>{const A=(A,t)=>{this.isInitializing=!1,this.isInitialized&&!this.initializedStoreOnce&&this.logger.warn("init: i18next is already initialized. You should call init just once!"),this.isInitialized=!0,this.options.isClone||this.logger.log("initialized",this.options),this.emit("initialized",this.options),n.resolve(t),e(A,t)};if(this.languages&&!this.isInitialized)return A(null,this.t.bind(this));this.changeLanguage(this.options.lng,A)};return this.options.resources||!this.options.initAsync?l():setTimeout(l,0),n}loadResources(A){var t,e;let a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:noop$1;const r=isString(A)?A:this.language;if("function"==typeof A&&(a=A),!this.options.resources||this.options.partialBundledLanguages){if("cimode"===(null==r?void 0:r.toLowerCase())&&(!this.options.preload||0===this.options.preload.length))return a();const A=[],n=t=>{if(!t)return;if("cimode"===t)return;this.services.languageUtils.toResolveHierarchy(t).forEach(t=>{"cimode"!==t&&A.indexOf(t)<0&&A.push(t)})};if(r)n(r);else{this.services.languageUtils.getFallbackCodes(this.options.fallbackLng).forEach(A=>n(A))}null==(e=null==(t=this.options.preload)?void 0:t.forEach)||e.call(t,A=>n(A)),this.services.backendConnector.load(A,this.options.ns,A=>{A||this.resolvedLanguage||!this.language||this.setResolvedLanguage(this.language),a(A)})}else a(null)}reloadResources(A,t,e){const a=defer();return"function"==typeof A&&(e=A,A=void 0),"function"==typeof t&&(e=t,t=void 0),A||(A=this.languages),t||(t=this.options.ns),e||(e=noop$1),this.services.backendConnector.reload(A,t,A=>{a.resolve(),e(A)}),a}use(A){if(!A)throw new Error("You are passing an undefined module! Please check the object you are passing to i18next.use()");if(!A.type)throw new Error("You are passing a wrong module! Please check the object you are passing to i18next.use()");return"backend"===A.type&&(this.modules.backend=A),("logger"===A.type||A.log&&A.warn&&A.error)&&(this.modules.logger=A),"languageDetector"===A.type&&(this.modules.languageDetector=A),"i18nFormat"===A.type&&(this.modules.i18nFormat=A),"postProcessor"===A.type&&postProcessor.addPostProcessor(A),"formatter"===A.type&&(this.modules.formatter=A),"3rdParty"===A.type&&this.modules.external.push(A),this}setResolvedLanguage(A){if(A&&this.languages&&!(["cimode","dev"].indexOf(A)>-1))for(let t=0;t-1)&&this.store.hasLanguageSomeTranslations(A)){this.resolvedLanguage=A;break}}}changeLanguage(A,t){var e=this;this.isLanguageChangingTo=A;const a=defer();this.emit("languageChanging",A);const r=A=>{this.language=A,this.languages=this.services.languageUtils.toResolveHierarchy(A),this.resolvedLanguage=void 0,this.setResolvedLanguage(A)},n=(A,n)=>{n?(r(n),this.translator.changeLanguage(n),this.isLanguageChangingTo=void 0,this.emit("languageChanged",n),this.logger.log("languageChanged",n)):this.isLanguageChangingTo=void 0,a.resolve(function(){return e.t(...arguments)}),t&&t(A,function(){return e.t(...arguments)})},l=t=>{var e,a;A||t||!this.services.languageDetector||(t=[]);const l=isString(t)?t:this.services.languageUtils.getBestMatchFromCodes(t);l&&(this.language||r(l),this.translator.language||this.translator.changeLanguage(l),null==(a=null==(e=this.services.languageDetector)?void 0:e.cacheUserLanguage)||a.call(e,l)),this.loadResources(l,A=>{n(A,l)})};return A||!this.services.languageDetector||this.services.languageDetector.async?!A&&this.services.languageDetector&&this.services.languageDetector.async?0===this.services.languageDetector.detect.length?this.services.languageDetector.detect().then(l):this.services.languageDetector.detect(l):l(A):l(this.services.languageDetector.detect()),a}getFixedT(A,t,e){var a=this;const r=function(A,t){let n;if("object"!=typeof t){for(var l=arguments.length,i=new Array(l>2?l-2:0),o=2;o`${n.keyPrefix}${s}${A}`):n.keyPrefix?`${n.keyPrefix}${s}${A}`:A,a.t(p,n)};return isString(A)?r.lng=A:r.lngs=A,r.ns=t,r.keyPrefix=e,r}t(){for(var A,t=arguments.length,e=new Array(t),a=0;a1&&void 0!==arguments[1]?arguments[1]:{};if(!this.isInitialized)return this.logger.warn("hasLoadedNamespace: i18next was not initialized",this.languages),!1;if(!this.languages||!this.languages.length)return this.logger.warn("hasLoadedNamespace: i18n.languages were undefined or empty",this.languages),!1;const e=t.lng||this.resolvedLanguage||this.languages[0],a=!!this.options&&this.options.fallbackLng,r=this.languages[this.languages.length-1];if("cimode"===e.toLowerCase())return!0;const n=(A,t)=>{const e=this.services.backendConnector.state[`${A}|${t}`];return-1===e||0===e||2===e};if(t.precheck){const A=t.precheck(this,n);if(void 0!==A)return A}return!!this.hasResourceBundle(e,A)||(!(this.services.backendConnector.backend&&(!this.options.resources||this.options.partialBundledLanguages))||!(!n(e,A)||a&&!n(r,A)))}loadNamespaces(A,t){const e=defer();return this.options.ns?(isString(A)&&(A=[A]),A.forEach(A=>{this.options.ns.indexOf(A)<0&&this.options.ns.push(A)}),this.loadResources(A=>{e.resolve(),t&&t(A)}),e):(t&&t(),Promise.resolve())}loadLanguages(A,t){const e=defer();isString(A)&&(A=[A]);const a=this.options.preload||[],r=A.filter(A=>a.indexOf(A)<0&&this.services.languageUtils.isSupportedCode(A));return r.length?(this.options.preload=a.concat(r),this.loadResources(A=>{e.resolve(),t&&t(A)}),e):(t&&t(),Promise.resolve())}dir(A){var t,e;if(A||(A=this.resolvedLanguage||((null==(t=this.languages)?void 0:t.length)>0?this.languages[0]:this.language)),!A)return"rtl";const a=(null==(e=this.services)?void 0:e.languageUtils)||new LanguageUtil(get());return["ar","shu","sqr","ssh","xaa","yhd","yud","aao","abh","abv","acm","acq","acw","acx","acy","adf","ads","aeb","aec","afb","ajp","apc","apd","arb","arq","ars","ary","arz","auz","avl","ayh","ayl","ayn","ayp","bbz","pga","he","iw","ps","pbt","pbu","pst","prp","prd","ug","ur","ydd","yds","yih","ji","yi","hbo","men","xmn","fa","jpr","peo","pes","prs","dv","sam","ckb"].indexOf(a.getLanguagePartFromCode(A))>-1||A.toLowerCase().indexOf("-arab")>1?"rtl":"ltr"}static createInstance(){return new I18n(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},arguments.length>1?arguments[1]:void 0)}cloneInstance(){let A=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:noop$1;const e=A.forkResourceStore;e&&delete A.forkResourceStore;const a={...this.options,...A,isClone:!0},r=new I18n(a);void 0===A.debug&&void 0===A.prefix||(r.logger=r.logger.clone(A));if(["store","services","language"].forEach(A=>{r[A]=this[A]}),r.services={...this.services},r.services.utils={hasLoadedNamespace:r.hasLoadedNamespace.bind(r)},e){const A=Object.keys(this.store.data).reduce((A,t)=>(A[t]={...this.store.data[t]},Object.keys(A[t]).reduce((e,a)=>(e[a]={...A[t][a]},e),{})),{});r.store=new ResourceStore(A,a),r.services.resourceStore=r.store}return r.translator=new Translator(r.services,a),r.translator.on("*",function(A){for(var t=arguments.length,e=new Array(t>1?t-1:0),a=1;a__vitePreload(()=>import("./en-vU35wTjd.js"),[]),"./locales/ja.ts":()=>__vitePreload(()=>import("./ja-Q5acyAjl.js"),[]),"./locales/ko.ts":()=>__vitePreload(()=>import("./ko-CMKMFQrR.js"),[]),"./locales/zh-CN.ts":()=>__vitePreload(()=>import("./zh-CN-BZhLE8JW.js"),[]),"./locales/zh-TW.ts":()=>__vitePreload(()=>import("./zh-TW-DGtNjFz9.js"),[])}),ensureResourceLoaded=async A=>{if(instance.hasResourceBundle(A,"translation"))return;let t=null;const e=locales[`./locales/${A}.ts`];if(e)try{t=(await e()).default}catch(a){}if(!t){const e=A.toLowerCase(),r=Object.keys(locales).find(A=>A.toLowerCase()===`./locales/${e}.ts`);if(r){const A=locales[r];try{t=(await A()).default}catch(a){}}}if(!t&&A.includes("-")){const e=A.split("-")[0],r=locales[`./locales/${e}.ts`];if(r)try{t=(await r()).default}catch(a){}}if(!t&&"zh-CN"!==A)try{const A=locales["./locales/zh-CN.ts"];if(A){t=(await A()).default}}catch(a){}t&&instance.addResourceBundle(A,"translation",t,!0,!0)};instance.use(initReactI18next).init({resources:{},lng:getBrowserLang(),fallbackLng:"zh-CN",interpolation:{escapeValue:!1},react:{bindI18n:"languageChanged added",useSuspense:!1}});const initialLang=getBrowserLang();ensureResourceLoaded(initialLang).then(()=>{instance.language!==initialLang&&instance.changeLanguage(initialLang)}),instance.on("languageChanged",async A=>{await ensureResourceLoaded(A)}); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const toKebabCase=A=>A.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),mergeClasses=(...A)=>A.filter((A,t,e)=>Boolean(A)&&""!==A.trim()&&e.indexOf(A)===t).join(" ").trim(); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ var defaultAttributes={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}; /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const Icon=reactExports.forwardRef(({color:A="currentColor",size:t=24,strokeWidth:e=2,absoluteStrokeWidth:a,className:r="",children:n,iconNode:l,...i},o)=>reactExports.createElement("svg",{ref:o,...defaultAttributes,width:t,height:t,stroke:A,strokeWidth:a?24*Number(e)/Number(t):e,className:mergeClasses("lucide",r),...i},[...l.map(([A,t])=>reactExports.createElement(A,t)),...Array.isArray(n)?n:[n]])),createLucideIcon=(A,t)=>{const e=reactExports.forwardRef(({className:e,...a},r)=>reactExports.createElement(Icon,{ref:r,iconNode:t,className:mergeClasses(`lucide-${toKebabCase(A)}`,e),...a}));return e.displayName=`${A}`,e},Check=createLucideIcon("Check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]),ChevronRight=createLucideIcon("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]),CircleCheckBig=createLucideIcon("CircleCheckBig",[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]),CircleCheck=createLucideIcon("CircleCheck",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]),CircleHelp=createLucideIcon("CircleHelp",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]]),CircleX=createLucideIcon("CircleX",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]),Circle=createLucideIcon("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]),Info=createLucideIcon("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]),Languages=createLucideIcon("Languages",[["path",{d:"m5 8 6 6",key:"1wu5hv"}],["path",{d:"m4 14 6-6 2-3",key:"1k1g8d"}],["path",{d:"M2 5h12",key:"or177f"}],["path",{d:"M7 2h1",key:"1t2jsx"}],["path",{d:"m22 22-5-10-5 10",key:"don7ne"}],["path",{d:"M14 18h6",key:"1m8k6r"}]]),LoaderCircle=createLucideIcon("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]),Lock=createLucideIcon("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]),Moon=createLucideIcon("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]),OctagonX=createLucideIcon("OctagonX",[["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z",key:"2d38gg"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]),Palette=createLucideIcon("Palette",[["circle",{cx:"13.5",cy:"6.5",r:".5",fill:"currentColor",key:"1okk4w"}],["circle",{cx:"17.5",cy:"10.5",r:".5",fill:"currentColor",key:"f64h9f"}],["circle",{cx:"8.5",cy:"7.5",r:".5",fill:"currentColor",key:"fotxhn"}],["circle",{cx:"6.5",cy:"12.5",r:".5",fill:"currentColor",key:"qy21gx"}],["path",{d:"M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z",key:"12rzf8"}]]),SunMoon=createLucideIcon("SunMoon",[["path",{d:"M12 8a2.83 2.83 0 0 0 4 4 4 4 0 1 1-4-4",key:"1fu5g2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.9 4.9 1.4 1.4",key:"b9915j"}],["path",{d:"m17.7 17.7 1.4 1.4",key:"qc3ed3"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.3 17.7-1.4 1.4",key:"5gca6"}],["path",{d:"m19.1 4.9-1.4 1.4",key:"wpu9u6"}]]),Sun=createLucideIcon("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),TriangleAlert=createLucideIcon("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]),X=createLucideIcon("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]); /** * @license lucide-react v0.468.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */function createContext2(A,t){const e=reactExports.createContext(t),a=A=>{const{children:t,...a}=A,r=reactExports.useMemo(()=>a,Object.values(a));return jsxRuntimeExports.jsx(e.Provider,{value:r,children:t})};return a.displayName=A+"Provider",[a,function(a){const r=reactExports.useContext(e);if(r)return r;if(void 0!==t)return t;throw new Error(`\`${a}\` must be used within \`${A}\``)}]}function createContextScope(A,t=[]){let e=[];const a=()=>{const t=e.map(A=>reactExports.createContext(A));return function(e){const a=(null==e?void 0:e[A])||t;return reactExports.useMemo(()=>({[`__scope${A}`]:{...e,[A]:a}}),[e,a])}};return a.scopeName=A,[function(t,a){const r=reactExports.createContext(a),n=e.length;e=[...e,a];const l=t=>{var e;const{scope:a,children:l,...i}=t,o=(null==(e=null==a?void 0:a[A])?void 0:e[n])||r,s=reactExports.useMemo(()=>i,Object.values(i));return jsxRuntimeExports.jsx(o.Provider,{value:s,children:l})};return l.displayName=t+"Provider",[l,function(e,l){var i;const o=(null==(i=null==l?void 0:l[A])?void 0:i[n])||r,s=reactExports.useContext(o);if(s)return s;if(void 0!==a)return a;throw new Error(`\`${e}\` must be used within \`${t}\``)}]},composeContextScopes(a,...t)]}function composeContextScopes(...A){const t=A[0];if(1===A.length)return t;const e=()=>{const e=A.map(A=>({useScope:A(),scopeName:A.scopeName}));return function(A){const a=e.reduce((t,{useScope:e,scopeName:a})=>({...t,...e(A)[`__scope${a}`]}),{});return reactExports.useMemo(()=>({[`__scope${t.scopeName}`]:a}),[a])}};return e.scopeName=t.scopeName,e}function setRef(A,t){if("function"==typeof A)return A(t);null!=A&&(A.current=t)}function composeRefs(...A){return t=>{let e=!1;const a=A.map(A=>{const a=setRef(A,t);return e||"function"!=typeof a||(e=!0),a});if(e)return()=>{for(let t=0;t{},useReactId=React$1[" useId ".trim().toString()]||(()=>{}),count$1=0;function useId(A){const[t,e]=reactExports.useState(useReactId());return useLayoutEffect2(()=>{e(A=>A??String(count$1++))},[A]),t?`radix-${t}`:""}var useInsertionEffect=React$1[" useInsertionEffect ".trim().toString()]||useLayoutEffect2;function useControllableState({prop:A,defaultProp:t,onChange:e=()=>{},caller:a}){const[r,n,l]=useUncontrolledState({defaultProp:t,onChange:e}),i=void 0!==A,o=i?A:r;{const t=reactExports.useRef(void 0!==A);reactExports.useEffect(()=>{const A=t.current;if(A!==i){}t.current=i},[i,a])}return[o,reactExports.useCallback(t=>{var e;if(i){const a=isFunction(t)?t(A):t;a!==A&&(null==(e=l.current)||e.call(l,a))}else n(t)},[i,A,n,l])]}function useUncontrolledState({defaultProp:A,onChange:t}){const[e,a]=reactExports.useState(A),r=reactExports.useRef(e),n=reactExports.useRef(t);return useInsertionEffect(()=>{n.current=t},[t]),reactExports.useEffect(()=>{var A;r.current!==e&&(null==(A=n.current)||A.call(n,e),r.current=e)},[e,r]),[e,a,n]}function isFunction(A){return"function"==typeof A}var reactDom={exports:{}},reactDom_production={},hasRequiredReactDom_production,hasRequiredReactDom;function requireReactDom_production(){if(hasRequiredReactDom_production)return reactDom_production;hasRequiredReactDom_production=1;var A=requireReact();function t(A){var t="https://react.dev/errors/"+A;if(1{const{children:a,...r}=A,n=reactExports.Children.toArray(a),l=n.find(isSlottable$1);if(l){const A=l.props.children,a=n.map(t=>t===l?reactExports.Children.count(A)>1?reactExports.Children.only(null):reactExports.isValidElement(A)?A.props.children:null:t);return jsxRuntimeExports.jsx(t,{...r,ref:e,children:reactExports.isValidElement(A)?reactExports.cloneElement(A,void 0,a):null})}return jsxRuntimeExports.jsx(t,{...r,ref:e,children:a})});return e.displayName=`${A}.Slot`,e}function createSlotClone$1(A){const t=reactExports.forwardRef((A,t)=>{const{children:e,...a}=A;if(reactExports.isValidElement(e)){const A=getElementRef$2(e),r=mergeProps$1(a,e.props);return e.type!==reactExports.Fragment&&(r.ref=t?composeRefs(t,A):A),reactExports.cloneElement(e,r)}return reactExports.Children.count(e)>1?reactExports.Children.only(null):null});return t.displayName=`${A}.SlotClone`,t}var SLOTTABLE_IDENTIFIER$1=Symbol("radix.slottable");function createSlottable(A){const t=({children:A})=>jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment,{children:A});return t.displayName=`${A}.Slottable`,t.__radixId=SLOTTABLE_IDENTIFIER$1,t}function isSlottable$1(A){return reactExports.isValidElement(A)&&"function"==typeof A.type&&"__radixId"in A.type&&A.type.__radixId===SLOTTABLE_IDENTIFIER$1}function mergeProps$1(A,t){const e={...t};for(const a in t){const r=A[a],n=t[a];/^on[A-Z]/.test(a)?r&&n?e[a]=(...A)=>{const t=n(...A);return r(...A),t}:r&&(e[a]=r):"style"===a?e[a]={...r,...n}:"className"===a&&(e[a]=[r,n].filter(Boolean).join(" "))}return{...A,...e}}function getElementRef$2(A){var t,e;let a=null==(t=Object.getOwnPropertyDescriptor(A.props,"ref"))?void 0:t.get,r=a&&"isReactWarning"in a&&a.isReactWarning;return r?A.ref:(a=null==(e=Object.getOwnPropertyDescriptor(A,"ref"))?void 0:e.get,r=a&&"isReactWarning"in a&&a.isReactWarning,r?A.props.ref:A.props.ref||A.ref)}var NODES=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],Primitive=NODES.reduce((A,t)=>{const e=createSlot$1(`Primitive.${t}`),a=reactExports.forwardRef((A,a)=>{const{asChild:r,...n}=A,l=r?e:t;return"undefined"!=typeof window&&(window[Symbol.for("radix-ui")]=!0),jsxRuntimeExports.jsx(l,{...n,ref:a})});return a.displayName=`Primitive.${t}`,{...A,[t]:a}},{});function dispatchDiscreteCustomEvent(A,t){A&&reactDomExports.flushSync(()=>A.dispatchEvent(t))}function useCallbackRef$1(A){const t=reactExports.useRef(A);return reactExports.useEffect(()=>{t.current=A}),reactExports.useMemo(()=>(...A)=>{var e;return null==(e=t.current)?void 0:e.call(t,...A)},[])}function useEscapeKeydown(A,t=(null==globalThis?void 0:globalThis.document)){const e=useCallbackRef$1(A);reactExports.useEffect(()=>{const A=A=>{"Escape"===A.key&&e(A)};return t.addEventListener("keydown",A,{capture:!0}),()=>t.removeEventListener("keydown",A,{capture:!0})},[e,t])}var DISMISSABLE_LAYER_NAME="DismissableLayer",CONTEXT_UPDATE="dismissableLayer.update",POINTER_DOWN_OUTSIDE="dismissableLayer.pointerDownOutside",FOCUS_OUTSIDE="dismissableLayer.focusOutside",originalBodyPointerEvents,DismissableLayerContext=reactExports.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),DismissableLayer=reactExports.forwardRef((A,t)=>{const{disableOutsidePointerEvents:e=!1,onEscapeKeyDown:a,onPointerDownOutside:r,onFocusOutside:n,onInteractOutside:l,onDismiss:i,...o}=A,s=reactExports.useContext(DismissableLayerContext),[p,c]=reactExports.useState(null),u=(null==p?void 0:p.ownerDocument)??(null==globalThis?void 0:globalThis.document),[,d]=reactExports.useState({}),h=useComposedRefs(t,A=>c(A)),S=Array.from(s.layers),[f]=[...s.layersWithOutsidePointerEventsDisabled].slice(-1),m=S.indexOf(f),L=p?S.indexOf(p):-1,W=s.layersWithOutsidePointerEventsDisabled.size>0,g=L>=m,y=usePointerDownOutside(A=>{const t=A.target,e=[...s.branches].some(A=>A.contains(t));g&&!e&&(null==r||r(A),null==l||l(A),A.defaultPrevented||null==i||i())},u),v=useFocusOutside(A=>{const t=A.target;[...s.branches].some(A=>A.contains(t))||(null==n||n(A),null==l||l(A),A.defaultPrevented||null==i||i())},u);return useEscapeKeydown(A=>{L===s.layers.size-1&&(null==a||a(A),!A.defaultPrevented&&i&&(A.preventDefault(),i()))},u),reactExports.useEffect(()=>{if(p)return e&&(0===s.layersWithOutsidePointerEventsDisabled.size&&(originalBodyPointerEvents=u.body.style.pointerEvents,u.body.style.pointerEvents="none"),s.layersWithOutsidePointerEventsDisabled.add(p)),s.layers.add(p),dispatchUpdate(),()=>{e&&1===s.layersWithOutsidePointerEventsDisabled.size&&(u.body.style.pointerEvents=originalBodyPointerEvents)}},[p,u,e,s]),reactExports.useEffect(()=>()=>{p&&(s.layers.delete(p),s.layersWithOutsidePointerEventsDisabled.delete(p),dispatchUpdate())},[p,s]),reactExports.useEffect(()=>{const A=()=>d({});return document.addEventListener(CONTEXT_UPDATE,A),()=>document.removeEventListener(CONTEXT_UPDATE,A)},[]),jsxRuntimeExports.jsx(Primitive.div,{...o,ref:h,style:{pointerEvents:W?g?"auto":"none":void 0,...A.style},onFocusCapture:composeEventHandlers(A.onFocusCapture,v.onFocusCapture),onBlurCapture:composeEventHandlers(A.onBlurCapture,v.onBlurCapture),onPointerDownCapture:composeEventHandlers(A.onPointerDownCapture,y.onPointerDownCapture)})});DismissableLayer.displayName=DISMISSABLE_LAYER_NAME;var BRANCH_NAME="DismissableLayerBranch",DismissableLayerBranch=reactExports.forwardRef((A,t)=>{const e=reactExports.useContext(DismissableLayerContext),a=reactExports.useRef(null),r=useComposedRefs(t,a);return reactExports.useEffect(()=>{const A=a.current;if(A)return e.branches.add(A),()=>{e.branches.delete(A)}},[e.branches]),jsxRuntimeExports.jsx(Primitive.div,{...A,ref:r})});function usePointerDownOutside(A,t=(null==globalThis?void 0:globalThis.document)){const e=useCallbackRef$1(A),a=reactExports.useRef(!1),r=reactExports.useRef(()=>{});return reactExports.useEffect(()=>{const A=A=>{if(A.target&&!a.current){let a=function(){handleAndDispatchCustomEvent(POINTER_DOWN_OUTSIDE,e,n,{discrete:!0})};const n={originalEvent:A};"touch"===A.pointerType?(t.removeEventListener("click",r.current),r.current=a,t.addEventListener("click",r.current,{once:!0})):a()}else t.removeEventListener("click",r.current);a.current=!1},n=window.setTimeout(()=>{t.addEventListener("pointerdown",A)},0);return()=>{window.clearTimeout(n),t.removeEventListener("pointerdown",A),t.removeEventListener("click",r.current)}},[t,e]),{onPointerDownCapture:()=>a.current=!0}}function useFocusOutside(A,t=(null==globalThis?void 0:globalThis.document)){const e=useCallbackRef$1(A),a=reactExports.useRef(!1);return reactExports.useEffect(()=>{const A=A=>{if(A.target&&!a.current){handleAndDispatchCustomEvent(FOCUS_OUTSIDE,e,{originalEvent:A},{discrete:!1})}};return t.addEventListener("focusin",A),()=>t.removeEventListener("focusin",A)},[t,e]),{onFocusCapture:()=>a.current=!0,onBlurCapture:()=>a.current=!1}}function dispatchUpdate(){const A=new CustomEvent(CONTEXT_UPDATE);document.dispatchEvent(A)}function handleAndDispatchCustomEvent(A,t,e,{discrete:a}){const r=e.originalEvent.target,n=new CustomEvent(A,{bubbles:!1,cancelable:!0,detail:e});t&&r.addEventListener(A,t,{once:!0}),a?dispatchDiscreteCustomEvent(r,n):r.dispatchEvent(n)}DismissableLayerBranch.displayName=BRANCH_NAME;var AUTOFOCUS_ON_MOUNT="focusScope.autoFocusOnMount",AUTOFOCUS_ON_UNMOUNT="focusScope.autoFocusOnUnmount",EVENT_OPTIONS$1={bubbles:!1,cancelable:!0},FOCUS_SCOPE_NAME="FocusScope",FocusScope=reactExports.forwardRef((A,t)=>{const{loop:e=!1,trapped:a=!1,onMountAutoFocus:r,onUnmountAutoFocus:n,...l}=A,[i,o]=reactExports.useState(null),s=useCallbackRef$1(r),p=useCallbackRef$1(n),c=reactExports.useRef(null),u=useComposedRefs(t,A=>o(A)),d=reactExports.useRef({paused:!1,pause(){this.paused=!0},resume(){this.paused=!1}}).current;reactExports.useEffect(()=>{if(a){let A=function(A){if(d.paused||!i)return;const t=A.target;i.contains(t)?c.current=t:focus(c.current,{select:!0})},t=function(A){if(d.paused||!i)return;const t=A.relatedTarget;null!==t&&(i.contains(t)||focus(c.current,{select:!0}))},e=function(A){if(document.activeElement===document.body)for(const t of A)t.removedNodes.length>0&&focus(i)};document.addEventListener("focusin",A),document.addEventListener("focusout",t);const a=new MutationObserver(e);return i&&a.observe(i,{childList:!0,subtree:!0}),()=>{document.removeEventListener("focusin",A),document.removeEventListener("focusout",t),a.disconnect()}}},[a,i,d.paused]),reactExports.useEffect(()=>{if(i){focusScopesStack.add(d);const A=document.activeElement;if(!i.contains(A)){const t=new CustomEvent(AUTOFOCUS_ON_MOUNT,EVENT_OPTIONS$1);i.addEventListener(AUTOFOCUS_ON_MOUNT,s),i.dispatchEvent(t),t.defaultPrevented||(focusFirst$2(removeLinks(getTabbableCandidates(i)),{select:!0}),document.activeElement===A&&focus(i))}return()=>{i.removeEventListener(AUTOFOCUS_ON_MOUNT,s),setTimeout(()=>{const t=new CustomEvent(AUTOFOCUS_ON_UNMOUNT,EVENT_OPTIONS$1);i.addEventListener(AUTOFOCUS_ON_UNMOUNT,p),i.dispatchEvent(t),t.defaultPrevented||focus(A??document.body,{select:!0}),i.removeEventListener(AUTOFOCUS_ON_UNMOUNT,p),focusScopesStack.remove(d)},0)}}},[i,s,p,d]);const h=reactExports.useCallback(A=>{if(!e&&!a)return;if(d.paused)return;const t="Tab"===A.key&&!A.altKey&&!A.ctrlKey&&!A.metaKey,r=document.activeElement;if(t&&r){const t=A.currentTarget,[a,n]=getTabbableEdges(t);a&&n?A.shiftKey||r!==n?A.shiftKey&&r===a&&(A.preventDefault(),e&&focus(n,{select:!0})):(A.preventDefault(),e&&focus(a,{select:!0})):r===t&&A.preventDefault()}},[e,a,d.paused]);return jsxRuntimeExports.jsx(Primitive.div,{tabIndex:-1,...l,ref:u,onKeyDown:h})});function focusFirst$2(A,{select:t=!1}={}){const e=document.activeElement;for(const a of A)if(focus(a,{select:t}),document.activeElement!==e)return}function getTabbableEdges(A){const t=getTabbableCandidates(A);return[findVisible(t,A),findVisible(t.reverse(),A)]}function getTabbableCandidates(A){const t=[],e=document.createTreeWalker(A,NodeFilter.SHOW_ELEMENT,{acceptNode:A=>{const t="INPUT"===A.tagName&&"hidden"===A.type;return A.disabled||A.hidden||t?NodeFilter.FILTER_SKIP:A.tabIndex>=0?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;e.nextNode();)t.push(e.currentNode);return t}function findVisible(A,t){for(const e of A)if(!isHidden(e,{upTo:t}))return e}function isHidden(A,{upTo:t}){if("hidden"===getComputedStyle(A).visibility)return!0;for(;A;){if(void 0!==t&&A===t)return!1;if("none"===getComputedStyle(A).display)return!0;A=A.parentElement}return!1}function isSelectableInput(A){return A instanceof HTMLInputElement&&"select"in A}function focus(A,{select:t=!1}={}){if(A&&A.focus){const e=document.activeElement;A.focus({preventScroll:!0}),A!==e&&isSelectableInput(A)&&t&&A.select()}}FocusScope.displayName=FOCUS_SCOPE_NAME;var focusScopesStack=createFocusScopesStack();function createFocusScopesStack(){let A=[];return{add(t){const e=A[0];t!==e&&(null==e||e.pause()),A=arrayRemove(A,t),A.unshift(t)},remove(t){var e;A=arrayRemove(A,t),null==(e=A[0])||e.resume()}}}function arrayRemove(A,t){const e=[...A],a=e.indexOf(t);return-1!==a&&e.splice(a,1),e}function removeLinks(A){return A.filter(A=>"A"!==A.tagName)}var PORTAL_NAME$4="Portal",Portal$2=reactExports.forwardRef((A,t)=>{var e;const{container:a,...r}=A,[n,l]=reactExports.useState(!1);useLayoutEffect2(()=>l(!0),[]);const i=a||n&&(null==(e=null==globalThis?void 0:globalThis.document)?void 0:e.body);return i?ReactDOM$1.createPortal(jsxRuntimeExports.jsx(Primitive.div,{...r,ref:t}),i):null});function useStateMachine(A,t){return reactExports.useReducer((A,e)=>t[A][e]??A,A)}Portal$2.displayName=PORTAL_NAME$4;var Presence=A=>{const{present:t,children:e}=A,a=usePresence(t),r="function"==typeof e?e({present:a.isPresent}):reactExports.Children.only(e),n=useComposedRefs(a.ref,getElementRef$1(r));return"function"==typeof e||a.isPresent?reactExports.cloneElement(r,{ref:n}):null};function usePresence(A){const[t,e]=reactExports.useState(),a=reactExports.useRef(null),r=reactExports.useRef(A),n=reactExports.useRef("none"),l=A?"mounted":"unmounted",[i,o]=useStateMachine(l,{mounted:{UNMOUNT:"unmounted",ANIMATION_OUT:"unmountSuspended"},unmountSuspended:{MOUNT:"mounted",ANIMATION_END:"unmounted"},unmounted:{MOUNT:"mounted"}});return reactExports.useEffect(()=>{const A=getAnimationName(a.current);n.current="mounted"===i?A:"none"},[i]),useLayoutEffect2(()=>{const t=a.current,e=r.current;if(e!==A){const a=n.current,l=getAnimationName(t);if(A)o("MOUNT");else if("none"===l||"none"===(null==t?void 0:t.display))o("UNMOUNT");else{o(e&&a!==l?"ANIMATION_OUT":"UNMOUNT")}r.current=A}},[A,o]),useLayoutEffect2(()=>{if(t){let A;const e=t.ownerDocument.defaultView??window,l=n=>{const l=getAnimationName(a.current).includes(CSS.escape(n.animationName));if(n.target===t&&l&&(o("ANIMATION_END"),!r.current)){const a=t.style.animationFillMode;t.style.animationFillMode="forwards",A=e.setTimeout(()=>{"forwards"===t.style.animationFillMode&&(t.style.animationFillMode=a)})}},i=A=>{A.target===t&&(n.current=getAnimationName(a.current))};return t.addEventListener("animationstart",i),t.addEventListener("animationcancel",l),t.addEventListener("animationend",l),()=>{e.clearTimeout(A),t.removeEventListener("animationstart",i),t.removeEventListener("animationcancel",l),t.removeEventListener("animationend",l)}}o("ANIMATION_END")},[t,o]),{isPresent:["mounted","unmountSuspended"].includes(i),ref:reactExports.useCallback(A=>{a.current=A?getComputedStyle(A):null,e(A)},[])}}function getAnimationName(A){return(null==A?void 0:A.animationName)||"none"}function getElementRef$1(A){var t,e;let a=null==(t=Object.getOwnPropertyDescriptor(A.props,"ref"))?void 0:t.get,r=a&&"isReactWarning"in a&&a.isReactWarning;return r?A.ref:(a=null==(e=Object.getOwnPropertyDescriptor(A,"ref"))?void 0:e.get,r=a&&"isReactWarning"in a&&a.isReactWarning,r?A.props.ref:A.props.ref||A.ref)}Presence.displayName="Presence";var count=0;function useFocusGuards(){reactExports.useEffect(()=>{const A=document.querySelectorAll("[data-radix-focus-guard]");return document.body.insertAdjacentElement("afterbegin",A[0]??createFocusGuard()),document.body.insertAdjacentElement("beforeend",A[1]??createFocusGuard()),count++,()=>{1===count&&document.querySelectorAll("[data-radix-focus-guard]").forEach(A=>A.remove()),count--}},[])}function createFocusGuard(){const A=document.createElement("span");return A.setAttribute("data-radix-focus-guard",""),A.tabIndex=0,A.style.outline="none",A.style.opacity="0",A.style.position="fixed",A.style.pointerEvents="none",A}var __assign$1=function(){return __assign$1=Object.assign||function(A){for(var t,e=1,a=arguments.length;e