Showing preview only (6,864K chars total). Download the full file or copy to clipboard to get everything.
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<<EOF" >> $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<<EOF" >> $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)
<h1 align="center">Fast Note Sync Service</h1>
<p align="center">
<a href="https://github.com/haierkeys/fast-note-sync-service/releases"><img src="https://img.shields.io/github/release/haierkeys/fast-note-sync-service?style=flat-square" alt="release"></a>
<a href="https://github.com/haierkeys/fast-note-sync-service/releases"><img src="https://img.shields.io/github/v/tag/haierkeys/fast-note-sync-service?label=release-alpha&style=flat-square" alt="alpha-release"></a>
<a href="https://github.com/haierkeys/fast-note-sync-service/blob/master/LICENSE"><img src="https://img.shields.io/github/license/haierkeys/fast-note-sync-service?style=flat-square" alt="license"></a>
<img src="https://img.shields.io/badge/Language-Go-00ADD8?style=flat-square" alt="Go">
</p>
<p align="center">
<strong>High-performance, low-latency note syncing, online management, and remote REST API service platform</strong>
<br>
<em>Built with Golang + Websocket + React</em>
</p>
<p align="center">
Data provision requires the use of the client plugin: <a href="https://github.com/haierkeys/obsidian-fast-note-sync">Obsidian Fast Note Sync Plugin</a>
</p>
<div align="center">
<div align="center">
<a href="/docs/images/vault.png"><img src="/docs/images/vault.png" alt="fast-note-sync-service-preview" width="400" /></a>
<a href="/docs/images/attach.png"><img src="/docs/images/attach.png" alt="fast-note-sync-service-preview" width="400" /></a>
</div>
<div align="center">
<a href="/docs/images/note.png"><img src="/docs/images/note.png" alt="fast-note-sync-service-preview" width="400" /></a>
<a href="/docs/images/setting.png"><img src="/docs/images/setting.png" alt="fast-note-sync-service-preview" width="400" /></a>
</div>
</div>
---
## 🎯 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* |
|--------------------------------------------------------------------------------------------------|----|------------------------------------------------|
| [<img src="/docs/images/kofi.png" alt="BuyMeACoffee" height="150">](https://ko-fi.com/haierkeys) | OR | <img src="/docs/images/wxds.png" height="150"> |
- Supported List:
- <a href="https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/Support.en.md">Support.en.md</a>
- <a href="https://cnb.cool/haierkeys/fast-note-sync-service/-/blob/master/docs/Support.en.md">Support.en.md (cnb.cool Mirror)</a>
## ⏱️ 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://<Your Server IP or Domain>:<Port>/api/mcp/sse`
- **Authentication Header**: `Authorization: Bearer <Your API Token>` (Obtained from “Copy API Configuration” in the WebGUI).
- **Optional Header**: `X-Default-Vault-Name: <Vault Name>` (Identifies the default vault for MCP operations; utilized if the `vault` parameter is unspecified during a tool call)
- **Optional Header**: `X-Client: <Client Type>` (Relates to the client type connecting the MCP, e.g., Cherry Studio / OpenClaw)
- **Optional Header**: `X-Client-Version: <Client Version>` (Pertains to the client's actual version interfacing with the MCP, e.g., 1.1)
- **Optional Header**: `X-Client-Name: <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 `<ServerIP>`, `<Port>`, `<Token>`, and `<VaultName>` with your proper contextual details)*
```json
{
"mcpServers": {
"fns": {
"url": "http://<ServerIP>:<Port>/api/mcp/sse",
"type": "sse",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer <Token>",
"X-Default-Vault-Name": "<VaultName>",
"X-Client": "<Client>",
"X-Client-Version": "<ClientVersion>",
"X-Client-Name": "<ClientName>"
}
}
}
}
```
## 🔗 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 <username> -p <password> [-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 <haierkeys@gmail.com>
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 <haierkeys@gmail.com>"
LABEL org.opencontainers.image.title="Fast Note Sync Service"
LABEL org.opencontainers.image.created=${BUILD_DATE}
LABEL org.opencontainers.image.authors="HaierKeys <haierkeys@gmail.com>"
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=<vault>&path=<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=<vault>&path=<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=<vault>&path=<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=<vault>&path=<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=<vault>&path=<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=<vault>&path=<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=<vault>&path=<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=<vault>&path=<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=<name>&depth=<optional>
```
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)
<h1 align="center">Fast Note Sync Service</h1>
<p align="center">
<a href="https://github.com/haierkeys/fast-note-sync-service/releases"><img src="https://img.shields.io/github/release/haierkeys/fast-note-sync-service?style=flat-square" alt="release"></a>
<a href="https://github.com/haierkeys/fast-note-sync-service/releases"><img src="https://img.shields.io/github/v/tag/haierkeys/fast-note-sync-service?label=release-alpha&style=flat-square" alt="alpha-release"></a>
<a href="https://github.com/haierkeys/fast-note-sync-service/blob/master/LICENSE"><img src="https://img.shields.io/github/license/haierkeys/fast-note-sync-service?style=flat-square" alt="license"></a>
<img src="https://img.shields.io/badge/Language-Go-00ADD8?style=flat-square" alt="Go">
</p>
<p align="center">
<strong>高性能・低遅延なノート同期、オンライン管理、リモートREST APIサービスプラットフォーム</strong>
<br>
<em>Golang + Websocket + Reactで構築</em>
</p>
<p align="center">
データを利用するには、クライアントプラグインを併用する必要があります:<a href="https://github.com/haierkeys/obsidian-fast-note-sync">Obsidian Fast Note Sync Plugin</a>
</p>
<div align="center">
<div align="center">
<a href="/docs/images/vault.png"><img src="/docs/images/vault.png" alt="fast-note-sync-service-preview" width="400" /></a>
<a href="/docs/images/attach.png"><img src="/docs/images/attach.png" alt="fast-note-sync-service-preview" width="400" /></a>
</div>
<div align="center">
<a href="/docs/images/note.png"><img src="/docs/images/note.png" alt="fast-note-sync-service-preview" width="400" /></a>
<a href="/docs/images/setting.png"><img src="/docs/images/setting.png" alt="fast-note-sync-service-preview" width="400" /></a>
</div>
</div>
---
## 🎯 コア機能
* **🧰 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コード *中国地域* |
|--------------------------------------------------------------------------------------------------|----|------------------------------------------------|
| [<img src="/docs/images/kofi.png" alt="BuyMeACoffee" height="150">](https://ko-fi.com/haierkeys) | または | <img src="/docs/images/wxds.png" height="150"> |
- サポートリスト:
- <a href="https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/Support.ja.md">Support.ja.md</a>
- <a href="https://cnb.cool/haierkeys/fast-note-sync-service/-/blob/master/docs/Support.ja.md">Support.ja.md (cnb.cool ミラー)</a>
## ⏱️ 更新履歴 (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クライアント設定にて、以下の記述をご参考ください:
*(注: `<ServerIP>`、`<Port>`、`<Token>`、および `<VaultName>` を各自の実際の情報に置き換えてください)*
```json
{
"mcpServers": {
"fns": {
"url": "http://<ServerIP>:<Port>/api/mcp/sse",
"type": "sse",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer <Token>",
"X-Default-Vault-Name": "<VaultName>",
"X-Client": "<Client>",
"X-Client-Version": "<ClientVersion>",
"X-Client-Name": "<ClientName>"
}
}
}
}
```
## 🔗 クライアントとクライアントプラグイン
* 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)
<h1 align="center">Fast Note Sync Service</h1>
<p align="center">
<a href="https://github.com/haierkeys/fast-note-sync-service/releases"><img src="https://img.shields.io/github/release/haierkeys/fast-note-sync-service?style=flat-square" alt="release"></a>
<a href="https://github.com/haierkeys/fast-note-sync-service/releases"><img src="https://img.shields.io/github/v/tag/haierkeys/fast-note-sync-service?label=release-alpha&style=flat-square" alt="alpha-release"></a>
<a href="https://github.com/haierkeys/fast-note-sync-service/blob/master/LICENSE"><img src="https://img.shields.io/github/license/haierkeys/fast-note-sync-service?style=flat-square" alt="license"></a>
<img src="https://img.shields.io/badge/Language-Go-00ADD8?style=flat-square" alt="Go">
</p>
<p align="center">
<strong>고성능, 저지연의 노트 동기화, 온라인 관리, 원격 REST API 서비스 플랫폼</strong>
<br>
<em>Golang + Websocket + React 기반으로 구축</em>
</p>
<p align="center">
데이터를 사용하려면 클라이언트 플러그인과 함께 사용해야 합니다: <a href="https://github.com/haierkeys/obsidian-fast-note-sync">Obsidian Fast Note Sync Plugin</a>
</p>
<div align="center">
<div align="center">
<a href="/docs/images/vault.png"><img src="/docs/images/vault.png" alt="fast-note-sync-service-preview" width="400" /></a>
<a href="/docs/images/attach.png"><img src="/docs/images/attach.png" alt="fast-note-sync-service-preview" width="400" /></a>
</div>
<div align="center">
<a href="/docs/images/note.png"><img src="/docs/images/note.png" alt="fast-note-sync-service-preview" width="400" /></a>
<a href="/docs/images/setting.png"><img src="/docs/images/setting.png" alt="fast-note-sync-service-preview" width="400" /></a>
</div>
</div>
---
## 🎯 핵심 기능
* **🧰 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+ 버전 필요)
* **🗑️ 휴지통 기능**:
* 노트를 삭제한 후 이가 휴지통에 자동으로 들어가는 것을 지원합니다.
* 휴지통에서 노트를 복구하는 기능을 지원합니다. (첨부 파일의 복구 기능도 지속적으로 추가될 예정입니다)
* **🚫 오프라인 동기화 모드 전략**:
* 오프라인 상태에서 노트 편집에 대해 자동으로 병합하는 것을 지원합니다. (플러그인 옵션 설정이 필요합니다)
* 오프라인에서 항목을 삭제한 경우, 재연결 후 서버
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
Showing preview only (755K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (7808 symbols across 379 files)
FILE: cmd/bootstrap.go
function init (line 16) | func init() {
function BootstrapLogger (line 41) | func BootstrapLogger() *zap.Logger {
FILE: cmd/gorm_gen/gen.go
function init (line 27) | func init() {
function SQLColumnToHumpStyle (line 40) | func SQLColumnToHumpStyle(in string) (ret string) {
function Db (line 54) | func Db(dsn string, dbType string) *gorm.DB {
function useDia (line 68) | func useDia(dsn string, dbType string) gorm.Dialector {
function getTableDefaultValueTags (line 82) | func getTableDefaultValueTags(db *gorm.DB, table string) []gen.ModelOpt {
function isPrimaryKey (line 155) | func isPrimaryKey(col gorm.ColumnType) bool {
function main (line 162) | func main() {
FILE: cmd/mfmt/main.go
function init (line 25) | func init() {
function init (line 40) | func init() {
function main (line 61) | func main() {
function sort (line 143) | func sort(imports []*ast.ImportSpec, comments map[string]string) string {
FILE: cmd/model_gen/gen.go
function main (line 13) | func main() {
FILE: cmd/reset_password.go
function init (line 19) | func init() {
FILE: cmd/root.go
function Execute (line 24) | func Execute(efs embed.FS, c string) {
FILE: cmd/run.go
type runFlags (line 20) | type runFlags struct
function init (line 27) | func init() {
FILE: cmd/run_server.go
constant DefaultShutdownTimeout (line 44) | DefaultShutdownTimeout = 30 * time.Second
type Server (line 46) | type Server struct
method GetApp (line 430) | func (s *Server) GetApp() *internalApp.App {
method GetConfig (line 436) | func (s *Server) GetConfig() *internalApp.AppConfig {
function checkSecurityConfigWithConfig (line 59) | func checkSecurityConfigWithConfig(cfg *internalApp.AppConfig, lg *zap.L...
function NewServer (line 89) | func NewServer(runEnv *runFlags) (*Server, error) {
function initScheduler (line 317) | func initScheduler(s *Server) {
function initLoggerWithConfig (line 336) | func initLoggerWithConfig(s *Server, cfg *internalApp.AppConfig) error {
function initValidatorWithLogger (line 352) | func initValidatorWithLogger(lg *zap.Logger) (*ut.UniversalTranslator, e...
function initDatabaseWithConfig (line 388) | func initDatabaseWithConfig(cfg *internalApp.AppConfig, lg *zap.Logger) ...
function initStorageWithConfig (line 404) | func initStorageWithConfig(cfg *internalApp.AppConfig) error {
FILE: cmd/upgrade.go
function init (line 81) | func init() {
FILE: cmd/version.go
function init (line 19) | func init() {
FILE: docs/docs.go
constant docTemplate (line 6) | docTemplate = `{
function init (line 8129) | func init() {
FILE: frontend/assets/auth-form-BjZ9qVzL.js
function k (line 1) | function k({onSuccess:k,registerIsEnable:P=!0}){const{t:I}=s(),{isLoadin...
FILE: frontend/assets/badge-C63ATniC.js
function o (line 1) | function o({className:t,variant:o,...a}){return r.jsx("div",{className:e...
FILE: frontend/assets/canvas-viewer-Cxwbo1vR.js
function w (line 13) | function w(){const e=t.useCallback(()=>{const e=localStorage.getItem("to...
function j (line 13) | function j(e){if(e)return v[e]??e}
function R (line 13) | function R(e,t){const n=e.replace("#","");if(/^[0-9a-fA-F]{6}$/.test(n))...
function b (line 13) | function b(e,t){const n=e.x+e.width/2,s=e.y+e.height/2;switch(t){case"to...
function C (line 13) | function C(e,t){switch(e){case"top":return{dx:0,dy:-t};case"bottom":retu...
function N (line 13) | function N({edge:e,nodeMap:t}){const n=t.get(e.fromNode),s=t.get(e.toNod...
function $ (line 13) | function $({node:e,onNodeClick:t,onWikiLinkClick:n,isDragRef:s}){var a;c...
function I (line 13) | function I({nodes:e,edges:n,viewport:s,onViewportChange:a,onNodeClick:r,...
function M (line 13) | function M(e,t,n){if(0===e.length||0===t||0===n)return{x:0,y:0,zoom:1};l...
function L (line 13) | function L({vault:e,note:s,onBack:a,onWikiLinkClick:r,isRecycle:o}){var ...
FILE: frontend/assets/checkbox-DhTHgmeh.js
function m (line 1) | function m(t){const{__scopeCheckbox:o,checked:n,children:s,defaultChecke...
function j (line 1) | function j(e){return"function"==typeof e}
function w (line 1) | function w(e){return"indeterminate"===e}
function R (line 1) | function R(e){return w(e)?"indeterminate":e?"checked":"unchecked"}
FILE: frontend/assets/file-manager-Bz0QGSbU.js
function G (line 13) | function G({file:e,url:o,onClose:i}){var c;const{t:d}=s(),m=(null==(c=e....
function q (line 13) | function q(e){if(0===e)return"0 B";const s=Math.floor(Math.log(e)/Math.l...
function U (line 13) | function U({vault:e,vaults:n,onVaultChange:m,isRecycle:h=!1,page:u,setPa...
function J (line 13) | function J({vault:e,onVaultChange:l,onNavigateToVaults:n,isRecycle:o=!1}...
FILE: frontend/assets/font-loader-CIrh3KnA.js
function _mergeNamespaces (line 1) | function _mergeNamespaces(A,t){for(var e=0;e<t.length;e++){const a=t[e];...
function getDefaultExportFromCjs (line 1) | function getDefaultExportFromCjs(A){return A&&A.__esModule&&Object.proto...
function t (line 1) | function t(A){if(A.ep)return;A.ep=!0;const t=function(A){const t={};retu...
function requireReactJsxRuntime_production (line 1) | function requireReactJsxRuntime_production(){if(hasRequiredReactJsxRunti...
function requireJsxRuntime (line 1) | function requireJsxRuntime(){return hasRequiredJsxRuntime||(hasRequiredJ...
function r (line 1) | function r(A){const t=new Event("vite:preloadError",{cancelable:!0});if(...
function requireReact_production (line 1) | function requireReact_production(){if(hasRequiredReact_production)return...
function requireReact (line 1) | function requireReact(){return hasRequiredReact||(hasRequiredReact=1,rea...
method init (line 1) | init(A){setDefaults(A.options.react),setI18n(A)}
class ReportNamespaces (line 1) | class ReportNamespaces{constructor(){this.usedNamespaces={}}addUsedNames...
method constructor (line 1) | constructor(){this.usedNamespaces={}}
method addUsedNamespaces (line 1) | addUsedNamespaces(A){A.forEach(A=>{this.usedNamespaces[A]||(this.usedN...
method getUsedNamespaces (line 1) | getUsedNamespaces(){return Object.keys(this.usedNamespaces)}
function getBrowserLang (line 1) | function getBrowserLang(A){if(A)return localStorage.getItem(A)||navigato...
class RegExpCache (line 1) | class RegExpCache{constructor(A){this.capacity=A,this.regExpMap=new Map,...
method constructor (line 1) | constructor(A){this.capacity=A,this.regExpMap=new Map,this.regExpQueue...
method getRegExp (line 1) | getRegExp(A){const t=this.regExpMap.get(A);if(void 0!==t)return t;cons...
method log (line 1) | log(A){this.output("log",A)}
method warn (line 1) | warn(A){this.output("warn",A)}
method error (line 1) | error(A){this.output("error",A)}
method output (line 1) | output(A,t){var e,a;null==(a=null==(e=null==console?void 0:console[A])?v...
class Logger (line 1) | class Logger{constructor(A){let t=arguments.length>1&&void 0!==arguments...
method constructor (line 1) | constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?argumen...
method init (line 1) | init(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{...
method log (line 1) | log(){for(var A=arguments.length,t=new Array(A),e=0;e<A;e++)t[e]=argum...
method warn (line 1) | warn(){for(var A=arguments.length,t=new Array(A),e=0;e<A;e++)t[e]=argu...
method error (line 1) | error(){for(var A=arguments.length,t=new Array(A),e=0;e<A;e++)t[e]=arg...
method deprecate (line 1) | deprecate(){for(var A=arguments.length,t=new Array(A),e=0;e<A;e++)t[e]...
method forward (line 1) | forward(A,t,e,a){return a&&!this.debug?null:(isString(A[0])&&(A[0]=`${...
method create (line 1) | create(A){return new Logger(this.logger,{prefix:`${this.prefix}:${A}:`...
method clone (line 1) | clone(A){return(A=A||this.options).prefix=A.prefix||this.prefix,new Lo...
class EventEmitter (line 1) | class EventEmitter{constructor(){this.observers={}}on(A,t){return A.spli...
method constructor (line 1) | constructor(){this.observers={}}
method on (line 1) | on(A,t){return A.split(" ").forEach(A=>{this.observers[A]||(this.obser...
method off (line 1) | off(A,t){this.observers[A]&&(t?this.observers[A].delete(t):delete this...
method emit (line 1) | emit(A){for(var t=arguments.length,e=new Array(t>1?t-1:0),a=1;a<t;a++)...
class ResourceStore (line 1) | class ResourceStore extends EventEmitter{constructor(A){let t=arguments....
method constructor (line 1) | constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?argumen...
method addNamespaces (line 1) | addNamespaces(A){this.options.ns.indexOf(A)<0&&this.options.ns.push(A)}
method removeNamespaces (line 1) | removeNamespaces(A){const t=this.options.ns.indexOf(A);t>-1&&this.opti...
method getResource (line 1) | getResource(A,t,e){var a,r;let n=arguments.length>3&&void 0!==argument...
method addResource (line 1) | addResource(A,t,e,a){let r=arguments.length>4&&void 0!==arguments[4]?a...
method addResources (line 1) | addResources(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?ar...
method addResourceBundle (line 1) | addResourceBundle(A,t,e,a,r){let n=arguments.length>5&&void 0!==argume...
method removeResourceBundle (line 1) | removeResourceBundle(A,t){this.hasResourceBundle(A,t)&&delete this.dat...
method hasResourceBundle (line 1) | hasResourceBundle(A,t){return void 0!==this.getResource(A,t)}
method getResourceBundle (line 1) | getResourceBundle(A,t){return t||(t=this.options.defaultNS),this.getRe...
method getDataByLanguage (line 1) | getDataByLanguage(A){return this.data[A]}
method hasLanguageSomeTranslations (line 1) | hasLanguageSomeTranslations(A){const t=this.getDataByLanguage(A);retur...
method toJSON (line 1) | toJSON(){return this.data}
method addPostProcessor (line 1) | addPostProcessor(A){this.processors[A.name]=A}
method handle (line 1) | handle(A,t,e,a,r){return A.forEach(A=>{var n;t=(null==(n=this.processors...
class Translator (line 1) | class Translator extends EventEmitter{constructor(A){let t=arguments.len...
method constructor (line 1) | constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?argumen...
method changeLanguage (line 1) | changeLanguage(A){A&&(this.language=A)}
method exists (line 1) | exists(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]...
method extractFromKey (line 1) | extractFromKey(A,t){let e=void 0!==t.nsSeparator?t.nsSeparator:this.op...
method translate (line 1) | translate(A,t,e){if("object"!=typeof t&&this.options.overloadTranslati...
method extendTranslation (line 1) | extendTranslation(A,t,e,a,r){var n,l,i=this;if(null==(n=this.i18nForma...
method resolve (line 1) | resolve(A){let t,e,a,r,n,l=arguments.length>1&&void 0!==arguments[1]?a...
method isValidLookup (line 1) | isValidLookup(A){return!(void 0===A||!this.options.returnNull&&null===...
method getResource (line 1) | getResource(A,t,e){var a;let r=arguments.length>3&&void 0!==arguments[...
method getUsedParamsDetails (line 1) | getUsedParamsDetails(){let A=arguments.length>0&&void 0!==arguments[0]...
method hasDefaultValue (line 1) | static hasDefaultValue(A){const t="defaultValue";for(const e in A)if(O...
class LanguageUtil (line 1) | class LanguageUtil{constructor(A){this.options=A,this.supportedLngs=this...
method constructor (line 1) | constructor(A){this.options=A,this.supportedLngs=this.options.supporte...
method getScriptPartFromCode (line 1) | getScriptPartFromCode(A){if(!(A=getCleanedCode(A))||A.indexOf("-")<0)r...
method getLanguagePartFromCode (line 1) | getLanguagePartFromCode(A){if(!(A=getCleanedCode(A))||A.indexOf("-")<0...
method formatLanguageCode (line 1) | formatLanguageCode(A){if(isString(A)&&A.indexOf("-")>-1){let e;try{e=I...
method isSupportedCode (line 1) | isSupportedCode(A){return("languageOnly"===this.options.load||this.opt...
method getBestMatchFromCodes (line 1) | getBestMatchFromCodes(A){if(!A)return null;let t;return A.forEach(A=>{...
method getFallbackCodes (line 1) | getFallbackCodes(A,t){if(!A)return[];if("function"==typeof A&&(A=A(t))...
method toResolveHierarchy (line 1) | toResolveHierarchy(A,t){const e=this.getFallbackCodes(t||this.options....
class PluralResolver (line 1) | class PluralResolver{constructor(A){let t=arguments.length>1&&void 0!==a...
method constructor (line 1) | constructor(A){let t=arguments.length>1&&void 0!==arguments[1]?argumen...
method addRule (line 1) | addRule(A,t){this.rules[A]=t}
method clearCache (line 1) | clearCache(){this.pluralRulesCache={}}
method getRule (line 1) | getRule(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1...
method needsPlural (line 1) | needsPlural(A){let t=arguments.length>1&&void 0!==arguments[1]?argumen...
method getPluralFormsOfKey (line 1) | getPluralFormsOfKey(A,t){let e=arguments.length>2&&void 0!==arguments[...
method getSuffixes (line 1) | getSuffixes(A){let t=arguments.length>1&&void 0!==arguments[1]?argumen...
method getSuffix (line 1) | getSuffix(A,t){let e=arguments.length>2&&void 0!==arguments[2]?argumen...
class Interpolator (line 1) | class Interpolator{constructor(){var A;let t=arguments.length>0&&void 0!...
method constructor (line 1) | constructor(){var A;let t=arguments.length>0&&void 0!==arguments[0]?ar...
method init (line 1) | init(){let A=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}...
method reset (line 1) | reset(){this.options&&this.init(this.options)}
method resetRegExp (line 1) | resetRegExp(){const A=(A,t)=>(null==A?void 0:A.source)===t?(A.lastInde...
method interpolate (line 1) | interpolate(A,t,e,a){var r;let n,l,i;const o=this.options&&this.option...
method nest (line 1) | nest(A,t){let e,a,r,n=arguments.length>2&&void 0!==arguments[2]?argume...
class Formatter (line 1) | class Formatter{constructor(){let A=arguments.length>0&&void 0!==argumen...
method constructor (line 1) | constructor(){let A=arguments.length>0&&void 0!==arguments[0]?argument...
method init (line 1) | init(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{...
method add (line 1) | add(A,t){this.formats[A.toLowerCase().trim()]=t}
method addCached (line 1) | addCached(A,t){this.formats[A.toLowerCase().trim()]=createCachedFormat...
method format (line 1) | format(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?argument...
class Connector (line 1) | class Connector extends EventEmitter{constructor(A,t,e){var a,r;let n=ar...
method constructor (line 1) | constructor(A,t,e){var a,r;let n=arguments.length>3&&void 0!==argument...
method queueLoad (line 1) | queueLoad(A,t,e,a){const r={},n={},l={},i={};return A.forEach(A=>{let ...
method loaded (line 1) | loaded(A,t,e){const a=A.split("|"),r=a[0],n=a[1];t&&this.emit("failedL...
method read (line 1) | read(A,t,e){let a=arguments.length>3&&void 0!==arguments[3]?arguments[...
method prepareLoading (line 1) | prepareLoading(A,t){let e=arguments.length>2&&void 0!==arguments[2]?ar...
method load (line 1) | load(A,t,e){this.prepareLoading(A,t,{},e)}
method reload (line 1) | reload(A,t,e){this.prepareLoading(A,t,{reload:!0},e)}
method loadOne (line 1) | loadOne(A){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1...
method saveMissing (line 1) | saveMissing(A,t,e,a,r){var n,l,i,o,s;let p=arguments.length>5&&void 0!...
class I18n (line 1) | class I18n extends EventEmitter{constructor(){let A=arguments.length>0&&...
method constructor (line 1) | constructor(){let A=arguments.length>0&&void 0!==arguments[0]?argument...
method init (line 1) | init(){var A=this;let t=arguments.length>0&&void 0!==arguments[0]?argu...
method loadResources (line 1) | loadResources(A){var t,e;let a=arguments.length>1&&void 0!==arguments[...
method reloadResources (line 1) | reloadResources(A,t,e){const a=defer();return"function"==typeof A&&(e=...
method use (line 1) | use(A){if(!A)throw new Error("You are passing an undefined module! Ple...
method setResolvedLanguage (line 1) | setResolvedLanguage(A){if(A&&this.languages&&!(["cimode","dev"].indexO...
method changeLanguage (line 1) | changeLanguage(A,t){var e=this;this.isLanguageChangingTo=A;const a=def...
method getFixedT (line 1) | getFixedT(A,t,e){var a=this;const r=function(A,t){let n;if("object"!=t...
method t (line 1) | t(){for(var A,t=arguments.length,e=new Array(t),a=0;a<t;a++)e[a]=argum...
method exists (line 1) | exists(){for(var A,t=arguments.length,e=new Array(t),a=0;a<t;a++)e[a]=...
method setDefaultNamespace (line 1) | setDefaultNamespace(A){this.options.defaultNS=A}
method hasLoadedNamespace (line 1) | hasLoadedNamespace(A){let t=arguments.length>1&&void 0!==arguments[1]?...
method loadNamespaces (line 1) | loadNamespaces(A,t){const e=defer();return this.options.ns?(isString(A...
method loadLanguages (line 1) | loadLanguages(A,t){const e=defer();isString(A)&&(A=[A]);const a=this.o...
method dir (line 1) | dir(A){var t,e;if(A||(A=this.resolvedLanguage||((null==(t=this.languag...
method createInstance (line 1) | static createInstance(){return new I18n(arguments.length>0&&void 0!==a...
method cloneInstance (line 1) | cloneInstance(){let A=arguments.length>0&&void 0!==arguments[0]?argume...
method toJSON (line 1) | toJSON(){return{options:this.options,store:this.store,language:this.la...
function createContext2 (line 27) | function createContext2(A,t){const e=reactExports.createContext(t),a=A=>...
function createContextScope (line 27) | function createContextScope(A,t=[]){let e=[];const a=()=>{const t=e.map(...
function composeContextScopes (line 27) | function composeContextScopes(...A){const t=A[0];if(1===A.length)return ...
function setRef (line 27) | function setRef(A,t){if("function"==typeof A)return A(t);null!=A&&(A.cur...
function composeRefs (line 27) | function composeRefs(...A){return t=>{let e=!1;const a=A.map(A=>{const a...
function useComposedRefs (line 27) | function useComposedRefs(...A){return reactExports.useCallback(composeRe...
function composeEventHandlers (line 27) | function composeEventHandlers(A,t,{checkForDefaultPrevented:e=!0}={}){re...
function useId (line 27) | function useId(A){const[t,e]=reactExports.useState(useReactId());return ...
function useControllableState (line 27) | function useControllableState({prop:A,defaultProp:t,onChange:e=()=>{},ca...
function useUncontrolledState (line 27) | function useUncontrolledState({defaultProp:A,onChange:t}){const[e,a]=rea...
function isFunction (line 27) | function isFunction(A){return"function"==typeof A}
function requireReactDom_production (line 27) | function requireReactDom_production(){if(hasRequiredReactDom_production)...
function requireReactDom (line 27) | function requireReactDom(){if(hasRequiredReactDom)return reactDom.export...
function createSlot$1 (line 27) | function createSlot$1(A){const t=createSlotClone$1(A),e=reactExports.for...
function createSlotClone$1 (line 27) | function createSlotClone$1(A){const t=reactExports.forwardRef((A,t)=>{co...
function createSlottable (line 27) | function createSlottable(A){const t=({children:A})=>jsxRuntimeExports.js...
function isSlottable$1 (line 27) | function isSlottable$1(A){return reactExports.isValidElement(A)&&"functi...
function mergeProps$1 (line 27) | function mergeProps$1(A,t){const e={...t};for(const a in t){const r=A[a]...
function getElementRef$2 (line 27) | function getElementRef$2(A){var t,e;let a=null==(t=Object.getOwnProperty...
function dispatchDiscreteCustomEvent (line 27) | function dispatchDiscreteCustomEvent(A,t){A&&reactDomExports.flushSync((...
function useCallbackRef$1 (line 27) | function useCallbackRef$1(A){const t=reactExports.useRef(A);return react...
function useEscapeKeydown (line 27) | function useEscapeKeydown(A,t=(null==globalThis?void 0:globalThis.docume...
function usePointerDownOutside (line 27) | function usePointerDownOutside(A,t=(null==globalThis?void 0:globalThis.d...
function useFocusOutside (line 27) | function useFocusOutside(A,t=(null==globalThis?void 0:globalThis.documen...
function dispatchUpdate (line 27) | function dispatchUpdate(){const A=new CustomEvent(CONTEXT_UPDATE);docume...
function handleAndDispatchCustomEvent (line 27) | function handleAndDispatchCustomEvent(A,t,e,{discrete:a}){const r=e.orig...
method pause (line 27) | pause(){this.paused=!0}
method resume (line 27) | resume(){this.paused=!1}
function focusFirst$2 (line 27) | function focusFirst$2(A,{select:t=!1}={}){const e=document.activeElement...
function getTabbableEdges (line 27) | function getTabbableEdges(A){const t=getTabbableCandidates(A);return[fin...
function getTabbableCandidates (line 27) | function getTabbableCandidates(A){const t=[],e=document.createTreeWalker...
function findVisible (line 27) | function findVisible(A,t){for(const e of A)if(!isHidden(e,{upTo:t}))retu...
function isHidden (line 27) | function isHidden(A,{upTo:t}){if("hidden"===getComputedStyle(A).visibili...
function isSelectableInput (line 27) | function isSelectableInput(A){return A instanceof HTMLInputElement&&"sel...
function focus (line 27) | function focus(A,{select:t=!1}={}){if(A&&A.focus){const e=document.activ...
function createFocusScopesStack (line 27) | function createFocusScopesStack(){let A=[];return{add(t){const e=A[0];t!...
function arrayRemove (line 27) | function arrayRemove(A,t){const e=[...A],a=e.indexOf(t);return-1!==a&&e....
function removeLinks (line 27) | function removeLinks(A){return A.filter(A=>"A"!==A.tagName)}
function useStateMachine (line 27) | function useStateMachine(A,t){return reactExports.useReducer((A,e)=>t[A]...
function usePresence (line 27) | function usePresence(A){const[t,e]=reactExports.useState(),a=reactExport...
function getAnimationName (line 27) | function getAnimationName(A){return(null==A?void 0:A.animationName)||"no...
function getElementRef$1 (line 27) | function getElementRef$1(A){var t,e;let a=null==(t=Object.getOwnProperty...
function useFocusGuards (line 27) | function useFocusGuards(){reactExports.useEffect(()=>{const A=document.q...
function createFocusGuard (line 27) | function createFocusGuard(){const A=document.createElement("span");retur...
function __rest (line 27) | function __rest(A,t){var e={};for(var a in A)Object.prototype.hasOwnProp...
function __spreadArray (line 27) | function __spreadArray(A,t,e){if(e||2===arguments.length)for(var a,r=0,n...
function assignRef (line 27) | function assignRef(A,t){return"function"==typeof A?A(t):A&&(A.current=t),A}
function useCallbackRef (line 27) | function useCallbackRef(A,t){var e=reactExports.useState(function(){retu...
function useMergeRefs (line 27) | function useMergeRefs(A,t){var e=useCallbackRef(null,function(t){return ...
function ItoI (line 27) | function ItoI(A){return A}
function innerCreateMedium (line 27) | function innerCreateMedium(A,t){void 0===t&&(t=ItoI);var e=[],a=!1;retur...
function createSidecarMedium (line 27) | function createSidecarMedium(A){void 0===A&&(A={});var t=innerCreateMedi...
function exportSidecar (line 27) | function exportSidecar(A,t){return A.useMedium(t),SideCar$1}
function makeStyleTag (line 27) | function makeStyleTag(){if(!document)return null;var A=document.createEl...
function injectStyles (line 27) | function injectStyles(A,t){A.styleSheet?A.styleSheet.cssText=t:A.appendC...
function insertStyleTag (line 27) | function insertStyleTag(A){(document.head||document.getElementsByTagName...
function RemoveScrollSideCar (line 27) | function RemoveScrollSideCar(A){var t=reactExports.useRef([]),e=reactExp...
function getOutermostShadowParent (line 27) | function getOutermostShadowParent(A){for(var t=null;null!==A;)A instance...
function getState (line 27) | function getState(A){return A?"open":"closed"}
function r (line 27) | function r(A){var t,e,a="";if("string"==typeof A||"number"==typeof A)a+=...
function clsx (line 27) | function clsx(){for(var A,t,e=0,a="",n=arguments.length;e<n;e++)(A=argum...
function isPromiseLike (line 27) | function isPromiseLike(A){return"object"==typeof A&&null!==A&&"then"in A}
function isLazyComponent (line 27) | function isLazyComponent(A){return null!=A&&"object"==typeof A&&"$$typeo...
function createSlot (line 27) | function createSlot(A){const t=createSlotClone(A),e=reactExports.forward...
function createSlotClone (line 27) | function createSlotClone(A){const t=reactExports.forwardRef((A,t)=>{let{...
function isSlottable (line 27) | function isSlottable(A){return reactExports.isValidElement(A)&&"function...
function mergeProps (line 27) | function mergeProps(A,t){const e={...t};for(const a in t){const r=A[a],n...
function getElementRef (line 27) | function getElementRef(A){var t,e;let a=null==(t=Object.getOwnPropertyDe...
method get (line 27) | get(A){let t=e[A];return void 0!==t?t:void 0!==(t=a[A])?(r(A,t),t):void 0}
method set (line 27) | set(A,t){A in e?e[A]=t:r(A,t)}
function cn$1 (line 27) | function cn$1(...A){return twMerge(clsx(A))}
function getSystemTheme (line 27) | function getSystemTheme(){return"undefined"==typeof window?"light":windo...
function getAutoTheme (line 27) | function getAutoTheme(){const A=(new Date).getHours();return A>=18||A<6?...
function updateMetaThemeColor (line 27) | function updateMetaThemeColor(A){let t=document.querySelector('meta[name...
function applyTheme (line 27) | function applyTheme(A){const t=window.document.documentElement;t.classLi...
function createJSONStorage (line 27) | function createJSONStorage(A,t){let e;try{e=A()}catch(a){return}return{g...
method catch (line 27) | catch(A){return this}
method then (line 27) | then(A){return this}
function useStore (line 27) | function useStore(A,t=identity){const e=React.useSyncExternalStore(A.sub...
function __insertCSS (line 27) | function __insertCSS(A){if("undefined"==typeof document)return;let t=doc...
class Observer (line 27) | class Observer{constructor(){this.subscribe=A=>(this.subscribers.push(A)...
method constructor (line 27) | constructor(){this.subscribe=A=>(this.subscribers.push(A),()=>{const t...
function isAction (line 27) | function isAction(A){return void 0!==A.label}
function cn (line 27) | function cn(...A){return A.filter(Boolean).join(" ")}
function getDefaultSwipeDirections (line 27) | function getDefaultSwipeDirections(A){const[t,e]=A.split("-"),a=[];retur...
function getDocumentDirection (line 27) | function getDocumentDirection(){if("undefined"==typeof window)return"ltr...
function assignOffset (line 27) | function assignOffset(A,t){const e={};return[A,t].forEach((A,t)=>{const ...
function requireScheduler_production (line 27) | function requireScheduler_production(){return hasRequiredScheduler_produ...
function requireScheduler (line 27) | function requireScheduler(){return hasRequiredScheduler||(hasRequiredSch...
function requireReactDomClient_production (line 36) | function requireReactDomClient_production(){if(hasRequiredReactDomClient...
function requireClient (line 36) | function requireClient(){if(hasRequiredClient)return client.exports;retu...
function addCacheBuster (line 36) | function addCacheBuster(A){const t=A.includes("?")?"&":"?";return`${A}${...
function buildApiHeaders (line 36) | function buildApiHeaders({token:A,includeContentType:t=!0,includeDomain:...
function __extends (line 50) | function __extends(A,t){if("function"!=typeof t&&null!==t)throw new Type...
function __awaiter (line 50) | function __awaiter(A,t,e,a){return new(e||(e=Promise))(function(t,r){fun...
function __generator (line 50) | function __generator(A,t){var e,a,r,n,l={label:0,sent:function(){if(1&r[...
function createCommonjsModule (line 50) | function createCommonjsModule(A,t){return A(t={exports:{}},t.exports),t....
function createTag (line 50) | function createTag(A){return document.createElement(A)}
function extendPrototype (line 50) | function extendPrototype(A,t){var e,a,r=A.length;for(e=0;e<r;e+=1)for(va...
function getDescriptor (line 50) | function getDescriptor(A,t){return Object.getOwnPropertyDescriptor(A,t)}
function createProxyFunction (line 50) | function createProxyFunction(A){function t(){}return t.prototype=A,t}
function A (line 50) | function A(A){this.audios=[],this.audioFactory=A,this._volume=1,this._is...
function A (line 50) | function A(A,t){var e,a=0,r=[];switch(A){case"int16":case"uint8c":e=1;br...
function createSizedArray (line 50) | function createSizedArray(A){return Array.apply(null,{length:A})}
function _typeof$6 (line 50) | function _typeof$6(A){return(_typeof$6="function"==typeof Symbol&&"symbo...
function styleDiv (line 50) | function styleDiv(A){A.style.position="absolute",A.style.top=0,A.style.l...
function BMEnterFrameEvent (line 50) | function BMEnterFrameEvent(A,t,e,a){this.type=A,this.currentTime=t,this....
function BMCompleteEvent (line 50) | function BMCompleteEvent(A,t){this.type=A,this.direction=t<0?-1:1}
function BMCompleteLoopEvent (line 50) | function BMCompleteLoopEvent(A,t,e,a){this.type=A,this.currentLoop=e,thi...
function BMSegmentStartEvent (line 50) | function BMSegmentStartEvent(A,t,e){this.type=A,this.firstFrame=t,this.t...
function BMDestroyEvent (line 50) | function BMDestroyEvent(A,t){this.type=A,this.target=t}
function BMRenderFrameErrorEvent (line 50) | function BMRenderFrameErrorEvent(A,t){this.type="renderFrameError",this....
function BMConfigErrorEvent (line 50) | function BMConfigErrorEvent(A){this.type="configError",this.nativeError=A}
function HSVtoRGB (line 50) | function HSVtoRGB(A,t,e){var a,r,n,l,i,o,s,p;switch(o=e*(1-t),s=e*(1-(i=...
function RGBtoHSV (line 50) | function RGBtoHSV(A,t,e){var a,r=Math.max(A,t,e),n=Math.min(A,t,e),l=r-n...
function addSaturationToRGB (line 50) | function addSaturationToRGB(A,t){var e=RGBtoHSV(255*A[0],255*A[1],255*A[...
function addBrightnessToRGB (line 50) | function addBrightnessToRGB(A,t){var e=RGBtoHSV(255*A[0],255*A[1],255*A[...
function addHueToRGB (line 50) | function addHueToRGB(A,t){var e=RGBtoHSV(255*A[0],255*A[1],255*A[2]);ret...
function createNS (line 50) | function createNS(A){return document.createElementNS(svgNS,A)}
function _typeof$5 (line 50) | function _typeof$5(A){return(_typeof$5="function"==typeof Symbol&&"symbo...
function l (line 50) | function l(){t||((t=function(t){if(window.Worker&&window.Blob&&getWebWor...
function i (line 50) | function i(A,t){var r="processId_"+(e+=1);return a[r]={onComplete:A,onEr...
function t (line 50) | function t(){this.loadedAssets+=1,this.loadedAssets===this.totalImages&&...
function e (line 50) | function e(){this.loadedFootagesCount+=1,this.loadedAssets===this.totalI...
function a (line 50) | function a(A,t,e){var a="";if(A.e)a=A.p;else if(t){var r=A.p;-1!==r.inde...
function r (line 50) | function r(A){var t=0,e=setInterval(function(){(A.getBBox().width||t>500...
function n (line 50) | function n(A){var t={assetData:A},e=a(A,this.assetsPath,this.path);retur...
function l (line 50) | function l(){this._imageLoaded=t.bind(this),this._footageLoaded=e.bind(t...
function BaseEvent (line 50) | function BaseEvent(){}
function A (line 50) | function A(A){for(var t,e=A.split("\r\n"),a={},r=0,n=0;n<e.length;n+=1)2...
function A (line 50) | function A(A){this.compositions.push(A)}
function t (line 50) | function t(A){for(var t=0,e=this.compositions.length;t<e;){if(this.compo...
function getRenderer (line 50) | function getRenderer(A){return renderers[A]}
function getRegisteredRenderer (line 50) | function getRegisteredRenderer(){if(renderers.canvas)return"canvas";for(...
function _typeof$4 (line 50) | function _typeof$4(A){return(_typeof$4="function"==typeof Symbol&&"symbo...
function i (line 50) | function i(A){for(var e=0,r=A.target;e<a;)t[e].animation===r&&(t.splice(...
function o (line 50) | function o(A,e){if(!A)return null;for(var r=0;r<a;){if(t[r].elem===A&&nu...
function s (line 50) | function s(){r+=1,h()}
function p (line 50) | function p(){r-=1}
function c (line 50) | function c(A,e){A.addEventListener("destroy",i),A.addEventListener("_act...
function u (line 50) | function u(A){var i,o=A-e;for(i=0;i<a;i+=1)t[i].animation.advanceTime(o)...
function d (line 50) | function d(A){e=A,window.requestAnimationFrame(u)}
function h (line 50) | function h(){!l&&r&&n&&(window.requestAnimationFrame(d),n=!1)}
function a (line 50) | function a(A,t){return 1-3*t+3*A}
function r (line 50) | function r(A,t){return 3*t-6*A}
function n (line 50) | function n(A){return 3*A}
function l (line 50) | function l(A,t,e){return((a(t,e)*A+r(t,e))*A+n(t))*A}
function i (line 50) | function i(A,t,e){return 3*a(t,e)*A*A+2*r(t,e)*A+n(t)}
function o (line 50) | function o(A){this._p=A,this._mSampleValues=e?new Float32Array(11):new A...
function bezFunction (line 50) | function bezFunction(){var A=Math;function t(A,t,e,a,r,n){var l=A*a+t*r+...
function interpolateValue (line 50) | function interpolateValue(A,t){var e,a=this.offsetTime;"multidimensional...
function slerp (line 50) | function slerp(A,t,e){var a,r,n,l,i,o=[],s=A[0],p=A[1],c=A[2],u=A[3],d=t...
function quaternionToEuler (line 50) | function quaternionToEuler(A,t){var e=t[0],a=t[1],r=t[2],n=t[3],l=Math.a...
function createQuaternion (line 50) | function createQuaternion(A){var t=A[0]*degToRads,e=A[1]*degToRads,a=A[2...
function getValueAtCurrentTime (line 50) | function getValueAtCurrentTime(){var A=this.comp.renderedFrame-this.offs...
function setVValue (line 50) | function setVValue(A){var t;if("unidimensional"===this.propType)t=A*this...
function processEffectsSequence (line 50) | function processEffectsSequence(){if(this.elem.globalData.frameId!==this...
function addEffect (line 50) | function addEffect(A){this.effectsSequence.push(A),this.container.addDyn...
function ValueProperty (line 50) | function ValueProperty(A,t,e,a){this.propType="unidimensional",this.mult...
function MultiDimensionalProperty (line 50) | function MultiDimensionalProperty(A,t,e,a){var r;this.propType="multidim...
function KeyframedValueProperty (line 50) | function KeyframedValueProperty(A,t,e,a){this.propType="unidimensional",...
function KeyframedMultidimensionalProperty (line 50) | function KeyframedMultidimensionalProperty(A,t,e,a){var r;this.propType=...
function DynamicPropertyContainer (line 50) | function DynamicPropertyContainer(){}
function ShapePath (line 50) | function ShapePath(){this.c=!1,this._length=0,this._maxLength=8,this.v=c...
function ShapeCollection (line 50) | function ShapeCollection(){this._length=0,this._maxLength=4,this.shapes=...
function A (line 50) | function A(A,t,e){var a,r,n,l,i,o,s,p,c,u=e.lastIndex,d=this.keyframes;i...
function t (line 50) | function t(){var A=this.comp.renderedFrame-this.offsetTime,t=this.keyfra...
function e (line 50) | function e(){this.paths=this.localShapeCollection}
function a (line 50) | function a(A){(function(A,t){if(A._length!==t._length||A.c!==t.c)return!...
function r (line 50) | function r(){if(this.elem.globalData.frameId!==this.frameId)if(this.effe...
function n (line 50) | function n(A,t,a){this.propType="shape",this.comp=A.comp,this.container=...
function l (line 50) | function l(A){this.effectsSequence.push(A),this.container.addDynamicProp...
function i (line 50) | function i(A,a,r){this.propType="shape",this.comp=A.comp,this.elem=A,thi...
function t (line 50) | function t(A,t){this.v=shapePool.newElement(),this.v.setPathData(!0,4),t...
function A (line 50) | function A(A,t){this.v=shapePool.newElement(),this.v.setPathData(!0,0),t...
function A (line 50) | function A(A,t){this.v=shapePool.newElement(),this.v.c=!0,this.localShap...
function r (line 50) | function r(){return this.props[0]=1,this.props[1]=0,this.props[2]=0,this...
function n (line 50) | function n(e){if(0===e)return this;var a=A(e),r=t(e);return this._t(a,-r...
function l (line 50) | function l(e){if(0===e)return this;var a=A(e),r=t(e);return this._t(1,0,...
function i (line 50) | function i(e){if(0===e)return this;var a=A(e),r=t(e);return this._t(a,0,...
function o (line 50) | function o(e){if(0===e)return this;var a=A(e),r=t(e);return this._t(a,-r...
function s (line 50) | function s(A,t){return this._t(1,t,A,1,0,0)}
function p (line 50) | function p(A,t){return this.shear(e(A),e(t))}
function c (line 50) | function c(a,r){var n=A(r),l=t(r);return this._t(n,l,0,0,-l,n,0,0,0,0,1,...
function u (line 50) | function u(A,t,e){return e||0===e||(e=1),1===A&&1===t&&1===e?this:this._...
function d (line 50) | function d(A,t,e,a,r,n,l,i,o,s,p,c,u,d,h,S){return this.props[0]=A,this....
function h (line 50) | function h(A,t,e){return e=e||0,0!==A||0!==t||0!==e?this._t(1,0,0,0,0,1,...
function S (line 50) | function S(A,t,e,a,r,n,l,i,o,s,p,c,u,d,h,S){var f=this.props;if(1===A&&0...
function f (line 50) | function f(A){var t=A.props;return this.transform(t[0],t[1],t[2],t[3],t[...
function m (line 50) | function m(){return this._identityCalculated||(this._identity=!(1!==this...
function L (line 50) | function L(A){for(var t=0;t<16;){if(A.props[t]!==this.props[t])return!1;...
function W (line 50) | function W(A){var t;for(t=0;t<16;t+=1)A.props[t]=this.props[t];return A}
function g (line 50) | function g(A){var t;for(t=0;t<16;t+=1)this.props[t]=A[t]}
function y (line 50) | function y(A,t,e){return{x:A*this.props[0]+t*this.props[4]+e*this.props[...
function v (line 50) | function v(A,t,e){return A*this.props[0]+t*this.props[4]+e*this.props[8]...
function b (line 50) | function b(A,t,e){return A*this.props[1]+t*this.props[5]+e*this.props[9]...
function x (line 50) | function x(A,t,e){return A*this.props[2]+t*this.props[6]+e*this.props[10...
function E (line 50) | function E(){var A=this.props[0]*this.props[5]-this.props[1]*this.props[...
function k (line 50) | function k(A){return this.getInverseMatrix().applyToPointArray(A[0],A[1]...
function w (line 50) | function w(A){var t,e=A.length,a=[];for(t=0;t<e;t+=1)a[t]=k(A[t]);return a}
function C (line 50) | function C(A,t,e){var a=createTypedArray("float32",6);if(this.isIdentity...
function P (line 50) | function P(A,t,e){return this.isIdentity()?[A,t,e]:[A*this.props[0]+t*th...
function T (line 50) | function T(A,t){if(this.isIdentity())return A+","+t;var e=this.props;ret...
function _ (line 50) | function _(){for(var A=0,t=this.props,e="matrix3d(";A<16;)e+=a(1e4*t[A])...
function M (line 50) | function M(A){return A<1e-6&&A>0||A>-1e-6&&A<0?a(1e4*A)/1e4:A}
function D (line 50) | function D(){var A=this.props;return"matrix("+M(A[0])+","+M(A[1])+","+M(...
function _typeof$3 (line 50) | function _typeof$3(A){return(_typeof$3="function"==typeof Symbol&&"symbo...
function setLocation (line 50) | function setLocation(A){setLocationHref(A)}
function searchAnimations (line 50) | function searchAnimations(){animationManager.searchAnimations()}
function setSubframeRendering (line 50) | function setSubframeRendering(A){setSubframeEnabled(A)}
function setPrefix (line 50) | function setPrefix(A){setIdPrefix(A)}
function loadAnimation (line 50) | function loadAnimation(A){return animationManager.loadAnimation(A)}
function setQuality (line 50) | function setQuality(A){if("string"==typeof A)switch(A){case"high":setDef...
function inBrowser (line 50) | function inBrowser(){return"undefined"!=typeof navigator}
function installPlugin (line 50) | function installPlugin(A,t){"expressions"===A&&setExpressionsPlugin(t)}
function getFactory (line 50) | function getFactory(A){switch(A){case"propertyFactory":return PropertyFa...
function checkReady (line 50) | function checkReady(){"complete"===document.readyState&&(clearInterval(r...
function getQueryVariable (line 50) | function getQueryVariable(A){for(var t=queryString.split("&"),e=0;e<t.le...
function ShapeModifier (line 50) | function ShapeModifier(){}
function TrimModifier (line 50) | function TrimModifier(){}
function PuckerAndBloatModifier (line 50) | function PuckerAndBloatModifier(){}
function t (line 50) | function t(A,t,e){if(this.elem=A,this.frameId=-1,this.propType="transfor...
function RepeaterModifier (line 50) | function RepeaterModifier(){}
function RoundCornersModifier (line 50) | function RoundCornersModifier(){}
function floatEqual (line 50) | function floatEqual(A,t){return 1e5*Math.abs(A-t)<=Math.min(Math.abs(A),...
function floatZero (line 50) | function floatZero(A){return Math.abs(A)<=1e-5}
function lerp (line 50) | function lerp(A,t,e){return A*(1-e)+t*e}
function lerpPoint (line 50) | function lerpPoint(A,t,e){return[lerp(A[0],t[0],e),lerp(A[1],t[1],e)]}
function quadRoots (line 50) | function quadRoots(A,t,e){if(0===A)return[];var a=t*t-4*A*e;if(a<0)retur...
function polynomialCoefficients (line 50) | function polynomialCoefficients(A,t,e,a){return[3*t-A-3*e+a,3*A-6*t+3*e,...
function singlePoint (line 50) | function singlePoint(A){return new PolynomialBezier(A,A,A,A,!1)}
function PolynomialBezier (line 50) | function PolynomialBezier(A,t,e,a,r){r&&pointEqual(A,t)&&(t=lerpPoint(A,...
function extrema (line 50) | function extrema(A,t){var e=A.points[0][t],a=A.points[A.points.length-1]...
function intersectData (line 50) | function intersectData(A,t,e){var a=A.boundingBox();return{cx:a.cx,cy:a....
function splitData (line 50) | function splitData(A){var t=A.bez.split(.5);return[intersectData(t[0],A....
function boxIntersect (line 50) | function boxIntersect(A,t){return 2*Math.abs(A.cx-t.cx)<A.width+t.width&...
function intersectsImpl (line 50) | function intersectsImpl(A,t,e,a,r,n){if(boxIntersect(A,t))if(e>=n||A.wid...
function crossProduct (line 50) | function crossProduct(A,t){return[A[1]*t[2]-A[2]*t[1],A[2]*t[0]-A[0]*t[2...
function lineIntersection (line 50) | function lineIntersection(A,t,e,a){var r=[A[0],A[1],1],n=[t[0],t[1],1],l...
function polarOffset (line 50) | function polarOffset(A,t,e){return[A[0]+Math.cos(t)*e,A[1]-Math.sin(t)*e]}
function pointDistance (line 50) | function pointDistance(A,t){return Math.hypot(A[0]-t[0],A[1]-t[1])}
function pointEqual (line 50) | function pointEqual(A,t){return floatEqual(A[0],t[0])&&floatEqual(A[1],t...
function ZigZagModifier (line 50) | function ZigZagModifier(){}
function setPoint (line 50) | function setPoint(A,t,e,a,r,n,l){var i=e-Math.PI/2,o=e+Math.PI/2,s=t[0]+...
function getPerpendicularVector (line 50) | function getPerpendicularVector(A,t){var e=[t[0]-A[0],t[1]-A[1]],a=.5*-M...
function getProjectingAngle (line 50) | function getProjectingAngle(A,t){var e=0===t?A.length()-1:t-1,a=(t+1)%A....
function zigZagCorner (line 50) | function zigZagCorner(A,t,e,a,r,n,l){var i=getProjectingAngle(t,e),o=t.v...
function zigZagSegment (line 50) | function zigZagSegment(A,t,e,a,r,n){for(var l=0;l<a;l+=1){var i=(l+1)/(a...
function linearOffset (line 50) | function linearOffset(A,t,e){var a=Math.atan2(t[0]-A[0],t[1]-A[1]);retur...
function offsetSegment (line 50) | function offsetSegment(A,t){var e,a,r,n,l,i,o;e=(o=linearOffset(A.points...
function joinLines (line 50) | function joinLines(A,t,e,a,r){var n=t.points[3],l=e.points[0];if(3===a)r...
function getIntersection (line 50) | function getIntersection(A,t){var e=A.intersections(t);return e.length&&...
function pruneSegmentIntersection (line 50) | function pruneSegmentIntersection(A,t){var e=A.slice(),a=t.slice(),r=get...
function pruneIntersections (line 50) | function pruneIntersections(A){for(var t,e=1;e<A.length;e+=1)t=pruneSegm...
function offsetSegmentSplit (line 50) | function offsetSegmentSplit(A,t){var e,a,r,n,l=A.inflectionPoints();if(0...
function OffsetPathModifier (line 50) | function OffsetPathModifier(){}
function getFontProperties (line 50) | function getFontProperties(A){for(var t=A.fStyle?A.fStyle.split(" "):[],...
function a (line 50) | function a(A,t){var e=createTag("span");e.setAttribute("aria-hidden",!0)...
function r (line 50) | function r(A,t){var e,a=document.body&&t?"svg":"canvas",r=getFontPropert...
function n (line 50) | function n(A){var t=0,e=A.charCodeAt(0);if(e>=55296&&e<=56319){var a=A.c...
function l (line 50) | function l(A){var t=n(A);return t>=127462&&t<=127487}
function SlotManager (line 50) | function SlotManager(A){this.animationData=A}
function slotFactory (line 50) | function slotFactory(A){return new SlotManager(A)}
function RenderableElement (line 50) | function RenderableElement(){}
function SliderEffect (line 50) | function SliderEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,0,0,e)}
function AngleEffect (line 50) | function AngleEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,0,0,e)}
function ColorEffect (line 50) | function ColorEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,1,0,e)}
function PointEffect (line 50) | function PointEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,1,0,e)}
function LayerIndexEffect (line 50) | function LayerIndexEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,0,...
function MaskIndexEffect (line 50) | function MaskIndexEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,0,0...
function CheckboxEffect (line 50) | function CheckboxEffect(A,t,e){this.p=PropertyFactory.getProp(t,A.v,0,0,e)}
function NoValueEffect (line 50) | function NoValueEffect(){this.p={}}
function EffectsManager (line 50) | function EffectsManager(A,t){var e,a=A.ef||[];this.effectElements=[];var...
function GroupEffect (line 50) | function GroupEffect(A,t){this.init(A,t)}
function BaseElement (line 50) | function BaseElement(){}
function FrameElement (line 50) | function FrameElement(){}
function FootageElement (line 50) | function FootageElement(A,t,e){this.initFrame(),this.initRenderable(),th...
function AudioElement (line 50) | function AudioElement(A,t,e){this.initFrame(),this.initRenderable(),this...
function BaseRenderer (line 50) | function BaseRenderer(){}
function TransformElement (line 50) | function TransformElement(){}
function MaskElement (line 50) | function MaskElement(A,t,e){this.data=A,this.element=t,this.globalData=e...
function SVGEffects (line 50) | function SVGEffects(A){var t,e,a="SourceGraphic",r=A.data.ef?A.data.ef.l...
function registerEffect$1 (line 50) | function registerEffect$1(A,t,e){registeredEffects$1[A]={effect:t,counts...
function SVGBaseElement (line 50) | function SVGBaseElement(){}
function HierarchyElement (line 50) | function HierarchyElement(){}
function RenderableDOMElement (line 50) | function RenderableDOMElement(){}
function IImageElement (line 50) | function IImageElement(A,t,e){this.assetData=t.getAssetData(A.refId),thi...
function ProcessedElement (line 50) | function ProcessedElement(A,t){this.elem=A,this.pos=t}
function IShapeElement (line 50) | function IShapeElement(){}
function SVGShapeData (line 50) | function SVGShapeData(A,t,e){this.caches=[],this.styles=[],this.transfor...
function SVGStyleData (line 50) | function SVGStyleData(A,t){this.data=A,this.type=A.ty,this.d="",this.lvl...
function DashProperty (line 50) | function DashProperty(A,t,e,a){var r;this.elem=A,this.frameId=-1,this.da...
function SVGStrokeStyleData (line 50) | function SVGStrokeStyleData(A,t,e){this.initDynamicPropertyContainer(A),...
function SVGFillStyleData (line 50) | function SVGFillStyleData(A,t,e){this.initDynamicPropertyContainer(A),th...
function SVGNoStyleData (line 50) | function SVGNoStyleData(A,t,e){this.initDynamicPropertyContainer(A),this...
function GradientProperty (line 50) | function GradientProperty(A,t,e){this.data=t,this.c=createTypedArray("ui...
function SVGGradientFillStyleData (line 50) | function SVGGradientFillStyleData(A,t,e){this.initDynamicPropertyContain...
function SVGGradientStrokeStyleData (line 50) | function SVGGradientStrokeStyleData(A,t,e){this.initDynamicPropertyConta...
function ShapeGroupData (line 50) | function ShapeGroupData(){this.it=[],this.prevViewData=[],this.gr=create...
function SVGTransformData (line 50) | function SVGTransformData(A,t,e){this.transform={mProps:A,op:t,container...
function e (line 50) | function e(A,t,e){(e||t.transform.op._mdf)&&t.transform.container.setAtt...
function a (line 50) | function a(){}
function r (line 50) | function r(e,a,r){var n,l,i,o,s,p,c,u,d,h,S=a.styles.length,f=a.lvl;for(...
function n (line 50) | function n(A,t,e){var a=t.style;(t.c._mdf||e)&&a.pElem.setAttribute("fil...
function l (line 50) | function l(A,t,e){i(A,t,e),o(0,t,e)}
function i (line 50) | function i(A,t,e){var a,r,n,l,i,o=t.gf,s=t.g._hasOpacity,p=t.s.v,c=t.e.v...
function o (line 50) | function o(A,t,e){var a=t.style,r=t.d;r&&(r._mdf||e)&&r.dashStr&&(a.pEle...
function SVGShapeElement (line 50) | function SVGShapeElement(A,t,e){this.shapes=[],this.shapesData=A.shapes,...
function LetterProps (line 50) | function LetterProps(A,t,e,a,r,n){this.o=A,this.sw=t,this.sc=e,this.fc=a...
function TextProperty (line 50) | function TextProperty(A,t){this._frameId=initialDefaultFrame,this.pv="",...
function a (line 50) | function a(A,t){this._currentTextLength=-1,this.k=!1,this.data=t,this.el...
function TextAnimatorDataProperty (line 50) | function TextAnimatorDataProperty(A,t,e){var a={propType:!1},r=PropertyF...
function TextAnimatorProperty (line 50) | function TextAnimatorProperty(A,t,e){this._isFirstFrame=!0,this._hasMask...
function ITextElement (line 50) | function ITextElement(){}
function SVGTextLottieElement (line 50) | function SVGTextLottieElement(A,t,e){this.textSpans=[],this.renderType="...
function ISolidElement (line 50) | function ISolidElement(A,t,e){this.initElement(A,t,e)}
function NullElement (line 50) | function NullElement(A,t,e){this.initFrame(),this.initBaseData(A,t,e),th...
function SVGRendererBase (line 50) | function SVGRendererBase(){}
function ICompElement (line 50) | function ICompElement(){}
function SVGCompElement (line 50) | function SVGCompElement(A,t,e){this.layers=A.layers,this.supports3d=!0,t...
function SVGRenderer (line 50) | function SVGRenderer(A,t){this.animationItem=A,this.layers=null,this.ren...
function ShapeTransformManager (line 50) | function ShapeTransformManager(){this.sequences={},this.sequenceList=[],...
function r (line 50) | function r(){var r,n,l;t||(r=createNS("svg"),n=createNS("filter"),l=crea...
function createCanvas (line 50) | function createCanvas(A,t){if(featureSupport.offscreenCanvas)return new ...
function CVEffects (line 50) | function CVEffects(A){var t,e,a=A.data.ef?A.data.ef.length:0;for(this.fi...
function registerEffect (line 50) | function registerEffect(A,t){registeredEffects[A]={effect:t}}
function CVMaskElement (line 50) | function CVMaskElement(A,t){var e;this.data=A,this.element=t,this.masksP...
function CVBaseElement (line 50) | function CVBaseElement(){}
function CVShapeData (line 50) | function CVShapeData(A,t,e,a){this.styledShapes=[],this.tr=[0,0,0,0,0,0]...
function CVShapeElement (line 50) | function CVShapeElement(A,t,e){this.shapes=[],this.shapesData=A.shapes,t...
function CVTextElement (line 50) | function CVTextElement(A,t,e){this.textSpans=[],this.yOffset=0,this.fill...
function CVImageElement (line 50) | function CVImageElement(A,t,e){this.assetData=t.getAssetData(A.refId),th...
function CVSolidElement (line 50) | function CVSolidElement(A,t,e){this.initElement(A,t,e)}
function CanvasRendererBase (line 50) | function CanvasRendererBase(){}
function CanvasContext (line 50) | function CanvasContext(){this.opacity=-1,this.transform=createTypedArray...
function CVContextData (line 50) | function CVContextData(){var A;for(this.stack=[],this.cArrPos=0,this.cTr...
function CVCompElement (line 50) | function CVCompElement(A,t,e){this.completeLayers=!1,this.layers=A.layer...
function CanvasRenderer (line 50) | function CanvasRenderer(A,t){this.animationItem=A,this.renderConfig={cle...
function HBaseElement (line 50) | function HBaseElement(){}
function HSolidElement (line 50) | function HSolidElement(A,t,e){this.initElement(A,t,e)}
function HShapeElement (line 50) | function HShapeElement(A,t,e){this.shapes=[],this.shapesData=A.shapes,th...
function HTextElement (line 50) | function HTextElement(A,t,e){this.textSpans=[],this.textPaths=[],this.cu...
function HCameraElement (line 50) | function HCameraElement(A,t,e){this.initFrame(),this.initBaseData(A,t,e)...
function HImageElement (line 50) | function HImageElement(A,t,e){this.assetData=t.getAssetData(A.refId),thi...
function HybridRendererBase (line 50) | function HybridRendererBase(A,t){this.animationItem=A,this.layers=null,t...
function HCompElement (line 50) | function HCompElement(A,t,e){this.layers=A.layers,this.supports3d=!A.has...
function HybridRenderer (line 50) | function HybridRenderer(A,t){this.animationItem=A,this.layers=null,this....
function t (line 50) | function t(t){for(var e=0,a=A.layers.length;e<a;){if(A.layers[e].nm===t|...
function _typeof$2 (line 50) | function _typeof$2(A){return(_typeof$2="function"==typeof Symbol&&"symbo...
function seedRandom (line 50) | function seedRandom(A,t){var e=this,a=t.pow(256,6),r=t.pow(2,52),n=2*r;f...
function initialize$2 (line 50) | function initialize$2(A){seedRandom([],A)}
function _typeof$1 (line 50) | function _typeof$1(A){return(_typeof$1="function"==typeof Symbol&&"symbo...
function resetFrame (line 50) | function resetFrame(){_lottieGlobal={}}
function $bm_isInstanceOfArray (line 50) | function $bm_isInstanceOfArray(A){return A.constructor===Array||A.constr...
function isNumerable (line 50) | function isNumerable(A,t){return"number"===A||t instanceof Number||"bool...
function $bm_neg (line 50) | function $bm_neg(A){var t=_typeof$1(A);if("number"===t||A instanceof Num...
function sum (line 50) | function sum(A,t){var e=_typeof$1(A),a=_typeof$1(t);if(isNumerable(e,A)&...
function sub (line 50) | function sub(A,t){var e=_typeof$1(A),a=_typeof$1(t);if(isNumerable(e,A)&...
function mul (line 50) | function mul(A,t){var e,a,r,n=_typeof$1(A),l=_typeof$1(t);if(isNumerable...
function div (line 50) | function div(A,t){var e,a,r,n=_typeof$1(A),l=_typeof$1(t);if(isNumerable...
function mod (line 50) | function mod(A,t){return"string"==typeof A&&(A=parseInt(A,10)),"string"=...
function clamp (line 50) | function clamp(A,t,e){if(t>e){var a=e;e=t,t=a}return Math.min(Math.max(A...
function radiansToDegrees (line 50) | function radiansToDegrees(A){return A/degToRads}
function degreesToRadians (line 50) | function degreesToRadians(A){return A*degToRads}
function length (line 50) | function length(A,t){if("number"==typeof A||A instanceof Number)return t...
function normalize (line 50) | function normalize(A){return div(A,length(A))}
function rgbToHsl (line 50) | function rgbToHsl(A){var t,e,a=A[0],r=A[1],n=A[2],l=Math.max(a,r,n),i=Ma...
function hue2rgb (line 50) | function hue2rgb(A,t,e){return e<0&&(e+=1),e>1&&(e-=1),e<1/6?A+6*(t-A)*e...
function hslToRgb (line 50) | function hslToRgb(A){var t,e,a,r=A[0],n=A[1],l=A[2];if(0===n)t=l,a=l,e=l...
function linear (line 50) | function linear(A,t,e,a,r){if(void 0!==a&&void 0!==r||(a=t,r=e,t=0,e=1),...
function random (line 50) | function random(A,t){if(void 0===t&&(void 0===A?(A=0,t=1):(t=A,A=void 0)...
function createPath (line 50) | function createPath(A,t,e,a){var r,n=A.length,l=shapePool.newElement();l...
function initiateExpression (line 50) | function initiateExpression(elem,data,property){function noOp(A){return ...
function A (line 50) | function A(A,t){this._mask=A,this._data=t}
function e (line 50) | function e(A,t,e){Object.defineProperty(A,"velocity",{get:function(){ret...
function a (line 50) | function a(){return A}
function t (line 50) | function t(A){switch(A){case"scale":case"Scale":case"ADBE Scale":case 6:...
function A (line 50) | function A(A){var t=new Matrix;return void 0!==A?this._elem.finalTransfo...
function t (line 50) | function t(A,t){var e=this.getMatrix(t);return e.props[12]=0,e.props[13]...
function e (line 50) | function e(A,t){var e=this.getMatrix(t);return this.applyPoint(e,A)}
function a (line 50) | function a(A,t){var e=this.getMatrix(t);return e.props[12]=0,e.props[13]...
function r (line 50) | function r(A,t){var e=this.getMatrix(t);return this.invertPoint(e,A)}
function n (line 50) | function n(A,t){if(this._elem.hierarchy&&this._elem.hierarchy.length){va...
function l (line 50) | function l(A,t){if(this._elem.hierarchy&&this._elem.hierarchy.length){va...
function i (line 50) | function i(A){var t=new Matrix;if(t.reset(),this._elem.finalTransform.mP...
function o (line 50) | function o(){return[1,1,1,1]}
function c (line 50) | function c(A){switch(A){case"ADBE Root Vectors Group":case"Contents":cas...
function A (line 50) | function A(e,a,r,n){function l(A){for(var t=e.ef,a=0,r=t.length;a<r;){if...
function t (line 50) | function t(A,t,e,a){var r=ExpressionPropertyInterface(A.p);return A.p.se...
function r (line 50) | function r(A){return"Shape"===A||"shape"===A||"Path"===A||"path"===A||"A...
function A (line 50) | function A(A,i,d){var h,S=[],f=A?A.length:0;for(h=0;h<f;h+=1)"gr"===A[h]...
function t (line 50) | function t(t,e,a){var r=function(A){switch(A){case"ADBE Vectors Group":c...
function e (line 50) | function e(A,t,e){function a(A){return"Color"===A||"color"===A?a.color:"...
function a (line 50) | function a(A,t,e){function a(A){return"Start Point"===A||"start point"==...
function r (line 50) | function r(){return function(){return null}}
function n (line 50) | function n(A,t,e){var a,r=propertyGroupFactory(s,e),n=propertyGroupFacto...
function l (line 50) | function l(A,t,e){function a(t){return t===A.e.ix||"End"===t||"end"===t?...
function i (line 50) | function i(A,t,e){function a(t){return A.a.ix===t||"Anchor Point"===t?a....
function o (line 50) | function o(A,t,e){function a(t){return A.p.ix===t?a.position:A.s.ix===t?...
function s (line 50) | function s(A,t,e){function a(t){return A.p.ix===t?a.position:A.r.ix===t?...
function p (line 50) | function p(A,t,e){function a(t){return A.p.ix===t?a.position:A.r.ix===t?...
function c (line 50) | function c(A,t,e){function a(t){return A.r.ix===t||"Round Corners 1"===t...
function u (line 50) | function u(A,t,e){function a(t){return A.c.ix===t||"Copies"===t?a.copies...
function n (line 50) | function n(A){if("number"==typeof A)return 0===(A=void 0===A?1:A)?a:r[A-...
function e (line 50) | function e(A){return"ADBE Text Document"===A?e.sourceText:null}
function _typeof (line 50) | function _typeof(A){return(_typeof="function"==typeof Symbol&&"symbol"==...
function t (line 50) | function t(A){return"Outline"===A?t.outlineInterface():null}
function a (line 50) | function a(A){if(e[A])return t=A,"object"===_typeof(e=e[A])?a:e;var r=A....
function t (line 50) | function t(A){return"Data"===A?t.dataInterface:null}
function getInterface (line 50) | function getInterface(A){return interfaces[A]||null}
function addPropertyDecorator (line 50) | function addPropertyDecorator(){function A(A,t,e){if(!this.k||!this.keyf...
function initialize$1 (line 50) | function initialize$1(){addPropertyDecorator()}
function addDecorator (line 50) | function addDecorator(){TextProperty.prototype.getExpressionValue=functi...
function initialize (line 50) | function initialize(){addDecorator()}
function SVGComposableEffect (line 50) | function SVGComposableEffect(){}
function SVGTintFilter (line 50) | function SVGTintFilter(A,t,e,a,r){this.filterManager=t;var n=createNS("f...
function SVGFillFilter (line 50) | function SVGFillFilter(A,t,e,a){this.filterManager=t;var r=createNS("feC...
function SVGStrokeEffect (line 50) | function SVGStrokeEffect(A,t,e){this.initialized=!1,this.filterManager=t...
function SVGTritoneFilter (line 50) | function SVGTritoneFilter(A,t,e,a){this.filterManager=t;var r=createNS("...
function SVGProLevelsFilter (line 50) | function SVGProLevelsFilter(A,t,e,a){this.filterManager=t;var r=this.fil...
function SVGDropShadowEffect (line 50) | function SVGDropShadowEffect(A,t,e,a,r){var n=t.container.globalData.ren...
function SVGMatte3Effect (line 50) | function SVGMatte3Effect(A,t,e){this.initialized=!1,this.filterManager=t...
function SVGGaussianBlurEffect (line 50) | function SVGGaussianBlurEffect(A,t,e,a){A.setAttribute("x","-100%"),A.se...
function TransformEffect (line 50) | function TransformEffect(){}
function SVGTransformEffect (line 50) | function SVGTransformEffect(A,t){this.init(t)}
function CVTransformEffect (line 50) | function CVTransformEffect(A){this.init(A)}
function parseSrc (line 50) | function parseSrc(A){if("object"==typeof A)return A;try{return JSON.pars...
function t (line 50) | function t(t){var e=A.call(this,t)||this;return e.container=null,e.unmou...
function styleInject (line 50) | function styleInject(A,t){void 0===t&&(t={});var e=t.insertAt;if("undefi...
function t (line 50) | function t(){var t=null!==A&&A.apply(this,arguments)||this;return t.stat...
function t (line 50) | function t(t){var e=A.call(this,t)||this;return e.inputRef=reactExports....
function t (line 50) | function t(t){var e=A.call(this,t)||this;return e.state={activeFrame:0,m...
function createCollection (line 50) | function createCollection(A){const t=A+"CollectionProvider",[e,a]=create...
function useDirection (line 50) | function useDirection(A){const t=reactExports.useContext(DirectionContex...
function clamp (line 50) | function clamp(A,t,e){return max(A,min(t,e))}
function evaluate (line 50) | function evaluate(A,t){return"function"==typeof A?A(t):A}
function getSide (line 50) | function getSide(A){return A.split("-")[0]}
function getAlignment (line 50) | function getAlignment(A){return A.split("-")[1]}
function getOppositeAxis (line 50) | function getOppositeAxis(A){return"x"===A?"y":"x"}
function getAxisLength (line 50) | function getAxisLength(A){return"y"===A?"height":"width"}
function getSideAxis (line 50) | function getSideAxis(A){return yAxisSides.has(getSide(A))?"y":"x"}
function getAlignmentAxis (line 50) | function getAlignmentAxis(A){return getOppositeAxis(getSideAxis(A))}
function getAlignmentSides (line 50) | function getAlignmentSides(A,t,e){void 0===e&&(e=!1);const a=getAlignmen...
function getExpandedPlacements (line 50) | function getExpandedPlacements(A){const t=getOppositePlacement(A);return...
function getOppositeAlignmentPlacement (line 50) | function getOppositeAlignmentPlacement(A){return A.replace(/start|end/g,...
function getSideList (line 50) | function getSideList(A,t,e){switch(A){case"top":case"bottom":return e?t?...
function getOppositeAxisPlacements (line 50) | function getOppositeAxisPlacements(A,t,e,a){const r=getAlignment(A);let ...
function getOppositePlacement (line 50) | function getOppositePlacement(A){return A.replace(/left|right|bottom|top...
function expandPaddingObject (line 50) | function expandPaddingObject(A){return{top:0,right:0,bottom:0,left:0,...A}}
function getPaddingObject (line 50) | function getPaddingObject(A){return"number"!=typeof A?expandPaddingObjec...
function rectToClientRect (line 50) | function rectToClientRect(A){const{x:t,y:e,width:a,height:r}=A;return{wi...
function computeCoordsFromPlacement (line 50) | function computeCoordsFromPlacement(A,t,e){let{reference:a,floating:r}=A...
function detectOverflow (line 50) | async function detectOverflow(A,t){var e;void 0===t&&(t={});const{x:a,y:...
method fn (line 50) | async fn(t){const{x:e,y:a,placement:r,rects:n,platform:l,elements:i,midd...
method fn (line 50) | async fn(t){var e,a;const{placement:r,middlewareData:n,rects:l,initialPl...
function getSideOffsets (line 50) | function getSideOffsets(A,t){return{top:A.top-t.height,right:A.right-t.w...
function isAnySideFullyClipped (line 50) | function isAnySideFullyClipped(A){return sides.some(t=>A[t]>=0)}
method fn (line 50) | async fn(t){const{rects:e}=t,{strategy:a="referenceHidden",...r}=evaluat...
function convertValueToCoords (line 50) | async function convertValueToCoords(A,t){const{placement:e,platform:a,el...
method fn (line 50) | async fn(t){var e,a;const{x:r,y:n,placement:l,middlewareData:i}=t,o=awai...
method fn (line 50) | async fn(t){const{x:e,y:a,placement:r}=t,{mainAxis:n=!0,crossAxis:l=!1,l...
method fn (line 50) | fn(t){const{x:e,y:a,placement:r,rects:n,middlewareData:l}=t,{offset:i=0,...
method fn (line 50) | async fn(t){var e,a;const{placement:r,rects:n,platform:l,elements:i}=t,{...
function hasWindow (line 50) | function hasWindow(){return"undefined"!=typeof window}
function getNodeName (line 50) | function getNodeName(A){return isNode(A)?(A.nodeName||"").toLowerCase():...
function getWindow (line 50) | function getWindow(A){var t;return(null==A||null==(t=A.ownerDocument)?vo...
function getDocumentElement (line 50) | function getDocumentElement(A){var t;return null==(t=(isNode(A)?A.ownerD...
function isNode (line 50) | function isNode(A){return!!hasWindow()&&(A instanceof Node||A instanceof...
function isElement (line 50) | function isElement(A){return!!hasWindow()&&(A instanceof Element||A inst...
function isHTMLElement (line 50) | function isHTMLElement(A){return!!hasWindow()&&(A instanceof HTMLElement...
function isShadowRoot (line 50) | function isShadowRoot(A){return!(!hasWindow()||"undefined"==typeof Shado...
function isOverflowElement (line 50) | function isOverflowElement(A){const{overflow:t,overflowX:e,overflowY:a,d...
function isTableElement (line 50) | function isTableElement(A){return tableElements.has(getNodeName(A))}
function isTopLayer (line 50) | function isTopLayer(A){return topLayerSelectors.some(t=>{try{return A.ma...
function isContainingBlock (line 50) | function isContainingBlock(A){const t=isWebKit(),e=isElement(A)?getCompu...
function getContainingBlock (line 50) | function getContainingBlock(A){let t=getParentNode(A);for(;isHTMLElement...
function isWebKit (line 50) | function isWebKit(){return!("undefined"==typeof CSS||!CSS.supports)&&CSS...
function isLastTraversableNode (line 50) | function isLastTraversableNode(A){return lastTraversableNodeNames.has(ge...
function getComputedStyle$1 (line 50) | function getComputedStyle$1(A){return getWindow(A).getComputedStyle(A)}
function getNodeScroll (line 50) | function getNodeScroll(A){return isElement(A)?{scrollLeft:A.scrollLeft,s...
function getParentNode (line 50) | function getParentNode(A){if("html"===getNodeName(A))return A;const t=A....
function getNearestOverflowAncestor (line 50) | function getNearestOverflowAncestor(A){const t=getParentNode(A);return i...
function getOverflowAncestors (line 50) | function getOverflowAncestors(A,t,e){var a;void 0===t&&(t=[]),void 0===e...
function getFrameElement (line 50) | function getFrameElement(A){return A.parent&&Object.getPrototypeOf(A.par...
function getCssDimensions (line 50) | function getCssDimensions(A){const t=getComputedStyle$1(A);let e=parseFl...
function unwrapElement (line 50) | function unwrapElement(A){return isElement(A)?A:A.contextElement}
function getScale (line 50) | function getScale(A){const t=unwrapElement(A);if(!isHTMLElement(t))retur...
function getVisualOffsets (line 50) | function getVisualOffsets(A){const t=getWindow(A);return isWebKit()&&t.v...
function shouldAddVisualOffsets (line 50) | function shouldAddVisualOffsets(A,t,e){return void 0===t&&(t=!1),!(!e||t...
function getBoundingClientRect (line 50) | function getBoundingClientRect(A,t,e,a){void 0===t&&(t=!1),void 0===e&&(...
function getWindowScrollBarX (line 50) | function getWindowScrollBarX(A,t){const e=getNodeScroll(A).scrollLeft;re...
function getHTMLOffset (line 50) | function getHTMLOffset(A,t){const e=A.getBoundingClientRect();return{x:e...
function convertOffsetParentRelativeRectToViewportRelativeRect (line 50) | function convertOffsetParentRelativeRectToViewportRelativeRect(A){let{el...
function getClientRects (line 50) | function getClientRects(A){return Array.from(A.getClientRects())}
function getDocumentRect (line 50) | function getDocumentRect(A){const t=getDocumentElement(A),e=getNodeScrol...
function getViewportRect (line 50) | function getViewportRect(A,t){const e=getWindow(A),a=getDocumentElement(...
function getInnerBoundingClientRect (line 50) | function getInnerBoundingClientRect(A,t){const e=getBoundingClientRect(A...
function getClientRectFromClippingAncestor (line 50) | function getClientRectFromClippingAncestor(A,t,e){let a;if("viewport"===...
function hasFixedPositionAncestor (line 50) | function hasFixedPositionAncestor(A,t){const e=getParentNode(A);return!(...
function getClippingElementAncestors (line 50) | function getClippingElementAncestors(A,t){const e=t.get(A);if(e)return e...
function getClippingRect (line 50) | function getClippingRect(A){let{element:t,boundary:e,rootBoundary:a,stra...
function getDimensions (line 50) | function getDimensions(A){const{width:t,height:e}=getCssDimensions(A);re...
function getRectRelativeToOffsetParent (line 50) | function getRectRelativeToOffsetParent(A,t,e){const a=isHTMLElement(t),r...
function isStaticPositioned (line 50) | function isStaticPositioned(A){return"static"===getComputedStyle$1(A).po...
function getTrueOffsetParent (line 50) | function getTrueOffsetParent(A,t){if(!isHTMLElement(A)||"fixed"===getCom...
function getOffsetParent (line 50) | function getOffsetParent(A,t){const e=getWindow(A);if(isTopLayer(A))retu...
function isRTL (line 50) | function isRTL(A){return"rtl"===getComputedStyle$1(A).direction}
function rectsAreEqual (line 50) | function rectsAreEqual(A,t){return A.x===t.x&&A.y===t.y&&A.width===t.wid...
function observeMove (line 50) | function observeMove(A,t){let e,a=null;const r=getDocumentElement(A);fun...
function autoUpdate (line 50) | function autoUpdate(A,t,e,a){void 0===a&&(a={});const{ancestorScroll:r=!...
function deepEqual (line 50) | function deepEqual(A,t){if(A===t)return!0;if(typeof A!=typeof t)return!1...
function getDPR (line 50) | function getDPR(A){if("undefined"==typeof window)return 1;return(A.owner...
function roundByDPR (line 50) | function roundByDPR(A,t){const e=getDPR(A);return Math.round(t*e)/e}
function useLatestRef (line 50) | function useLatestRef(A){const t=reactExports.useRef(A);return index(()=...
function useFloating (line 50) | function useFloating(A){void 0===A&&(A={});const{placement:t="bottom",st...
method fn (line 50) | fn(t){const{element:e,padding:a}="function"==typeof A?A(t):A;return e&&(...
function useSize (line 50) | function useSize(A){const[t,e]=reactExports.useState(void 0);return useL...
function isNotNull (line 50) | function isNotNull(A){return null!==A}
method fn (line 50) | fn(t){var e,a,r;const{placement:n,rects:l,middlewareData:i}=t,o=0!==(nul...
function getSideAndAlignFromPlacement (line 50) | function getSideAndAlignFromPlacement(A){const[t,e="center"]=A.split("-"...
function getDirectionAwareKey (line 50) | function getDirectionAwareKey(A,t){return"rtl"!==t?A:"ArrowLeft"===A?"Ar...
function getFocusIntent (line 50) | function getFocusIntent(A,t,e){const a=getDirectionAwareKey(A.key,e);if(...
function focusFirst$1 (line 50) | function focusFirst$1(A,t=!1){const e=document.activeElement;for(const a...
function wrapArray$1 (line 50) | function wrapArray$1(A,t){return A.map((e,a)=>A[(t+a)%A.length])}
function getOpenState (line 50) | function getOpenState(A){return A?"open":"closed"}
function isIndeterminate (line 50) | function isIndeterminate(A){return"indeterminate"===A}
function getCheckedState (line 50) | function getCheckedState(A){return isIndeterminate(A)?"indeterminate":A?...
function focusFirst (line 50) | function focusFirst(A){const t=document.activeElement;for(const e of A){...
function wrapArray (line 50) | function wrapArray(A,t){return A.map((e,a)=>A[(t+a)%A.length])}
function getNextMatch (line 50) | function getNextMatch(A,t,e){const a=t.length>1&&Array.from(t).every(A=>...
function isPointInPolygon (line 50) | function isPointInPolygon(A,t){const{x:e,y:a}=A;let r=!1;for(let n=0,l=t...
function isPointerInGraceArea (line 50) | function isPointerInGraceArea(A,t){if(!t)return!1;return isPointInPolygo...
function whenMouse (line 50) | function whenMouse(A){return t=>"mouse"===t.pointerType?A(t):void 0}
function ColorSchemeSwitcher (line 50) | function ColorSchemeSwitcher({className:A,isShare:t=!1}){const{t:e}=useT...
function LanguageSwitcher (line 50) | function LanguageSwitcher({className:A,showText:t=!1,storageKey:e="lang"...
FILE: frontend/assets/format-CdHm7RWL.js
function a (line 7) | function a(t,e){return"function"==typeof t?t(e):t&&"object"==typeof t&&r...
function o (line 7) | function o(t,e){return a(e||t,t)}
function u (line 7) | function u(){return i}
function s (line 7) | function s(t,e){var n,r,a,i;const s=u(),c=(null==e?void 0:e.weekStartsOn...
function c (line 7) | function c(t,e){return s(t,{...e,weekStartsOn:1})}
function d (line 7) | function d(t,e){const n=o(t,null==e?void 0:e.in),r=n.getFullYear(),i=a(n...
function l (line 7) | function l(t){const e=o(t),n=new Date(Date.UTC(e.getFullYear(),e.getMont...
function h (line 7) | function h(t,e){const n=o(t,null==e?void 0:e.in);return n.setHours(0,0,0...
function m (line 7) | function m(t,e,n){const[r,o]=function(t,...e){const n=a.bind(null,e.find...
function f (line 7) | function f(t){return!(!((e=t)instanceof Date||"object"==typeof e&&"[obje...
function w (line 7) | function w(t){return(e={})=>{const n=e.width?String(e.width):t.defaultWi...
function v (line 7) | function v(t){return(e,n)=>{let r;if("formatting"===((null==n?void 0:n.c...
function p (line 7) | function p(t){return(e,n={})=>{const r=n.width,a=r&&t.matchPatterns[r]||...
function x (line 7) | function x(t,e){const n=o(t,null==e?void 0:e.in),r=m(n,function(t,e){con...
function P (line 7) | function P(t,e){const r=o(t,null==e?void 0:e.in),i=+c(r)-+function(t,e){...
function S (line 7) | function S(t,e){var n,r,i,c;const d=o(t,null==e?void 0:e.in),l=d.getFull...
function W (line 7) | function W(t,e){const r=o(t,null==e?void 0:e.in),i=+s(r,e)-+function(t,e...
function D (line 7) | function D(t,e){return(t<0?"-":"")+Math.abs(t).toString().padStart(e,"0")}
method y (line 7) | y(t,e){const n=t.getFullYear(),r=n>0?n:1-n;return D("yy"===e?r%100:r,e.l...
method M (line 7) | M(t,e){const n=t.getMonth();return"M"===e?String(n+1):D(n+1,2)}
method a (line 7) | a(t,e){const n=t.getHours()/12>=1?"pm":"am";switch(e){case"a":case"aa":r...
method S (line 7) | S(t,e){const n=e.length,r=t.getMilliseconds();return D(Math.trunc(r*Math...
function E (line 7) | function E(t,e=""){const n=t>0?"-":"+",r=Math.abs(t),a=Math.trunc(r/60),...
function z (line 7) | function z(t,e){if(t%60==0){return(t>0?"-":"+")+D(Math.abs(t)/60,2)}retu...
function j (line 7) | function j(t,e=""){const n=t>0?"-":"+",r=Math.abs(t);return n+D(Math.tru...
function V (line 7) | function V(t,e,n){var r,a,i,s;const c=u(),d=c.locale??k,l=c.firstWeekCon...
function K (line 7) | function K(t){const e=t.match(I);return e?e[1].replace(R,"'"):t}
FILE: frontend/assets/git-automation-tBJ0Wppw.js
function se (line 13) | function se(){const{t:e}=s(),l=localStorage.getItem("token"),o=t.useCall...
function te (line 13) | function te({configId:e,open:a,onOpenChange:r}){const{t:i}=s(),{handleGi...
function ae (line 13) | function ae({config:e,vaults:a,onSubmit:r,onCancel:i}){const{t:n}=s(),{h...
function re (line 13) | function re(){const{t:e}=s(),{openConfirmDialog:a}=h(),{handleGitSyncLis...
FILE: frontend/assets/index-JfsWWBj_.js
function r (line 1) | function r(t){if(t<768)return!1;for(let e=0,r=i.length;;){let s=e+r>>1;i...
function s (line 1) | function s(t){return t>=127462&&t<=127487}
function o (line 1) | function o(t,e,i=!0,n=!0){return(i?a:l)(t,e,n)}
function a (line 1) | function a(t,e,i){if(e==t.length)return e;e&&c(t.charCodeAt(e))&&u(t.cha...
function l (line 1) | function l(t,e,i){for(;e>0;){let n=a(t,e-2,i);if(n<e)return n;e--}return 0}
function h (line 1) | function h(t,e){let i=t.charCodeAt(e);if(!u(i)||e+1==t.length)return i;l...
function c (line 1) | function c(t){return t>=56320&&t<57344}
function u (line 1) | function u(t){return t>=55296&&t<56320}
function d (line 1) | function d(t){return t<65536?1:2}
class f (line 1) | class f{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid po...
method lineAt (line 1) | lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position...
method line (line 1) | line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number...
method replace (line 1) | replace(t,e,i){[t,e]=x(this,t,e);let n=[];return this.decompose(0,t,n,...
method append (line 1) | append(t){return this.replace(this.length,this.length,t)}
method slice (line 1) | slice(t,e=this.length){[t,e]=x(this,t,e);let i=[];return this.decompos...
method eq (line 1) | eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.line...
method iter (line 1) | iter(t=1){return new Q(this,t)}
method iterRange (line 1) | iterRange(t,e=this.length){return new b(this,t,e)}
method iterLines (line 1) | iterLines(t,e){let i;if(null==t)i=this.iter();else{null==e&&(e=this.li...
method toString (line 1) | toString(){return this.sliceString(0)}
method toJSON (line 1) | toJSON(){let t=[];return this.flatten(t),t}
method constructor (line 1) | constructor(){}
method of (line 1) | static of(t){if(0==t.length)throw new RangeError("A document must have...
class O (line 1) | class O extends f{constructor(t,e=function(t){let e=-1;for(let i of t)e+...
method constructor (line 1) | constructor(t,e=function(t){let e=-1;for(let i of t)e+=i.length+1;retu...
method lines (line 1) | get lines(){return this.text.length}
method children (line 1) | get children(){return null}
method lineInner (line 1) | lineInner(t,e,i,n){for(let r=0;;r++){let s=this.text[r],o=n+s.length;i...
method decompose (line 1) | decompose(t,e,i,n){let r=t<=0&&e>=this.length?this:new O(g(this.text,t...
method replace (line 1) | replace(t,e,i){if(!(i instanceof O))return super.replace(t,e,i);[t,e]=...
method sliceString (line 1) | sliceString(t,e=this.length,i="\n"){[t,e]=x(this,t,e);let n="";for(let...
method flatten (line 1) | flatten(t){for(let e of this.text)t.push(e)}
method scanIdentical (line 1) | scanIdentical(){return 0}
method split (line 1) | static split(t,e){let i=[],n=-1;for(let r of t)i.push(r),n+=r.length+1...
class p (line 1) | class p extends f{constructor(t,e){super(),this.children=t,this.length=e...
method constructor (line 1) | constructor(t,e){super(),this.children=t,this.length=e,this.lines=0;fo...
method lineInner (line 1) | lineInner(t,e,i,n){for(let r=0;;r++){let s=this.children[r],o=n+s.leng...
method decompose (line 1) | decompose(t,e,i,n){for(let r=0,s=0;s<=e&&r<this.children.length;r++){l...
method replace (line 1) | replace(t,e,i){if([t,e]=x(this,t,e),i.lines<this.lines)for(let n=0,r=0...
method sliceString (line 1) | sliceString(t,e=this.length,i="\n"){[t,e]=x(this,t,e);let n="";for(let...
method flatten (line 1) | flatten(t){for(let e of this.children)e.flatten(t)}
method scanIdentical (line 1) | scanIdentical(t,e){if(!(t instanceof p))return 0;let i=0,[n,r,s,o]=e>0...
method from (line 1) | static from(t,e=t.reduce((t,e)=>t+e.length+1,-1)){let i=0;for(let d of...
function m (line 1) | function m(t,e,i=0,n=1e9){for(let r=0,s=0,o=!0;s<t.length&&r<=n;s++){let...
function g (line 1) | function g(t,e,i){return m(t,[""],e,i)}
class Q (line 1) | class Q{constructor(t,e=1){this.dir=e,this.done=!1,this.lineBreak=!1,thi...
method constructor (line 1) | constructor(t,e=1){this.dir=e,this.done=!1,this.lineBreak=!1,this.valu...
method nextInner (line 1) | nextInner(t,e){for(this.done=this.lineBreak=!1;;){let i=this.nodes.len...
method next (line 1) | next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.lengt...
class b (line 1) | class b{constructor(t,e,i){this.value="",this.done=!1,this.cursor=new Q(...
method constructor (line 1) | constructor(t,e,i){this.value="",this.done=!1,this.cursor=new Q(t,e>i?...
method nextInner (line 1) | nextInner(t,e){if(e<0?this.pos<=this.from:this.pos>=this.to)return thi...
method next (line 1) | next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min...
method lineBreak (line 1) | get lineBreak(){return this.cursor.lineBreak&&""!=this.value}
class v (line 1) | class v{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",thi...
method constructor (line 1) | constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done...
method next (line 1) | next(t=0){let{done:e,lineBreak:i,value:n}=this.inner.next(t);return e&...
method lineBreak (line 1) | get lineBreak(){return!1}
method constructor (line 1) | constructor(t,e,i,n){this.from=t,this.to=e,this.number=i,this.text=n}
method length (line 1) | get length(){return this.to-this.from}
function x (line 1) | function x(t,e,i){return[e=Math.max(0,Math.min(t.length,e)),Math.max(e,M...
function S (line 1) | function S(t,e,i=!0,n=!0){return o(t,e,i,n)}
function y (line 1) | function y(t,e){let i=t.charCodeAt(e);if(!(n=i,n>=55296&&n<56320&&e+1!=t...
function k (line 1) | function k(t){return t<=65535?String.fromCharCode(t):(t-=65536,String.fr...
function $ (line 1) | function $(t){return t<65536?1:2}
class C (line 1) | class C{constructor(t){this.sections=t}get length(){let t=0;for(let e=0;...
method constructor (line 1) | constructor(t){this.sections=t}
method length (line 1) | get length(){let t=0;for(let e=0;e<this.sections.length;e+=2)t+=this.s...
method newLength (line 1) | get newLength(){let t=0;for(let e=0;e<this.sections.length;e+=2){let i...
method empty (line 1) | get empty(){return 0==this.sections.length||2==this.sections.length&&t...
method iterGaps (line 1) | iterGaps(t){for(let e=0,i=0,n=0;e<this.sections.length;){let r=this.se...
method iterChangedRanges (line 1) | iterChangedRanges(t,e=!1){M(this,t,e)}
method invertedDesc (line 1) | get invertedDesc(){let t=[];for(let e=0;e<this.sections.length;){let i...
method composeDesc (line 1) | composeDesc(t){return this.empty?t:t.empty?this:z(this,t)}
method mapDesc (line 1) | mapDesc(t,e=!1){return t.empty?this:R(this,t,e)}
method mapPos (line 1) | mapPos(t,e=-1,i=T.Simple){let n=0,r=0;for(let s=0;s<this.sections.leng...
method touchesRange (line 1) | touchesRange(t,e=t){for(let i=0,n=0;i<this.sections.length&&n<=e;){let...
method toString (line 1) | toString(){let t="";for(let e=0;e<this.sections.length;){let i=this.se...
method toJSON (line 1) | toJSON(){return this.sections}
method fromJSON (line 1) | static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some(t=>"number...
method create (line 1) | static create(t){return new C(t)}
class Z (line 1) | class Z extends C{constructor(t,e){super(t),this.inserted=e}apply(t){if(...
method constructor (line 1) | constructor(t,e){super(t),this.inserted=e}
method apply (line 1) | apply(t){if(this.length!=t.length)throw new RangeError("Applying chang...
method mapDesc (line 1) | mapDesc(t,e=!1){return R(this,t,e,!0)}
method invert (line 1) | invert(t){let e=this.sections.slice(),i=[];for(let n=0,r=0;n<e.length;...
method compose (line 1) | compose(t){return this.empty?t:t.empty?this:z(this,t,!0)}
method map (line 1) | map(t,e=!1){return t.empty?this:R(this,t,e,!0)}
method iterChanges (line 1) | iterChanges(t,e=!1){M(this,t,e)}
method desc (line 1) | get desc(){return C.create(this.sections)}
method filter (line 1) | filter(t){let e=[],i=[],n=[],r=new _(this);t:for(let s=0,o=0;;){let a=...
method toJSON (line 1) | toJSON(){let t=[];for(let e=0;e<this.sections.length;e+=2){let i=this....
method of (line 1) | static of(t,e,i){let n=[],r=[],s=0,o=null;function a(t=!1){if(!t&&!n.l...
method empty (line 1) | static empty(t){return new Z(t?[t,-1]:[],[])}
method fromJSON (line 1) | static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid ...
method createSet (line 1) | static createSet(t,e){return new Z(t,e)}
function X (line 1) | function X(t,e,i,n=!1){if(0==e&&i<=0)return;let r=t.length-2;r>=0&&i<=0&...
function A (line 1) | function A(t,e,i){if(0==i.length)return;let n=e.length-2>>1;if(n<t.lengt...
function M (line 1) | function M(t,e,i){let n=t.inserted;for(let r=0,s=0,o=0;o<t.sections.leng...
function R (line 1) | function R(t,e,i,n=!1){let r=[],s=n?[]:null,o=new _(t),a=new _(e);for(le...
function z (line 1) | function z(t,e,i=!1){let n=[],r=i?[]:null,s=new _(t),o=new _(e);for(let ...
class _ (line 1) | class _{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sectio...
method constructor (line 1) | constructor(t){this.set=t,this.i=0,this.next()}
method next (line 1) | next(){let{sections:t}=this.set;this.i<t.length?(this.len=t[this.i++],...
method done (line 1) | get done(){return-2==this.ins}
method len2 (line 1) | get len2(){return this.ins<0?this.len:this.ins}
method text (line 1) | get text(){let{inserted:t}=this.set,e=this.i-2>>1;return e>=t.length?f...
method textBit (line 1) | textBit(t){let{inserted:e}=this.set,i=this.i-2>>1;return i>=e.length&&...
method forward (line 1) | forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}
method forward2 (line 1) | forward2(t){-1==this.ins?this.forward(t):t==this.ins?this.next():(this...
class E (line 1) | class E{constructor(t,e,i){this.from=t,this.to=e,this.flags=i}get anchor...
method constructor (line 1) | constructor(t,e,i){this.from=t,this.to=e,this.flags=i}
method anchor (line 1) | get anchor(){return 32&this.flags?this.to:this.from}
method head (line 1) | get head(){return 32&this.flags?this.from:this.to}
method empty (line 1) | get empty(){return this.from==this.to}
method assoc (line 1) | get assoc(){return 8&this.flags?-1:16&this.flags?1:0}
method bidiLevel (line 1) | get bidiLevel(){let t=7&this.flags;return 7==t?null:t}
method goalColumn (line 1) | get goalColumn(){let t=this.flags>>6;return 16777215==t?void 0:t}
method map (line 1) | map(t,e=-1){let i,n;return this.empty?i=n=t.mapPos(this.from,e):(i=t.m...
method extend (line 1) | extend(t,e=t){if(t<=this.anchor&&e>=this.anchor)return Y.range(t,e);le...
method eq (line 1) | eq(t,e=!1){return!(this.anchor!=t.anchor||this.head!=t.head||e&&this.e...
method toJSON (line 1) | toJSON(){return{anchor:this.anchor,head:this.head}}
method fromJSON (line 1) | static fromJSON(t){if(!t||"number"!=typeof t.anchor||"number"!=typeof ...
method create (line 1) | static create(t,e,i){return new E(t,e,i)}
class Y (line 1) | class Y{constructor(t,e){this.ranges=t,this.mainIndex=e}map(t,e=-1){retu...
method constructor (line 1) | constructor(t,e){this.ranges=t,this.mainIndex=e}
method map (line 1) | map(t,e=-1){return t.empty?this:Y.create(this.ranges.map(i=>i.map(t,e)...
method eq (line 1) | eq(t,e=!1){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.m...
method main (line 1) | get main(){return this.ranges[this.mainIndex]}
method asSingle (line 1) | asSingle(){return 1==this.ranges.length?this:new Y([this.main],0)}
method addRange (line 1) | addRange(t,e=!0){return Y.create([t].concat(this.ranges),e?0:this.main...
method replaceRange (line 1) | replaceRange(t,e=this.mainIndex){let i=this.ranges.slice();return i[e]...
method toJSON (line 1) | toJSON(){return{ranges:this.ranges.map(t=>t.toJSON()),main:this.mainIn...
method fromJSON (line 1) | static fromJSON(t){if(!t||!Array.isArray(t.ranges)||"number"!=typeof t...
method single (line 1) | static single(t,e=t){return new Y([Y.range(t,e)],0)}
method create (line 1) | static create(t,e=0){if(0==t.length)throw new RangeError("A selection ...
method cursor (line 1) | static cursor(t,e=0,i,n){return E.create(t,t,(0==e?0:e<0?8:16)|(null==...
method range (line 1) | static range(t,e,i,n){let r=(null!=i?i:16777215)<<6|(null==n?7:Math.mi...
method normalized (line 1) | static normalized(t,e=0){let i=t[e];t.sort((t,e)=>t.from-e.from),e=t.i...
function L (line 1) | function L(t,e){for(let i of t.ranges)if(i.to>e)throw new RangeError("Se...
class V (line 1) | class V{constructor(t,e,i,n,r){this.combine=t,this.compareInput=e,this.c...
method constructor (line 1) | constructor(t,e,i,n,r){this.combine=t,this.compareInput=e,this.compare...
method reader (line 1) | get reader(){return this}
method define (line 1) | static define(t={}){return new V(t.combine||(t=>t),t.compareInput||((t...
method of (line 1) | of(t){return new D([],this,0,t)}
method compute (line 1) | compute(t,e){if(this.isStatic)throw new Error("Can't compute a static ...
method computeN (line 1) | computeN(t,e){if(this.isStatic)throw new Error("Can't compute a static...
method from (line 1) | from(t,e){return e||(e=t=>t),this.compute([t],i=>e(i.field(t)))}
function W (line 1) | function W(t,e){return t==e||t.length==e.length&&t.every((t,i)=>t===e[i])}
class D (line 1) | class D{constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=...
method constructor (line 1) | constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=i,this...
method dynamicSlot (line 1) | dynamicSlot(t){var e;let i=this.value,n=this.facet.compareInput,r=this...
function B (line 1) | function B(t,e,i){if(t.length!=e.length)return!1;for(let n=0;n<t.length;...
function j (line 1) | function j(t,e){let i=!1;for(let n of e)1&st(t,n)&&(i=!0);return i}
function I (line 1) | function I(t,e,i){let n=i.map(e=>t[e.id]),r=i.map(t=>t.type),s=n.filter(...
class N (line 1) | class N{constructor(t,e,i,n,r){this.id=t,this.createF=e,this.updateF=i,t...
method constructor (line 1) | constructor(t,e,i,n,r){this.id=t,this.createF=e,this.updateF=i,this.co...
method define (line 1) | static define(t){let e=new N(q++,t.create,t.update,t.compare||((t,e)=>...
method create (line 1) | create(t){let e=t.facet(G).find(t=>t.field==this);return((null==e?void...
method slot (line 1) | slot(t){let e=t[this.id]>>1;return{create:t=>(t.values[e]=this.create(...
method init (line 1) | init(t){return[this,G.of({field:this,create:t})]}
method extension (line 1) | get extension(){return this}
function J (line 1) | function J(t){return e=>new et(e,t)}
class et (line 1) | class et{constructor(t,e){this.inner=t,this.prec=e}}
method constructor (line 1) | constructor(t,e){this.inner=t,this.prec=e}
class it (line 1) | class it{of(t){return new nt(this,t)}reconfigure(t){return it.reconfigur...
method of (line 1) | of(t){return new nt(this,t)}
method reconfigure (line 1) | reconfigure(t){return it.reconfigure.of({compartment:this,extension:t})}
method get (line 1) | get(t){return t.config.compartments.get(this)}
class nt (line 1) | class nt{constructor(t,e){this.compartment=t,this.inner=e}}
method constructor (line 1) | constructor(t,e){this.compartment=t,this.inner=e}
class rt (line 1) | class rt{constructor(t,e,i,n,r,s){for(this.base=t,this.compartments=e,th...
method constructor (line 1) | constructor(t,e,i,n,r,s){for(this.base=t,this.compartments=e,this.dyna...
method staticFacet (line 1) | staticFacet(t){let e=this.address[t.id];return null==e?t.default:this....
method resolve (line 1) | static resolve(t,e,i){let n=[],r=Object.create(null),s=new Map;for(let...
function st (line 1) | function st(t,e){if(1&e)return 2;let i=e>>1,n=t.status[i];if(4==n)throw ...
function ot (line 1) | function ot(t,e){return 1&e?t.config.staticValues[e>>1]:t.values[e>>1]}
class Ot (line 1) | class Ot{constructor(t,e){this.type=t,this.value=e}static define(){retur...
method constructor (line 1) | constructor(t,e){this.type=t,this.value=e}
method define (line 1) | static define(){return new pt}
class pt (line 1) | class pt{of(t){return new Ot(this,t)}}
method of (line 1) | of(t){return new Ot(this,t)}
class mt (line 1) | class mt{constructor(t){this.map=t}of(t){return new gt(this,t)}}
method constructor (line 1) | constructor(t){this.map=t}
method of (line 1) | of(t){return new gt(this,t)}
class gt (line 1) | class gt{constructor(t,e){this.type=t,this.value=e}map(t){let e=this.typ...
method constructor (line 1) | constructor(t,e){this.type=t,this.value=e}
method map (line 1) | map(t){let e=this.type.map(this.value,t);return void 0===e?void 0:e==t...
method is (line 1) | is(t){return this.type==t}
method define (line 1) | static define(t={}){return new mt(t.map||(t=>t))}
method mapEffects (line 1) | static mapEffects(t,e){if(!t.length)return t;let i=[];for(let n of t){...
class Qt (line 1) | class Qt{constructor(t,e,i,n,r,s){this.startState=t,this.changes=e,this....
method constructor (line 1) | constructor(t,e,i,n,r,s){this.startState=t,this.changes=e,this.selecti...
method create (line 1) | static create(t,e,i,n,r,s){return new Qt(t,e,i,n,r,s)}
method newDoc (line 1) | get newDoc(){return this._doc||(this._doc=this.changes.apply(this.star...
method newSelection (line 1) | get newSelection(){return this.selection||this.startState.selection.ma...
method state (line 1) | get state(){return this._state||this.startState.applyTransaction(this)...
method annotation (line 1) | annotation(t){for(let e of this.annotations)if(e.type==t)return e.value}
method docChanged (line 1) | get docChanged(){return!this.changes.empty}
method reconfigured (line 1) | get reconfigured(){return this.startState.config!=this.state.config}
method isUserEvent (line 1) | isUserEvent(t){let e=this.annotation(Qt.userEvent);return!(!e||!(e==t|...
function bt (line 1) | function bt(t,e){let i=[];for(let n=0,r=0;;){let s,o;if(n<t.length&&(r==...
function vt (line 1) | function vt(t,e,i){var n;let r,s,o;return i?(r=e.changes,s=Z.empty(e.cha...
function wt (line 1) | function wt(t,e,i){let n=e.selection,r=yt(e.annotations);return e.userEv...
function xt (line 1) | function xt(t,e,i){let n=wt(t,e.length?e[0]:{},t.doc.length);e.length&&!...
function yt (line 1) | function yt(t){return null==t?St:Array.isArray(t)?t:[t]}
function Tt (line 1) | function Tt(t){return e=>{if(!/\S/.test(e))return kt.Space;if(function(t...
class Ct (line 1) | class Ct{constructor(t,e,i,n,r,s){this.config=t,this.doc=e,this.selectio...
method constructor (line 1) | constructor(t,e,i,n,r,s){this.config=t,this.doc=e,this.selection=i,thi...
method field (line 1) | field(t,e=!0){let i=this.config.address[t.id];if(null!=i)return st(thi...
method update (line 1) | update(...t){return xt(this,t,!0)}
method applyTransaction (line 1) | applyTransaction(t){let e,i=this.config,{base:n,compartments:r}=i;for(...
method replaceSelection (line 1) | replaceSelection(t){return"string"==typeof t&&(t=this.toText(t)),this....
method changeByRange (line 1) | changeByRange(t){let e=this.selection,i=t(e.ranges[0]),n=this.changes(...
method changes (line 1) | changes(t=[]){return t instanceof Z?t:Z.of(t,this.doc.length,this.face...
method toText (line 1) | toText(t){return f.of(t.split(this.facet(Ct.lineSeparator)||P))}
method sliceDoc (line 1) | sliceDoc(t=0,e=this.doc.length){return this.doc.sliceString(t,e,this.l...
method facet (line 1) | facet(t){let e=this.config.address[t.id];return null==e?t.default:(st(...
method toJSON (line 1) | toJSON(t){let e={doc:this.sliceDoc(),selection:this.selection.toJSON()...
method fromJSON (line 1) | static fromJSON(t,e={},i){if(!t||"string"!=typeof t.doc)throw new Rang...
method create (line 1) | static create(t={}){let e=rt.resolve(t.extensions||[],new Map),i=t.doc...
method tabSize (line 1) | get tabSize(){return this.facet(Ct.tabSize)}
method lineBreak (line 1) | get lineBreak(){return this.facet(Ct.lineSeparator)||"\n"}
method readOnly (line 1) | get readOnly(){return this.facet(ft)}
method phrase (line 1) | phrase(t,...e){for(let i of this.facet(Ct.phrases))if(Object.prototype...
method languageDataAt (line 1) | languageDataAt(t,e,i=-1){let n=[];for(let r of this.facet(at))for(let ...
method charCategorizer (line 1) | charCategorizer(t){let e=this.languageDataAt("wordChars",t);return Tt(...
method wordAt (line 1) | wordAt(t){let{text:e,from:i,length:n}=this.doc.lineAt(t),r=this.charCa...
function Zt (line 1) | function Zt(t,e,i={}){let n={};for(let r of t)for(let t of Object.keys(r...
method compare (line 1) | compare(t,e){let i=Object.keys(t),n=Object.keys(e);return i.length==n.le...
class Xt (line 1) | class Xt{eq(t){return this==t}range(t,e=t){return Mt.create(t,e,this)}}
method eq (line 1) | eq(t){return this==t}
method range (line 1) | range(t,e=t){return Mt.create(t,e,this)}
function At (line 1) | function At(t,e){return t==e||t.constructor==e.constructor&&t.eq(e)}
method constructor (line 1) | constructor(t,e,i){this.from=t,this.to=e,this.value=i}
method create (line 1) | static create(e,i,n){return new t(e,i,n)}
function Rt (line 1) | function Rt(t,e){return t.from-e.from||t.value.startSide-e.value.startSide}
class zt (line 1) | class zt{constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.ma...
method constructor (line 1) | constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.maxPoint=n}
method length (line 1) | get length(){return this.to[this.to.length-1]}
method findIndex (line 1) | findIndex(t,e,i,n=0){let r=i?this.to:this.from;for(let s=n,o=r.length;...
method between (line 1) | between(t,e,i,n){for(let r=this.findIndex(e,-1e9,!0),s=this.findIndex(...
method map (line 1) | map(t,e){let i=[],n=[],r=[],s=-1,o=-1;for(let a=0;a<this.value.length;...
class _t (line 1) | class _t{constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLaye...
method constructor (line 1) | constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLayer=i,thi...
method create (line 1) | static create(t,e,i,n){return new _t(t,e,i,n)}
method length (line 1) | get length(){let t=this.chunk.length-1;return t<0?0:Math.max(this.chun...
method size (line 1) | get size(){if(this.isEmpty)return 0;let t=this.nextLayer.size;for(let ...
method chunkEnd (line 1) | chunkEnd(t){return this.chunkPos[t]+this.chunk[t].length}
method update (line 1) | update(t){let{add:e=[],sort:i=!1,filterFrom:n=0,filterTo:r=this.length...
method map (line 1) | map(t){if(t.empty||this.isEmpty)return this;let e=[],i=[],n=-1;for(let...
method between (line 1) | between(t,e,i){if(!this.isEmpty){for(let n=0;n<this.chunk.length;n++){...
method iter (line 1) | iter(t=0){return qt.from([this]).goto(t)}
method isEmpty (line 1) | get isEmpty(){return this.nextLayer==this}
method iter (line 1) | static iter(t,e=0){return qt.from(t).goto(e)}
method compare (line 1) | static compare(t,e,i,n,r=-1){let s=t.filter(t=>t.maxPoint>0||!t.isEmpt...
method eq (line 1) | static eq(t,e,i=0,n){null==n&&(n=999999999);let r=t.filter(t=>!t.isEmp...
method spans (line 1) | static spans(t,e,i,n,r=-1){let s=new Wt(t,null,r).goto(e),o=e,a=s.open...
method of (line 1) | static of(t,e=!1){let i=new Et;for(let n of t instanceof Mt?[t]:e?func...
method join (line 1) | static join(t){if(!t.length)return _t.empty;let e=t[t.length-1];for(le...
class Et (line 1) | class Et{finishChunk(t){this.chunks.push(new zt(this.from,this.to,this.v...
method finishChunk (line 1) | finishChunk(t){this.chunks.push(new zt(this.from,this.to,this.value,th...
method constructor (line 1) | constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this....
method add (line 1) | add(t,e,i){this.addInner(t,e,i)||(this.nextLayer||(this.nextLayer=new ...
method addInner (line 1) | addInner(t,e,i){let n=t-this.lastTo||i.startSide-this.last.endSide;if(...
method addChunk (line 1) | addChunk(t,e){if((t-this.lastTo||e.value[0].startSide-this.last.endSid...
method finish (line 1) | finish(){return this.finishInner(_t.empty)}
method finishInner (line 1) | finishInner(t){if(this.from.length&&this.finishChunk(!1),0==this.chunk...
function Yt (line 1) | function Yt(t,e,i){let n=new Map;for(let s of t)for(let t=0;t<s.chunk.le...
class Lt (line 1) | class Lt{constructor(t,e,i,n=0){this.layer=t,this.skip=e,this.minPoint=i...
method constructor (line 1) | constructor(t,e,i,n=0){this.layer=t,this.skip=e,this.minPoint=i,this.r...
method startSide (line 1) | get startSide(){return this.value?this.value.startSide:0}
method endSide (line 1) | get endSide(){return this.value?this.value.endSide:0}
method goto (line 1) | goto(t,e=-1e9){return this.chunkIndex=this.rangeIndex=0,this.gotoInner...
method gotoInner (line 1) | gotoInner(t,e,i){for(;this.chunkIndex<this.layer.chunk.length;){let e=...
method forward (line 1) | forward(t,e){(this.to-t||this.endSide-e)<0&&this.gotoInner(t,e,!0)}
method next (line 1) | next(){for(;;){if(this.chunkIndex==this.layer.chunk.length){this.from=...
method setRangeIndex (line 1) | setRangeIndex(t){if(t==this.layer.chunk[this.chunkIndex].value.length)...
method nextChunk (line 1) | nextChunk(){this.chunkIndex++,this.rangeIndex=0,this.next()}
method compare (line 1) | compare(t){return this.from-t.from||this.startSide-t.startSide||this.r...
class qt (line 1) | class qt{constructor(t){this.heap=t}static from(t,e=null,i=-1){let n=[];...
method constructor (line 1) | constructor(t){this.heap=t}
method from (line 1) | static from(t,e=null,i=-1){let n=[];for(let r=0;r<t.length;r++)for(let...
method startSide (line 1) | get startSide(){return this.value?this.value.startSide:0}
method goto (line 1) | goto(t,e=-1e9){for(let i of this.heap)i.goto(t,e);for(let i=this.heap....
method forward (line 1) | forward(t,e){for(let i of this.heap)i.forward(t,e);for(let i=this.heap...
method next (line 1) | next(){if(0==this.heap.length)this.from=this.to=1e9,this.value=null,th...
function Vt (line 1) | function Vt(t,e){for(let i=t[e];;){let n=1+(e<<1);if(n>=t.length)break;l...
class Wt (line 1) | class Wt{constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo...
method constructor (line 1) | constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo=[],thi...
method goto (line 1) | goto(t,e=-1e9){return this.cursor.goto(t,e),this.active.length=this.ac...
method forward (line 1) | forward(t,e){for(;this.minActive>-1&&(this.activeTo[this.minActive]-t|...
method removeActive (line 1) | removeActive(t){jt(this.active,t),jt(this.activeTo,t),jt(this.activeRa...
method addActive (line 1) | addActive(t){let e=0,{value:i,to:n,rank:r}=this.cursor;for(;e<this.act...
method next (line 1) | next(){let t=this.to,e=this.point;this.point=null;let i=this.openStart...
method activeForPoint (line 1) | activeForPoint(t){if(!this.active.length)return this.active;let e=[];f...
method openEnd (line 1) | openEnd(t){let e=0;for(let i=this.activeTo.length-1;i>=0&&this.activeT...
function Dt (line 1) | function Dt(t,e,i,n,r,s){t.goto(e),i.goto(n);let o=n+r,a=n,l=n-e,h=!!s.b...
function Bt (line 1) | function Bt(t,e){if(t.length!=e.length)return!1;for(let i=0;i<t.length;i...
function jt (line 1) | function jt(t,e){for(let i=e,n=t.length-1;i<n;i++)t[i]=t[i+1];t.pop()}
function It (line 1) | function It(t,e,i){for(let n=t.length-1;n>=e;n--)t[n+1]=t[n];t[e]=i}
function Gt (line 1) | function Gt(t,e){let i=-1,n=1e9;for(let r=0;r<e.length;r++)(e[r]-n||t[r]...
function Nt (line 1) | function Nt(t,e,i=t.length){let n=0;for(let r=0;r<i&&r<t.length;)9==t.ch...
function Ut (line 1) | function Ut(t,e,i,n){for(let r=0,s=0;;){if(s>=e)return r;if(r==t.length)...
class Jt (line 1) | class Jt{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(t...
method constructor (line 1) | constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(t){retur...
method getRules (line 1) | getRules(){return this.rules.join("\n")}
method newName (line 1) | static newName(){let t=Kt[Ht]||1;return Kt[Ht]=t+1,"ͼ"+t.toString(36)}
method mount (line 1) | static mount(t,e,i){let n=t[Ft],r=i&&i.nonce;n?r&&n.setNonce(r):n=new ...
class ee (line 1) | class ee{constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t...
method constructor (line 1) | constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t.head&&...
method mount (line 1) | mount(t,e){let i=this.sheet,n=0,r=0;for(let s=0;s<t.length;s++){let e=...
method setNonce (line 1) | setNonce(t){this.styleTag&&this.styleTag.getAttribute("nonce")!=t&&thi...
function le (line 1) | function le(){var t=arguments[0];"string"==typeof t&&(t=document.createE...
function he (line 1) | function he(t,e){if("string"==typeof e)t.appendChild(document.createText...
function xe (line 1) | function xe(t,e){for(let i in t)"class"==i&&e.class?e.class+=" "+t.class...
function ye (line 1) | function ye(t,e,i){if(t==e)return!0;t||(t=Se),e||(e=Se);let n=Object.key...
function ke (line 1) | function ke(t,e,i){let n=!1;if(e)for(let r in e)i&&r in i||(n=!0,"style"...
function $e (line 1) | function $e(t){let e=Object.create(null);for(let i=0;i<t.attributes.leng...
class Pe (line 1) | class Pe{eq(t){return!1}updateDOM(t,e){return!1}compare(t){return this==...
method eq (line 1) | eq(t){return!1}
method updateDOM (line 1) | updateDOM(t,e){return!1}
method compare (line 1) | compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}
method estimatedHeight (line 1) | get estimatedHeight(){return-1}
method lineBreaks (line 1) | get lineBreaks(){return 0}
method ignoreEvent (line 1) | ignoreEvent(t){return!0}
method coordsAt (line 1) | coordsAt(t,e,i){return null}
method isHidden (line 1) | get isHidden(){return!1}
method editable (line 1) | get editable(){return!1}
method destroy (line 1) | destroy(t){}
class Ce (line 1) | class Ce extends Xt{constructor(t,e,i,n){super(),this.startSide=t,this.e...
method constructor (line 1) | constructor(t,e,i,n){super(),this.startSide=t,this.endSide=e,this.widg...
method heightRelevant (line 1) | get heightRelevant(){return!1}
method mark (line 1) | static mark(t){return new Ze(t)}
method widget (line 1) | static widget(t){let e=Math.max(-1e4,Math.min(1e4,t.side||0)),i=!!t.bl...
method replace (line 1) | static replace(t){let e,i,n=!!t.block;if(t.isBlockGap)e=-5e8,i=4e8;els...
method line (line 1) | static line(t){return new Xe(t)}
method set (line 1) | static set(t,e=!1){return _t.of(t,e)}
method hasHeight (line 1) | hasHeight(){return!!this.widget&&this.widget.estimatedHeight>-1}
class Ze (line 1) | class Ze extends Ce{constructor(t){let{start:e,end:i}=Me(t);super(e?-1:5...
method constructor (line 1) | constructor(t){let{start:e,end:i}=Me(t);super(e?-1:5e8,i?1:-6e8,null,t...
method eq (line 1) | eq(t){return this==t||t instanceof Ze&&this.tagName==t.tagName&&ye(thi...
method range (line 1) | range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be...
class Xe (line 1) | class Xe extends Ce{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return ...
method constructor (line 1) | constructor(t){super(-2e8,-2e8,null,t)}
method eq (line 1) | eq(t){return t instanceof Xe&&this.spec.class==t.spec.class&&ye(this.s...
method range (line 1) | range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must...
class Ae (line 1) | class Ae extends Ce{constructor(t,e,i,n,r,s){super(e,i,r,t),this.block=n...
method constructor (line 1) | constructor(t,e,i,n,r,s){super(e,i,r,t),this.block=n,this.isReplace=s,...
method type (line 1) | get type(){return this.startSide!=this.endSide?Te.WidgetRange:this.sta...
method heightRelevant (line 1) | get heightRelevant(){return this.block||!!this.widget&&(this.widget.es...
method eq (line 1) | eq(t){return t instanceof Ae&&(e=this.widget,i=t.widget,e==i||!!(e&&i&...
method range (line 1) | range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.end...
function Me (line 1) | function Me(t,e=!1){let{inclusiveStart:i,inclusiveEnd:n}=t;return null==...
function Re (line 1) | function Re(t,e,i,n=0){let r=i.length-1;r>=0&&i[r]+n>=t?i[r]=Math.max(i[...
class ze (line 1) | class ze extends Xt{constructor(t,e){super(),this.tagName=t,this.attribu...
method constructor (line 1) | constructor(t,e){super(),this.tagName=t,this.attributes=e}
method eq (line 1) | eq(t){return t==this||t instanceof ze&&this.tagName==t.tagName&&ye(thi...
method create (line 1) | static create(t){return new ze(t.tagName,t.attributes||Se)}
method set (line 1) | static set(t,e=!1){return _t.of(t,e)}
function _e (line 1) | function _e(t){let e;return e=11==t.nodeType?t.getSelection?t:t.ownerDoc...
function Ee (line 1) | function Ee(t,e){return!!e&&(t==e||t.contains(1!=e.nodeType?e.parentNode...
function Ye (line 1) | function Ye(t,e){if(!e.anchorNode)return!1;try{return Ee(t,e.anchorNode)...
function Le (line 1) | function Le(t){return 3==t.nodeType?Ke(t,0,t.nodeValue.length).getClient...
function qe (line 1) | function qe(t,e,i,n){return!!i&&(De(t,e,i,n,-1)||De(t,e,i,n,1))}
function Ve (line 1) | function Ve(t){for(var e=0;;e++)if(!(t=t.previousSibling))return e}
function We (line 1) | function We(t){return 1==t.nodeType&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|...
function De (line 1) | function De(t,e,i,n,r){for(;;){if(t==i&&e==n)return!0;if(e==(r<0?0:Be(t)...
function Be (line 1) | function Be(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}
function je (line 1) | function je(t,e){let i=e?t.left:t.right;return{left:i,right:i,top:t.top,...
function Ie (line 1) | function Ie(t){let e=t.visualViewport;return e?{left:0,right:e.width,top...
function Ge (line 1) | function Ge(t,e){let i=e.width/t.offsetWidth,n=e.height/t.offsetHeight;r...
class Ne (line 1) | class Ne{constructor(){this.anchorNode=null,this.anchorOffset=0,this.foc...
method constructor (line 1) | constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=...
method eq (line 1) | eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.ancho...
method setRange (line 1) | setRange(t){let{anchorNode:e,focusNode:i}=t;this.set(e,Math.min(t.anch...
method set (line 1) | set(t,e,i,n){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,th...
function Fe (line 1) | function Fe(t){if(t.setActive)return t.setActive();if(He)return t.focus(...
function Ke (line 1) | function Ke(t,e,i=e){let n=Ue||(Ue=document.createRange());return n.setE...
function Je (line 1) | function Je(t,e,i,n){let r={key:e,code:e,keyCode:i,which:i,cancelable:!0...
function ti (line 1) | function ti(t){return t.scrollTop>Math.max(1,t.scrollHeight-t.clientHeig...
function ei (line 1) | function ei(t,e){for(let i=t,n=e;;){if(3==i.nodeType&&n>0)return{node:i,...
function ii (line 1) | function ii(t,e){for(let i=t,n=e;;){if(3==i.nodeType&&n<i.nodeValue.leng...
class ni (line 1) | class ni{constructor(t,e,i=!0){this.node=t,this.offset=e,this.precise=i}...
method constructor (line 1) | constructor(t,e,i=!0){this.node=t,this.offset=e,this.precise=i}
method before (line 1) | static before(t,e){return new ni(t.parentNode,Ve(t),e)}
method after (line 1) | static after(t,e){return new ni(t.parentNode,Ve(t)+1,e)}
function ai (line 1) | function ai(t){let e=[];for(let i=0;i<t.length;i++)e.push(1<<+t[i]);retu...
function di (line 1) | function di(t){return t<=247?li[t]:1424<=t&&t<=1524?2:1536<=t&&t<=1785?h...
class Oi (line 1) | class Oi{get dir(){return this.level%2?oi:si}constructor(t,e,i){this.fro...
method dir (line 1) | get dir(){return this.level%2?oi:si}
method constructor (line 1) | constructor(t,e,i){this.from=t,this.to=e,this.level=i}
method side (line 1) | side(t,e){return this.dir==e==t?this.to:this.from}
method forward (line 1) | forward(t,e){return t==(this.dir==e)}
method find (line 1) | static find(t,e,i,n){let r=-1;for(let s=0;s<t.length;s++){let o=t[s];i...
function pi (line 1) | function pi(t,e){if(t.length!=e.length)return!1;for(let i=0;i<t.length;i...
function gi (line 1) | function gi(t,e,i,n,r,s,o){let a=n%2?2:1;if(n%2==r%2)for(let l=e,h=0;l<i...
function Qi (line 1) | function Qi(t,e,i,n,r,s,o){let a=e%2?2:1;!function(t,e,i,n,r){for(let s=...
function bi (line 1) | function bi(t){return[new Oi(0,t,0)]}
function wi (line 1) | function wi(t,e,i,n,r){var s;let o=n.head-t.from,a=Oi.find(e,o,null!==(s...
function xi (line 1) | function xi(t,e,i){for(let n=e;n<i;n++){let e=di(t.charCodeAt(n));if(1==...
class zi (line 1) | class zi{constructor(t,e="nearest",i="nearest",n=5,r=5,s=!1){this.range=...
method constructor (line 1) | constructor(t,e="nearest",i="nearest",n=5,r=5,s=!1){this.range=t,this....
method map (line 1) | map(t){return t.empty?this:new zi(this.range.map(t),this.y,this.x,this...
method clip (line 1) | clip(t){return this.range.to<=t.doc.length?this:new zi(Y.cursor(t.doc....
function Yi (line 1) | function Yi(t,e,i){let n=t.facet($i);n.length?n[0](e):window.onerror&&wi...
class Wi (line 1) | class Wi{constructor(t,e,i,n,r){this.id=t,this.create=e,this.domEventHan...
method constructor (line 1) | constructor(t,e,i,n,r){this.id=t,this.create=e,this.domEventHandlers=i...
method of (line 1) | of(t){return this.baseExtensions.concat(Vi.of({plugin:this,arg:t}))}
method define (line 1) | static define(t,e){const{eventHandlers:i,eventObservers:n,provide:r,de...
method fromClass (line 1) | static fromClass(t,e){return Wi.define((e,i)=>new t(e,i),e)}
class Di (line 1) | class Di{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null...
method constructor (line 1) | constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}
method plugin (line 1) | get plugin(){return this.spec&&this.spec.plugin}
method update (line 1) | update(t){if(this.value){if(this.mustUpdate){let t=this.mustUpdate;if(...
method destroy (line 1) | destroy(t){var e;if(null===(e=this.value)||void 0===e?void 0:e.destroy...
method deactivate (line 1) | deactivate(){this.spec=this.value=null}
function Fi (line 1) | function Fi(t,e){let i=t.state.facet(Hi);if(!i.length)return i;let n=i.m...
function Ji (line 1) | function Ji(t){let e=0,i=0,n=0,r=0;for(let s of t.state.facet(Ki)){let o...
class en (line 1) | class en{constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this....
method constructor (line 1) | constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this.toB=n}
method join (line 1) | join(t){return new en(Math.min(this.fromA,t.fromA),Math.max(this.toA,t...
method addToSet (line 1) | addToSet(t){let e=t.length,i=this;for(;e>0;e--){let n=t[e-1];if(!(n.fr...
method extendWithRanges (line 1) | static extendWithRanges(t,e){if(0==e.length)return t;let i=[];for(let ...
class nn (line 1) | class nn{constructor(t,e,i){this.view=t,this.state=e,this.transactions=i...
method constructor (line 1) | constructor(t,e,i){this.view=t,this.state=e,this.transactions=i,this.f...
method create (line 1) | static create(t,e,i){return new nn(t,e,i)}
method viewportChanged (line 1) | get viewportChanged(){return(4&this.flags)>0}
method viewportMoved (line 1) | get viewportMoved(){return(8&this.flags)>0}
method heightChanged (line 1) | get heightChanged(){return(2&this.flags)>0}
method geometryChanged (line 1) | get geometryChanged(){return this.docChanged||(18&this.flags)>0}
method focusChanged (line 1) | get focusChanged(){return(1&this.flags)>0}
method docChanged (line 1) | get docChanged(){return!this.changes.empty}
method selectionSet (line 1) | get selectionSet(){return this.transactions.some(t=>t.selection)}
method empty (line 1) | get empty(){return 0==this.flags&&0==this.transactions.length}
class sn (line 1) | class sn{constructor(t,e,i=0){this.dom=t,this.length=e,this.flags=i,this...
method constructor (line 1) | constructor(t,e,i=0){this.dom=t,this.length=e,this.flags=i,this.parent...
method breakAfter (line 1) | get breakAfter(){return 1&this.flags}
method children (line 1) | get children(){return rn}
method isWidget (line 1) | isWidget(){return!1}
method isHidden (line 1) | get isHidden(){return!1}
method isComposite (line 1) | isComposite(){return!1}
method isLine (line 1) | isLine(){return!1}
method isText (line 1) | isText(){return!1}
method isBlock (line 1) | isBlock(){return!1}
method domAttrs (line 1) | get domAttrs(){return null}
method sync (line 1) | sync(t){if(this.flags|=2,4&this.flags){this.flags&=-5;let t=this.domAt...
method toString (line 1) | toString(){return this.constructor.name+(this.children.length?`(${this...
method destroy (line 1) | destroy(){this.parent=null}
method setDOM (line 1) | setDOM(t){this.dom=t,t.cmTile=this}
method posAtStart (line 1) | get posAtStart(){return this.parent?this.parent.posBefore(this):0}
method posAtEnd (line 1) | get posAtEnd(){return this.posAtStart+this.length}
method posBefore (line 1) | posBefore(t,e=this.posAtStart){let i=e;for(let n of this.children){if(...
method posAfter (line 1) | posAfter(t){return this.posBefore(t)+t.length}
method covers (line 1) | covers(t){return!0}
method coordsIn (line 1) | coordsIn(t,e){return null}
method domPosFor (line 1) | domPosFor(t,e){let i=Ve(this.dom),n=this.length?t>0:e>0;return new ni(...
method markDirty (line 1) | markDirty(t){this.flags&=-3,t&&(this.flags|=4),this.parent&&2&this.par...
method overrideDOMText (line 1) | get overrideDOMText(){return null}
method root (line 1) | get root(){for(let t=this;t;t=t.parent)if(t instanceof ln)return t;ret...
method get (line 1) | static get(t){return t.cmTile}
class on (line 1) | class on extends sn{constructor(t){super(t,0),this._children=[]}isCompos...
method constructor (line 1) | constructor(t){super(t,0),this._children=[]}
method isComposite (line 1) | isComposite(){return!0}
method children (line 1) | get children(){return this._children}
method lastChild (line 1) | get lastChild(){return this.children.length?this.children[this.childre...
method append (line 1) | append(t){this.children.push(t),t.parent=this}
method sync (line 1) | sync(t){if(2&this.flags)return;super.sync(t);let e,i=this.dom,n=null,r...
function an (line 1) | function an(t){let e=t.nextSibling;return t.parentNode.removeChild(t),e}
class ln (line 1) | class ln extends on{constructor(t,e){super(e),this.view=t}owns(t){for(;t...
method constructor (line 1) | constructor(t,e){super(e),this.view=t}
method owns (line 1) | owns(t){for(;t;t=t.parent)if(t==this)return!0;return!1}
method isBlock (line 1) | isBlock(){return!0}
method nearest (line 1) | nearest(t){for(;;){if(!t)return null;let e=sn.get(t);if(e&&this.owns(e...
method blockTiles (line 1) | blockTiles(t){for(let e=[],i=this,n=0,r=0;;)if(n==i.children.length){i...
method resolveBlock (line 1) | resolveBlock(t,e){let i,n,r=-1,s=-1;if(this.blockTiles((o,a)=>{let l=a...
class hn (line 1) | class hn extends on{constructor(t,e){super(t),this.wrapper=e}isBlock(){r...
method constructor (line 1) | constructor(t,e){super(t),this.wrapper=e}
method isBlock (line 1) | isBlock(){return!0}
method covers (line 1) | covers(t){return!!this.children.length&&(t<0?this.children[0].covers(-...
method domAttrs (line 1) | get domAttrs(){return this.wrapper.attributes}
method of (line 1) | static of(t,e){let i=new hn(e||document.createElement(t.tagName),t);re...
class cn (line 1) | class cn extends on{constructor(t,e){super(t),this.attrs=e}isLine(){retu...
method constructor (line 1) | constructor(t,e){super(t),this.attrs=e}
method isLine (line 1) | isLine(){return!0}
method start (line 1) | static start(t,e,i){let n=new cn(e||document.createElement("div"),t);r...
method domAttrs (line 1) | get domAttrs(){return this.attrs}
method resolveInline (line 1) | resolveInline(t,e,i){let n=null,r=-1,s=null,o=-1;!function t(a,l){for(...
method coordsIn (line 1) | coordsIn(t,e){let i=this.resolveInline(t,e,!0);return i?i.tile.coordsI...
method domIn (line 1) | domIn(t,e){let i=this.resolveInline(t,e);if(i){let{tile:t,offset:n}=i;...
function un (line 1) | function un(t,e){let i=t.coordsIn(0,1),n=e.coordsIn(0,1);return i&&n&&n....
class dn (line 1) | class dn extends on{constructor(t,e){super(t),this.mark=e}get domAttrs()...
method constructor (line 1) | constructor(t,e){super(t),this.mark=e}
method domAttrs (line 1) | get domAttrs(){return this.mark.attrs}
method of (line 1) | static of(t,e){let i=new dn(e||document.createElement(t.tagName),t);re...
class fn (line 1) | class fn extends sn{constructor(t,e){super(t,e.length),this.text=e}sync(...
method constructor (line 1) | constructor(t,e){super(t,e.length),this.text=e}
method sync (line 1) | sync(t){2&this.flags||(super.sync(t),this.dom.nodeValue!=this.text&&(t...
method isText (line 1) | isText(){return!0}
method toString (line 1) | toString(){return JSON.stringify(this.text)}
method coordsIn (line 1) | coordsIn(t,e){let i=this.dom.nodeValue.length;t>i&&(t=i);let n=t,r=t,s...
method of (line 1) | static of(t,e){let i=new fn(e||document.createTextNode(t),t);return e|...
class On (line 1) | class On extends sn{constructor(t,e,i,n){super(t,e,n),this.widget=i}isWi...
method constructor (line 1) | constructor(t,e,i,n){super(t,e,n),this.widget=i}
method isWidget (line 1) | isWidget(){return!0}
method isHidden (line 1) | get isHidden(){return this.widget.isHidden}
method covers (line 1) | covers(t){return!(48&this.flags)&&(this.flags&(t<0?64:128))>0}
method coordsIn (line 1) | coordsIn(t,e){return this.coordsInWidget(t,e,!1)}
method coordsInWidget (line 1) | coordsInWidget(t,e,i){let n=this.widget.coordsAt(this.dom,t,e);if(n)re...
method overrideDOMText (line 1) | get overrideDOMText(){if(!this.length)return f.empty;let{root:t}=this;...
method destroy (line 1) | destroy(){super.destroy(),this.widget.destroy(this.dom)}
method of (line 1) | static of(t,e,i,n,r){return r||(r=t.toDOM(e),t.editable||(r.contentEdi...
class pn (line 1) | class pn extends sn{constructor(t){let e=document.createElement("img");e...
method constructor (line 1) | constructor(t){let e=document.createElement("img");e.className="cm-wid...
method isHidden (line 1) | get isHidden(){return!0}
method overrideDOMText (line 1) | get overrideDOMText(){return f.empty}
method coordsIn (line 1) | coordsIn(t){return this.dom.getBoundingClientRect()}
class mn (line 1) | class mn{constructor(t){this.index=0,this.beforeBreak=!1,this.parents=[]...
method constructor (line 1) | constructor(t){this.index=0,this.beforeBreak=!1,this.parents=[],this.t...
method advance (line 1) | advance(t,e,i){let{tile:n,index:r,beforeBreak:s,parents:o}=this;for(;t...
method root (line 1) | get root(){return this.parents.length?this.parents[0].tile:this.tile}
class gn (line 1) | class gn{constructor(t,e,i,n){this.from=t,this.to=e,this.wrapper=i,this....
method constructor (line 1) | constructor(t,e,i,n){this.from=t,this.to=e,this.wrapper=i,this.rank=n}
class Qn (line 1) | class Qn{constructor(t,e,i){this.cache=t,this.root=e,this.blockWrappers=...
method constructor (line 1) | constructor(t,e,i){this.cache=t,this.root=e,this.blockWrappers=i,this....
method addText (line 1) | addText(t,e,i,n){var r;this.flushBuffer();let s=this.ensureMarks(e,i),...
method addComposition (line 1) | addComposition(t,e){let i=this.curLine;i.dom!=e.line.dom&&(i.setDOM(th...
method addInlineWidget (line 1) | addInlineWidget(t,e,i){let n=this.afterWidget&&48&t.flags&&(48&this.af...
method addMark (line 1) | addMark(t,e,i){this.flushBuffer(),this.ensureMarks(e,i).append(t),this...
method addBlockWidget (line 1) | addBlockWidget(t){this.getBlockPos().append(t),this.pos+=t.length,this...
method continueWidget (line 1) | continueWidget(t){(this.afterWidget||this.lastBlock).length+=t,this.po...
method addLineStart (line 1) | addLineStart(t,e){var i;t||(t=yn);let n=cn.start(t,e||(null===(i=this....
method addLine (line 1) | addLine(t){this.getBlockPos().append(t),this.pos+=t.length,this.lastBl...
method addBreak (line 1) | addBreak(){this.lastBlock.flags|=1,this.endLine(),this.pos++}
method addLineStartIfNotCovered (line 1) | addLineStartIfNotCovered(t){this.blockPosCovered()||this.addLineStart(t)}
method ensureLine (line 1) | ensureLine(t){this.curLine||this.addLineStart(t)}
method ensureMarks (line 1) | ensureMarks(t,e){var i;let n=this.curLine;for(let r=t.length-1;r>=0;r-...
method endLine (line 1) | endLine(){if(this.curLine){this.flushBuffer();let t=this.curLine.lastC...
method updateBlockWrappers (line 1) | updateBlockWrappers(){this.wrapperPos>this.pos+1e4&&(this.blockWrapper...
method getBlockPos (line 1) | getBlockPos(){var t;this.updateBlockWrappers();let e=this.root;for(let...
method blockPosCovered (line 1) | blockPosCovered(){let t=this.lastBlock;return null!=t&&!t.breakAfter&&...
method getBuffer (line 1) | getBuffer(t){let e=2|(t<0?16:32),i=this.cache.find(pn,void 0,1);return...
method flushBuffer (line 1) | flushBuffer(){!this.afterWidget||32&this.afterWidget.flags||(this.afte...
class bn (line 1) | class bn{constructor(t){this.skipCount=0,this.text="",this.textOff=0,thi...
method constructor (line 1) | constructor(t){this.skipCount=0,this.text="",this.textOff=0,this.curso...
method skip (line 1) | skip(t){this.textOff+t<=this.text.length?this.textOff+=t:(this.skipCou...
method next (line 1) | next(t){if(this.textOff==this.text.length){let{value:e,lineBreak:i,don...
class wn (line 1) | class wn{constructor(t){this.view=t,this.buckets=vn.map(()=>[]),this.ind...
method constructor (line 1) | constructor(t){this.view=t,this.buckets=vn.map(()=>[]),this.index=vn.m...
method add (line 1) | add(t){t.demo;let e=t.constructor.bucket,i=this.buckets[e];i.length<6?...
method find (line 1) | find(t,e,i=2){let n=t.bucket,r=this.buckets[n],s=this.index[n];for(let...
method findWidget (line 1) | findWidget(t,e,i){let n=this.buckets[0];if(t.demo,n.length)for(let r=0...
method reuse (line 1) | reuse(t){return this.reused.set(t,1),t}
method maybeReuse (line 1) | maybeReuse(t,e=2){if(!this.reused.has(t))return this.reused.set(t,e),t...
method clear (line 1) | clear(){for(let t=0;t<this.buckets.length;t++)this.buckets[t].length=t...
class xn (line 1) | class xn{constructor(t,e,i,n,r){this.view=t,this.decorations=n,this.disa...
method constructor (line 1) | constructor(t,e,i,n,r){this.view=t,this.decorations=n,this.disallowBlo...
method run (line 1) | run(t,e){let i=e&&this.getCompositionContext(e.text);for(let n=0,r=0,s...
method preserve (line 1) | preserve(t,e,i){let n=function(t){let e=[];for(let i=t.parents.length;...
method emit (line 1) | emit(t,e){let i=null,n=this.builder,r=0,s=_t.spans(this.decorations,t,...
method forward (line 1) | forward(t,e){e-t<=10?this.old.advance(e-t,1,this.reuseWalker):(this.ol...
method getCompositionContext (line 1) | getCompositionContext(t){let e=[],i=null;for(let n=t.parentNode;;n=n.p...
function Sn (line 1) | function Sn(t,e){let i=t=>{for(let n of t.children)if((e?n.isText():n.le...
function kn (line 1) | function kn(t){let e=sn.get(t);return e&&e.setDOM(t.cloneNode()),t}
class $n (line 1) | class $n extends Pe{constructor(t){super(),this.tag=t}eq(t){return t.tag...
method constructor (line 1) | constructor(t){super(),this.tag=t}
method eq (line 1) | eq(t){return t.tag==this.tag}
method toDOM (line 1) | toDOM(){return document.createElement(this.tag)}
method updateDOM (line 1) | updateDOM(t){return t.nodeName.toLowerCase()==this.tag}
method isHidden (line 1) | get isHidden(){return!0}
method toDOM (line 1) | toDOM(){return document.createElement("br")}
method isHidden (line 1) | get isHidden(){return!0}
method editable (line 1) | get editable(){return!0}
class Tn (line 1) | class Tn{constructor(t){this.view=t,this.decorations=[],this.blockWrappe...
method constructor (line 1) | constructor(t){this.view=t,this.decorations=[],this.blockWrappers=[],t...
method update (line 1) | update(t){var e;let i=t.changedRanges;this.minWidth>0&&i.length&&(i.ev...
method updateInner (line 1) | updateInner(t,e){this.view.viewState.mustMeasureContent=!0;let{observe...
method updateEditContextFormatting (line 1) | updateEditContextFormatting(t){this.editContextFormatting=this.editCon...
method updateSelection (line 1) | updateSelection(t=!1,e=!1){!t&&this.view.observer.selectionRange.focus...
method suppressWidgetCursorChange (line 1) | suppressWidgetCursorChange(t,e){return this.hasComposition&&e.empty&&q...
method enforceCursorAssoc (line 1) | enforceCursorAssoc(){if(this.hasComposition)return;let{view:t}=this,e=...
method posFromDOM (line 1) | posFromDOM(t,e){let i=this.tile.nearest(t);if(!i)return 2&this.tile.do...
method domAtPos (line 1) | domAtPos(t,e){let{tile:i,offset:n}=this.tile.resolveBlock(t,e);return ...
method inlineDOMNearPos (line 1) | inlineDOMNearPos(t,e){let i,n,r=-1,s=!1,o=-1,a=!1;return this.tile.blo...
method coordsAt (line 1) | coordsAt(t,e){let{tile:i,offset:n}=this.tile.resolveBlock(t,e);return ...
method lineAt (line 1) | lineAt(t,e){let{tile:i}=this.tile.resolveBlock(t,e);return i.isLine()?...
method coordsForChar (line 1) | coordsForChar(t){let{tile:e,offset:i}=this.tile.resolveBlock(t,1);if(!...
method measureVisibleLineHeights (line 1) | measureVisibleLineHeights(t){let e=[],{from:i,to:n}=t,r=this.view.cont...
method textDirectionAt (line 1) | textDirectionAt(t){let{tile:e}=this.tile.resolveBlock(t,1);return"rtl"...
method measureTextSize (line 1) | measureTextSize(){let t=this.tile.blockTiles(t=>{if(t.isLine()&&t.chil...
method computeBlockGapDeco (line 1) | computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,n=0;;...
method updateDeco (line 1) | updateDeco(){let t=1,e=this.view.state.facet(Ii).map(e=>(this.dynamicD...
method scrollIntoView (line 1) | scrollIntoView(t){if(t.isSnapshot){let e=this.view.viewState.lineBlock...
method lineHasWidget (line 1) | lineHasWidget(t){let e=t=>t.isWidget()||t.children.some(e);return e(th...
method destroy (line 1) | destroy(){Cn(this.tile)}
function Cn (line 1) | function Cn(t,e){let i=null==e?void 0:e.get(t);if(1!=i){null==i&&t.destr...
function Zn (line 1) | function Zn(t,e){let i=t.observer.selectionRange;if(!i.focusNode)return ...
method constructor (line 1) | constructor(){this.changes=[]}
method compareRange (line 1) | compareRange(t,e){Re(t,e,this.changes)}
method comparePoint (line 1) | comparePoint(t,e){Re(t,e,this.changes)}
method boundChange (line 1) | boundChange(t){Re(t,t,this.changes)}
class An (line 1) | class An{constructor(){this.changes=[]}compareRange(t,e){Re(t,e,this.cha...
method constructor (line 1) | constructor(){this.changes=[]}
method compareRange (line 1) | compareRange(t,e){Re(t,e,this.changes)}
method comparePoint (line 1) | comparePoint(){}
method boundChange (line 1) | boundChange(t){Re(t,t,this.changes)}
class Mn (line 1) | class Mn extends Pe{constructor(t){super(),this.height=t}toDOM(){let t=d...
method constructor (line 1) | constructor(t){super(),this.height=t}
method toDOM (line 1) | toDOM(){let t=document.createElement("div");return t.className="cm-gap...
method eq (line 1) | eq(t){return t.height==this.height}
method updateDOM (line 1) | updateDOM(t){return t.style.height=this.height+"px",!0}
method editable (line 1) | get editable(){return!0}
method estimatedHeight (line 1) | get estimatedHeight(){return this.height}
method ignoreEvent (line 1) | ignoreEvent(){return!1}
function Rn (line 1) | function Rn(t,e,i){let n=t.lineBlockAt(e);if(Array.isArray(n.type)){let ...
function zn (line 1) | function zn(t,e,i,n){let r=t.state.doc.lineAt(e.head),s=t.bidiSpans(r),o...
function _n (line 1) | function _n(t,e,i){for(;;){let n=0;for(let r of t)r.between(e-1,e+1,(t,r...
function En (line 1) | function En(t,e){let i=null;for(let n=0;n<e.ranges.length;n++){let r=e.r...
function Yn (line 1) | function Yn(t,e,i){let n=_n(t.state.facet(Ui).map(e=>e(t)),i.from,e.head...
class Ln (line 1) | class Ln{constructor(t,e){this.pos=t,this.assoc=e}}
method constructor (line 1) | constructor(t,e){this.pos=t,this.assoc=e}
function qn (line 1) | function qn(t,e,i,n){let r,s=t.contentDOM.getBoundingClientRect(),o=s.to...
function Vn (line 1) | function Vn(t,e,i,n,r){let s=-1,o=null,a=1e9,l=1e9,h=r,c=r,u=(t,e)=>{for...
function Wn (line 1) | function Wn(t,e){let i=t.state.doc.lineAt(e);return t.bidiSpans(i)[Oi.fi...
class Bn (line 1) | class Bn{constructor(t,e){this.points=t,this.view=e,this.text="",this.li...
method constructor (line 1) | constructor(t,e){this.points=t,this.view=e,this.text="",this.lineSepar...
method append (line 1) | append(t){this.text+=t}
method lineBreak (line 1) | lineBreak(){this.text+=Dn}
method readRange (line 1) | readRange(t,e){if(!t)return this;let i=t.parentNode;for(let n=t;;){thi...
method readTextNode (line 1) | readTextNode(t){let e=t.nodeValue;for(let i of this.points)i.node==t&&...
method readNode (line 1) | readNode(t){let e=sn.get(t),i=e&&e.overrideDOMText;if(null!=i){this.fi...
method findPointBefore (line 1) | findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[...
method findPointInside (line 1) | findPointInside(t,e){for(let i of this.points)(3==t.nodeType?i.node==t...
function jn (line 1) | function jn(t,e,i){for(;;){if(!e||i<Be(e))return!1;if(e==t)return!0;i=Ve...
function In (line 1) | function In(t,e){let i;for(;t!=e&&t;t=t.nextSibling){let e=sn.get(t);if(...
class Gn (line 1) | class Gn{constructor(t,e){this.node=t,this.offset=e,this.pos=-1}}
method constructor (line 1) | constructor(t,e){this.node=t,this.offset=e,this.pos=-1}
class Nn (line 1) | class Nn{constructor(t,e,i,n){this.typeOver=n,this.bounds=null,this.text...
method constructor (line 1) | constructor(t,e,i,n){this.typeOver=n,this.bounds=null,this.text="",thi...
function Un (line 1) | function Un(t,e,i,n){if(t.isComposite()){let r=-1,s=-1,o=-1,a=-1;for(let...
function Hn (line 1) | function Hn(t,e){let i,{newSel:n}=e,r=t.state.selection.main,s=t.inputSt...
function Fn (line 1) | function Fn(t,e,i,n=-1){if(we.ios&&t.inputState.flushIOSKey(e))return!0;...
function Kn (line 1) | function Kn(t,e,i,n){let r=Math.min(t.length,e.length),s=0;for(;s<r&&t.c...
class Jn (line 1) | class Jn{setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelec...
method setSelectionOrigin (line 1) | setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTim...
method constructor (line 1) | constructor(t){var e;this.view=t,this.lastKeyCode=0,this.lastKeyTime=0...
method handleEvent (line 1) | handleEvent(t){(function(t,e){if(!e.bubbles)return!0;if(e.defaultPreve...
method runHandlers (line 1) | runHandlers(t,e){let i=this.handlers[t];if(i){for(let t of i.observers...
method ensureHandlers (line 1) | ensureHandlers(t){let e=function(t){let e=Object.create(null);function...
method keydown (line 1) | keydown(t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),9...
method flushIOSKey (line 1) | flushIOSKey(t){let e=this.pendingIOSKey;return!!e&&(!("Enter"==e.key&&...
method ignoreDuringComposition (line 1) | ignoreDuringComposition(t){return!(!/^key/.test(t.type)||t.synthetic)&...
method startMouseSelection (line 1) | startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destro...
method update (line 1) | update(t){this.view.observer.update(t),this.mouseSelection&&this.mouse...
method destroy (line 1) | destroy(){this.mouseSelection&&this.mouseSelection.destroy()}
function tr (line 1) | function tr(t,e){return(i,n)=>{try{return e.call(t,n,i)}catch(r){Yi(i.st...
function rr (line 1) | function rr(t){return.7*Math.max(0,t)+8}
class sr (line 1) | class sr{constructor(t,e,i,n){this.view=t,this.startEvent=e,this.style=i...
method constructor (line 1) | constructor(t,e,i,n){this.view=t,this.startEvent=e,this.style=i,this.m...
method start (line 1) | start(t){!1===this.dragging&&this.select(t)}
method move (line 1) | move(t){if(0==t.buttons)return this.destroy();if(this.dragging||null==...
method up (line 1) | up(t){null==this.dragging&&this.select(this.lastEvent),this.dragging||...
method destroy (line 1) | destroy(){this.setScrollSpeed(0,0);let t=this.view.contentDOM.ownerDoc...
method setScrollSpeed (line 1) | setScrollSpeed(t,e){this.scrollSpeed={x:t,y:e},t||e?this.scrolling<0&&...
method scroll (line 1) | scroll(){let{x:t,y:e}=this.scrollSpeed;t&&this.scrollParents.x&&(this....
method select (line 1) | select(t){let{view:e}=this,i=En(this.atoms,this.style.get(t,this.exten...
method update (line 1) | update(t){t.transactions.some(t=>t.isUserEvent("input.type"))?this.des...
function hr (line 1) | function hr(t,e,i){for(let n of t.facet(e))i=n(i,t);return i}
function cr (line 1) | function cr(t,e){e=hr(t.state,Zi,e);let i,{state:n}=t,r=1,s=n.toText(e),...
function ur (line 1) | function ur(t,e,i,n){if(1==n)return Y.cursor(e,i);if(2==n)return functio...
method update (line 1) | update(t){t.docChanged&&(i.pos=t.changes.mapPos(i.pos),r=r.map(t.changes))}
method get (line 1) | get(e,s,o){let a,l=t.posAndSideAtCoords({x:e.clientX,y:e.clientY},!1),h=...
function mr (line 1) | function mr(t){if(!dr)return t.detail;let e=fr,i=pr;return fr=t,pr=Date....
function gr (line 1) | function gr(t,e,i,n){if(!(i=hr(t.state,Zi,i)))return;let r=t.posAtCoords...
function vr (line 1) | function vr(t,e){let i=[];for(let n of t.facet(Ci)){let r=n(t,e);r&&i.pu...
function wr (line 1) | function wr(t){setTimeout(()=>{let e=t.hasFocus;if(e!=t.inputState.notif...
function kr (line 1) | function kr(){yr=!1}
class $r (line 1) | class $r{constructor(t){this.lineWrapping=t,this.doc=f.empty,this.height...
method constructor (line 1) | constructor(t){this.lineWrapping=t,this.doc=f.empty,this.heightSamples...
method heightForGap (line 1) | heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).n...
method heightForLine (line 1) | heightForLine(t){if(!this.lineWrapping)return this.lineHeight;return(1...
method setDoc (line 1) | setDoc(t){return this.doc=t,this}
method mustRefreshForWrapping (line 1) | mustRefreshForWrapping(t){return Sr.indexOf(t)>-1!=this.lineWrapping}
method mustRefreshForHeights (line 1) | mustRefreshForHeights(t){let e=!1;for(let i=0;i<t.length;i++){let n=t[...
method refresh (line 1) | refresh(t,e,i,n,r,s){let o=Sr.indexOf(t)>-1,a=Math.round(e)!=Math.roun...
class Pr (line 1) | class Pr{constructor(t,e){this.from=t,this.heights=e,this.index=0}get mo...
method constructor (line 1) | constructor(t,e){this.from=t,this.heights=e,this.index=0}
method more (line 1) | get more(){return this.index<this.heights.length}
class Tr (line 1) | class Tr{constructor(t,e,i,n,r){this.from=t,this.length=e,this.top=i,thi...
method constructor (line 1) | constructor(t,e,i,n,r){this.from=t,this.length=e,this.top=i,this.heigh...
method type (line 1) | get type(){return"number"==typeof this._content?Te.Text:Array.isArray(...
method to (line 1) | get to(){return this.from+this.length}
method bottom (line 1) | get bottom(){return this.top+this.height}
method widget (line 1) | get widget(){return this._content instanceof Ae?this._content.widget:n...
method widgetLineBreaks (line 1) | get widgetLineBreaks(){return"number"==typeof this._content?this._cont...
method join (line 1) | join(t){let e=(Array.isArray(this._content)?this._content:[this]).conc...
class Xr (line 1) | class Xr{constructor(t,e,i=2){this.length=t,this.height=e,this.flags=i}g...
method constructor (line 1) | constructor(t,e,i=2){this.length=t,this.height=e,this.flags=i}
method outdated (line 1) | get outdated(){return(2&this.flags)>0}
method outdated (line 1) | set outdated(t){this.flags=(t?2:0)|-3&this.flags}
method setHeight (line 1) | setHeight(t){this.height!=t&&(Math.abs(this.height-t)>Zr&&(yr=!0),this...
method replace (line 1) | replace(t,e,i){return Xr.of(i)}
method decomposeLeft (line 1) | decomposeLeft(t,e){e.push(this)}
method decomposeRight (line 1) | decomposeRight(t,e){e.push(this)}
method applyChanges (line 1) | applyChanges(t,e,i,n){let r=this,s=i.doc;for(let o=n.length-1;o>=0;o--...
method empty (line 1) | static empty(){return new zr(0,0,0)}
method of (line 1) | static of(t){if(1==t.length)return t[0];let e=0,i=t.length,n=0,r=0;for...
function Ar (line 1) | function Ar(t,e){return t==e?t:(t.constructor!=e.constructor&&(yr=!0),e)}
class Rr (line 1) | class Rr extends Xr{constructor(t,e,i){super(t,e),this.deco=i,this.space...
method constructor (line 1) | constructor(t,e,i){super(t,e),this.deco=i,this.spaceAbove=0}
method mainBlock (line 1) | mainBlock(t,e){return new Tr(e,this.length,t+this.spaceAbove,this.heig...
method blockAt (line 1) | blockAt(t,e,i,n){return this.spaceAbove&&t<i+this.spaceAbove?new Tr(n,...
method lineAt (line 1) | lineAt(t,e,i,n,r){let s=this.mainBlock(n,r);return this.spaceAbove?thi...
method forEachLine (line 1) | forEachLine(t,e,i,n,r,s){t<=r+this.length&&e>=r&&s(this.lineAt(0,Cr.By...
method setMeasuredHeight (line 1) | setMeasuredHeight(t){let e=t.heights[t.index++];e<0?(this.spaceAbove=-...
method updateHeight (line 1) | updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more&&this.setMeasur...
method toString (line 1) | toString(){return`block(${this.length})`}
class zr (line 1) | class zr extends Rr{constructor(t,e,i){super(t,e,null),this.collapsed=0,...
method constructor (line 1) | constructor(t,e,i){super(t,e,null),this.collapsed=0,this.widgetHeight=...
method mainBlock (line 1) | mainBlock(t,e){return new Tr(e,this.length,t+this.spaceAbove,this.heig...
method replace (line 1) | replace(t,e,i){let n=i[0];return 1==i.length&&(n instanceof zr||n inst...
method updateHeight (line 1) | updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more?this.setMeasure...
method toString (line 1) | toString(){return`line(${this.length}${this.collapsed?-this.collapsed:...
class _r (line 1) | class _r extends Xr{constructor(t){super(t,0)}heightMetrics(t,e){let i,n...
method constructor (line 1) | constructor(t){super(t,0)}
method heightMetrics (line 1) | heightMetrics(t,e){let i,n=t.doc.lineAt(e).number,r=t.doc.lineAt(e+thi...
method blockAt (line 1) | blockAt(t,e,i,n){let{firstLine:r,lastLine:s,perLine:o,perChar:a}=this....
method lineAt (line 1) | lineAt(t,e,i,n,r){if(e==Cr.ByHeight)return this.blockAt(t,i,n,r);if(e=...
method forEachLine (line 1) | forEachLine(t,e,i,n,r,s){t=Math.max(t,r),e=Math.min(e,r+this.length);l...
method replace (line 1) | replace(t,e,i){let n=this.length-e;if(n>0){let t=i[i.length-1];t insta...
method decomposeLeft (line 1) | decomposeLeft(t,e){e.push(new _r(t-1),null)}
method decomposeRight (line 1) | decomposeRight(t,e){e.push(null,new _r(this.length-t-1))}
method updateHeight (line 1) | updateHeight(t,e=0,i=!1,n){let r=e+this.length;if(n&&n.from<=e+this.le...
method toString (line 1) | toString(){return`gap(${this.length})`}
class Er (line 1) | class Er extends Xr{constructor(t,e,i){super(t.length+e+i.length,t.heigh...
method constructor (line 1) | constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.ou...
method break (line 1) | get break(){return 1&this.flags}
method blockAt (line 1) | blockAt(t,e,i,n){let r=i+this.left.height;return t<r?this.left.blockAt...
method lineAt (line 1) | lineAt(t,e,i,n,r){let s=n+this.left.height,o=r+this.left.length+this.b...
method forEachLine (line 1) | forEachLine(t,e,i,n,r,s){let o=n+this.left.height,a=r+this.left.length...
method replace (line 1) | replace(t,e,i){let n=this.left.length+this.break;if(e<n)return this.ba...
method decomposeLeft (line 1) | decomposeLeft(t,e){let i=this.left.length;if(t<=i)return this.left.dec...
method decomposeRight (line 1) | decomposeRight(t,e){let i=this.left.length,n=i+this.break;if(t>=n)retu...
method balanced (line 1) | balanced(t,e){return t.size>2*e.size||e.size>2*t.size?Xr.of(this.break...
method updateHeight (line 1) | updateHeight(t,e=0,i=!1,n){let{left:r,right:s}=this,o=e+r.length+this....
method toString (line 1) | toString(){return this.left+(this.break?" ":"-")+this.right}
function Yr (line 1) | function Yr(t,e){let i,n;null==t[e]&&(i=t[e-1])instanceof _r&&(n=t[e+1])...
class Lr (line 1) | class Lr{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.li...
method constructor (line 1) | constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart...
method isCovered (line 1) | get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]=...
method span (line 1) | span(t,e){if(this.lineStart>-1){let t=Math.min(e,this.lineEnd),i=this....
method point (line 1) | point(t,e,i){if(t<e||i.heightRelevant){let n=i.widget?i.widget.estimat...
method enterLine (line 1) | enterLine(){if(this.lineStart>-1)return;let{from:t,to:e}=this.oracle.d...
method blankContent (line 1) | blankContent(t,e){let i=new _r(e-t);return this.oracle.doc.lineAt(t).t...
method ensureLine (line 1) | ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this....
method addBlock (line 1) | addBlock(t){this.enterLine();let e=t.deco;e&&e.startSide>0&&!this.isCo...
method addLineDeco (line 1) | addLineDeco(t,e,i){let n=this.ensureLine();n.length+=i,n.collapsed+=i,...
method finish (line 1) | finish(t){let e=0==this.nodes.length?null:this.nodes[this.nodes.length...
method build (line 1) | static build(t,e,i,n){let r=new Lr(i,t);return _t.spans(e,i,n,r,0),r.f...
class qr (line 1) | class qr{constructor(){this.changes=[]}compareRange(){}comparePoint(t,e,...
method constructor (line 1) | constructor(){this.changes=[]}
method compareRange (line 1) | compareRange(){}
method comparePoint (line 1) | comparePoint(t,e,i,n){(t<e||i&&i.heightRelevant||n&&n.heightRelevant)&...
function Vr (line 1) | function Vr(t,e){let i=t.getBoundingClientRect(),n=t.ownerDocument,r=n.d...
function Wr (line 1) | function Wr(t,e){let i=t.getBoundingClientRect();return{left:0,right:i.r...
class Dr (line 1) | class Dr{constructor(t,e,i,n){this.from=t,this.to=e,this.size=i,this.dis...
method constructor (line 1) | constructor(t,e,i,n){this.from=t,this.to=e,this.size=i,this.displaySiz...
method same (line 1) | static same(t,e){if(t.length!=e.length)return!1;for(let i=0;i<t.length...
method draw (line 1) | draw(t,e){return Ce.replace({widget:new Br(this.displaySize*(e?t.scale...
class Br (line 1) | class Br extends Pe{constructor(t,e){super(),this.size=t,this.vertical=e...
method constructor (line 1) | constructor(t,e){super(),this.size=t,this.vertical=e}
method eq (line 1) | eq(t){return t.size==this.size&&t.vertical==this.vertical}
method toDOM (line 1) | toDOM(){let t=document.createElement("div");return this.vertical?t.sty...
method estimatedHeight (line 1) | get estimatedHeight(){return this.vertical?this.size:-1}
class jr (line 1) | class jr{constructor(t){this.state=t,this.pixelViewport={left:0,right:wi...
method constructor (line 1) | constructor(t){this.state=t,this.pixelViewport={left:0,right:window.in...
method updateForViewport (line 1) | updateForViewport(){let t=[this.viewport],{main:e}=this.state.selectio...
method updateScaler (line 1) | updateScaler(){let t=this.scaler;return this.scaler=this.heightMap.hei...
method updateViewportLines (line 1) | updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine...
method update (line 1) | update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDec...
method measure (line 1) | measure(t){let e=t.contentDOM,i=window.getComputedStyle(e),n=this.heig...
method visibleTop (line 1) | get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}
method visibleBottom (line 1) | get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bott...
method getViewport (line 1) | getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),n=this.he...
method mapViewport (line 1) | mapViewport(t,e){let i=e.mapPos(t.from,-1),n=e.mapPos(t.to,1);return n...
method viewportIsAppropriate (line 1) | viewportIsAppropriate({from:t,to:e},i=0){if(!this.inView)return!0;let{...
method mapLineGaps (line 1) | mapLineGaps(t,e){if(!t.length||e.empty)return t;let i=[];for(let n of ...
method ensureLineGaps (line 1) | ensureLineGaps(t,e){let i=this.heightOracle.lineWrapping,n=i?1e4:2e3,r...
method gapSize (line 1) | gapSize(t,e,i,n){let r=Nr(n,i)-Nr(n,e);return this.heightOracle.lineWr...
method updateLineGaps (line 1) | updateLineGaps(t){Dr.same(t,this.lineGaps)||(this.lineGaps=t,this.line...
method computeVisibleRanges (line 1) | computeVisibleRanges(t){let e=this.stateDeco;this.lineGaps.length&&(e=...
method lineBlockAt (line 1) | lineBlockAt(t){return t>=this.viewport.from&&t<=this.viewport.to&&this...
method lineBlockAtHeight (line 1) | lineBlockAtHeight(t){return t>=this.viewportLines[0].top&&t<=this.view...
method scrollAnchorAt (line 1) | scrollAnchorAt(t){let e=this.lineBlockAtHeight(t+8);return e.from>=thi...
method elementAtHeight (line 1) | elementAtHeight(t){return Kr(this.heightMap.blockAt(this.scaler.fromDO...
method docHeight (line 1) | get docHeight(){return this.scaler.toDOM(this.heightMap.height)}
method contentHeight (line 1) | get contentHeight(){return this.docHeight+this.paddingTop+this.padding...
class Ir (line 1) | class Ir{constructor(t,e){this.from=t,this.to=e}}
method constructor (line 1) | constructor(t,e){this.from=t,this.to=e}
function Gr (line 1) | function Gr({total:t,ranges:e},i){if(i<=0)return e[0].from;if(i>=1)retur...
function Nr (line 1) | function Nr(t,e){let i=0;for(let{from:n,to:r}of t.ranges){if(e<=r){i+=e-...
method eq (line 1) | eq(t){return t==this}
function Hr (line 1) | function Hr(t){let e=t.facet(Ii).filter(t=>"function"!=typeof t),i=t.fac...
class Fr (line 1) | class Fr{constructor(t,e,i){let n=0,r=0,s=0;this.viewports=i.map(({from:...
method constructor (line 1) | constructor(t,e,i){let n=0,r=0,s=0;this.viewports=i.map(({from:i,to:r}...
method toDOM (line 1) | toDOM(t){for(let e=0,i=0,n=0;;e++){let r=e<this.viewports.length?this....
method fromDOM (line 1) | fromDOM(t){for(let e=0,i=0,n=0;;e++){let r=e<this.viewports.length?thi...
method eq (line 1) | eq(t){return t instanceof Fr&&(this.scale==t.scale&&this.viewports.len...
function Kr (line 1) | function Kr(t,e){if(1==e.scale)return t;let i=e.toDOM(t.top),n=e.toDOM(t...
function ss (line 1) | function ss(t,e,i){return new Jt(e,{finish:e=>/&/.test(e)?e.replace(/&\w...
class hs (line 1) | class hs{constructor(t){this.view=t,this.active=!1,this.editContext=null...
method constructor (line 1) | constructor(t){this.view=t,this.active=!1,this.editContext=null,this.s...
method onScrollChanged (line 1) | onScrollChanged(t){this.view.inputState.runHandlers("scroll",t),this.i...
method onScroll (line 1) | onScroll(t){this.intersecting&&this.flush(!1),this.editContext&&this.v...
method onResize (line 1) | onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{t...
method onPrint (line 1) | onPrint(t){("change"!=t.type&&t.type||t.matches)&&(this.view.viewState...
method updateGaps (line 1) | updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||th...
method onSelectionChange (line 1) | onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectio...
method readSelectionRange (line 1) | readSelectionRange(){let{view:t}=this,e=_e(t.root);if(!e)return!1;let ...
method setSelectionRange (line 1) | setSelectionRange(t,e){this.selectionRange.set(t.node,t.offset,e.node,...
method clearSelectionRange (line 1) | clearSelectionRange(){this.selectionRange.set(null,0,null,0)}
method listenForScroll (line 1) | listenForScroll(){this.parentCheck=-1;let t=0,e=null;for(let i=this.do...
method ignore (line 1) | ignore(t){if(!this.active)return t();try{return this.stop(),t()}finall...
method start (line 1) | start(){this.active||(this.observer.observe(this.dom,as),ls&&this.dom....
method stop (line 1) | stop(){this.active&&(this.active=!1,this.observer.disconnect(),ls&&thi...
method clear (line 1) | clear(){this.processRecords(),this.queue.length=0,this.selectionChange...
method delayAndroidKey (line 1) | delayAndroidKey(t,e){var i;if(!this.delayedAndroidKey){let t=()=>{let ...
method clearDelayedAndroidKey (line 1) | clearDelayedAndroidKey(){this.win.cancelAnimationFrame(this.flushingAn...
method flushSoon (line 1) | flushSoon(){this.delayedFlush<0&&(this.delayedFlush=this.view.win.requ...
method forceFlush (line 1) | forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame...
method pendingRecords (line 1) | pendingRecords(){for(let t of this.observer.takeRecords())this.queue.p...
method processRecords (line 1) | processRecords(){let t=this.pendingRecords();t.length&&(this.queue=[])...
method readChange (line 1) | readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),n=this....
method flush (line 1) | flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t...
method readMutation (line 1) | readMutation(t){let e=this.view.docView.tile.nearest(t.target);if(!e||...
method setWindow (line 1) | setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.w...
method addWindowListeners (line 1) | addWindowListeners(t){t.addEventListener("resize",this.onResize),this....
method removeWindowListeners (line 1) | removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll)...
method update (line 1) | update(t){this.editContext&&(this.editContext.update(t),t.startState.f...
method destroy (line 1) | destroy(){var t,e,i;this.stop(),null===(t=this.intersection)||void 0==...
function cs (line 1) | function cs(t,e,i){for(;e;){let n=sn.get(e);if(n&&n.parent==t)return n;l...
function us (line 1) | function us(t,e){let i=e.startContainer,n=e.startOffset,r=e.endContainer...
class ds (line 1) | class ds{constructor(t){this.from=0,this.to=0,this.pendingContextChange=...
method constructor (line 1) | constructor(t){this.from=0,this.to=0,this.pendingContextChange=null,th...
method applyEdits (line 1) | applyEdits(t){let e=0,i=!1,n=this.pendingContextChange;return t.change...
method update (line 1) | update(t){let e=this.pendingContextChange,i=t.startState.selection.mai...
method resetRange (line 1) | resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4)...
method reset (line 1) | reset(t){this.resetRange(t),this.editContext.updateText(0,this.editCon...
method revertPending (line 1) | revertPending(t){let e=this.pendingContextChange;this.pendingContextCh...
method setSelection (line 1) | setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(t...
method rangeIsValid (line 1) | rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-th...
method toEditorPos (line 1) | toEditorPos(t,e=this.to-this.from){t=Math.min(t,e);let i=this.composin...
method toContextPos (line 1) | toContextPos(t){let e=this.composing;return e&&e.drifted?e.contextBase...
method destroy (line 1) | destroy(){for(let t in this.handlers)this.editContext.removeEventListe...
class fs (line 1) | class fs{get state(){return this.viewState.state}get viewport(){return t...
method state (line 1) | get state(){return this.viewState.state}
method viewport (line 1) | get viewport(){return this.viewState.viewport}
method visibleRanges (line 1) | get visibleRanges(){return this.viewState.visibleRanges}
method inView (line 1) | get inView(){return this.viewState.inView}
method composing (line 1) | get composing(){return!!this.inputState&&this.inputState.composing>0}
method compositionStarted (line 1) | get compositionStarted(){return!!this.inputState&&this.inputState.comp...
method root (line 1) | get root(){return this._root}
method win (line 1) | get win(){return this.dom.ownerDocument.defaultView||window}
method constructor (line 1) | constructor(t={}){var e;this.plugins=[],this.pluginMap=new Map,this.ed...
method dispatch (line 1) | dispatch(...t){let e=1==t.length&&t[0]instanceof Qt?t:1==t.length&&Arr...
method update (line 1) | update(t){if(0!=this.updateState)throw new Error("Calls to EditorView....
method setState (line 1) | setState(t){if(0!=this.updateState)throw new Error("Calls to EditorVie...
method updatePlugins (line 1) | updatePlugins(t){let e=t.startState.facet(Vi),i=t.state.facet(Vi);if(e...
method docViewUpdate (line 1) | docViewUpdate(){for(let e of this.plugins){let i=e.value;if(i&&i.docVi...
method measure (line 1) | measure(t=!0){if(this.destroyed)return;if(this.measureScheduled>-1&&th...
method themeClasses (line 1) | get themeClasses(){return es+" "+(this.state.facet(ts)?ns:is)+" "+this...
method updateAttrs (line 1) | updateAttrs(){let t=gs(this,Bi,{class:"cm-editor"+(this.hasFocus?" cm-...
method showAnnouncements (line 1) | showAnnouncements(t){let e=!0;for(let i of t)for(let t of i.effects)if...
method mountStyles (line 1) | mountStyles(){this.styleModules=this.state.facet(tn);let t=this.state....
method readMeasured (line 1) | readMeasured(){if(2==this.updateState)throw new Error("Reading the edi...
method requestMeasure (line 1) | requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=t...
method plugin (line 1) | plugin(t){let e=this.pluginMap.get(t);return(void 0===e||e&&e.plugin!=...
method documentTop (line 1) | get documentTop(){return this.contentDOM.getBoundingClientRect().top+t...
method documentPadding (line 1) | get documentPadding(){return{top:this.viewState.paddingTop,bottom:this...
method scaleX (line 1) | get scaleX(){return this.viewState.scaleX}
method scaleY (line 1) | get scaleY(){return this.viewState.scaleY}
method elementAtHeight (line 1) | elementAtHeight(t){return this.readMeasured(),this.viewState.elementAt...
method lineBlockAtHeight (line 1) | lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlo...
method viewportLineBlocks (line 1) | get viewportLineBlocks(){return this.viewState.viewportLines}
method lineBlockAt (line 1) | lineBlockAt(t){return this.viewState.lineBlockAt(t)}
method contentHeight (line 1) | get contentHeight(){return this.viewState.contentHeight}
method moveByChar (line 1) | moveByChar(t,e,i){return Yn(this,t,zn(this,t,e,i))}
method moveByGroup (line 1) | moveByGroup(t,e){return Yn(this,t,zn(this,t,e,e=>function(t,e,i){let n...
method visualLineSide (line 1) | visualLineSide(t,e){let i=this.bidiSpans(t),n=this.textDirectionAt(t.f...
method moveToLineBoundary (line 1) | moveToLineBoundary(t,e,i=!0){return function(t,e,i,n){let r=Rn(t,e.hea...
method moveVertically (line 1) | moveVertically(t,e,i){return Yn(this,t,function(t,e,i,n){let r=e.head,...
method domAtPos (line 1) | domAtPos(t,e=1){return this.docView.domAtPos(t,e)}
method posAtDOM (line 1) | posAtDOM(t,e=0){return this.docView.posFromDOM(t,e)}
method posAtCoords (line 1) | posAtCoords(t,e=!0){this.readMeasured();let i=qn(this,t,e);return i&&i...
method posAndSideAtCoords (line 1) | posAndSideAtCoords(t,e=!0){return this.readMeasured(),qn(this,t,e)}
method coordsAtPos (line 1) | coordsAtPos(t,e=1){this.readMeasured();let i=this.docView.coordsAt(t,e...
method coordsForChar (line 1) | coordsForChar(t){return this.readMeasured(),this.docView.coordsForChar...
method defaultCharacterWidth (line 1) | get defaultCharacterWidth(){return this.viewState.heightOracle.charWidth}
method defaultLineHeight (line 1) | get defaultLineHeight(){return this.viewState.heightOracle.lineHeight}
method textDirection (line 1) | get textDirection(){return this.viewState.defaultTextDirection}
method textDirectionAt (line 1) | textDirectionAt(t){return!this.state.facet(Ai)||t<this.viewport.from||...
method lineWrapping (line 1) | get lineWrapping(){return this.viewState.heightOracle.lineWrapping}
method bidiSpans (line 1) | bidiSpans(t){if(t.length>Os)return bi(t.length);let e,i=this.textDirec...
method hasFocus (line 1) | get hasFocus(){var t;return(this.dom.ownerDocument.hasFocus()||we.safa...
method focus (line 1) | focus(){this.observer.ignore(()=>{Fe(this.contentDOM),this.docView.upd...
method setRoot (line 1) | setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((9==t....
method destroy (line 1) | destroy(){this.root.activeElement==this.contentDOM&&this.contentDOM.bl...
method scrollIntoView (line 1) | static scrollIntoView(t,e={}){return _i.of(new zi("number"==typeof t?Y...
method scrollSnapshot (line 1) | scrollSnapshot(){let{scrollTop:t,scrollLeft:e}=this.scrollDOM,i=this.v...
method setTabFocusMode (line 1) | setTabFocusMode(t){null==t?this.inputState.tabFocusMode=this.inputStat...
method domEventHandlers (line 1) | static domEventHandlers(t){return Wi.define(()=>({}),{eventHandlers:t})}
method domEventObservers (line 1) | static domEventObservers(t){return Wi.define(()=>({}),{eventObservers:...
method theme (line 1) | static theme(t,e){let i=Jt.newName(),n=[Jr.of(i),tn.of(ss(`.${i}`,t))]...
method baseTheme (line 1) | static baseTheme(t){return tt.lowest(tn.of(ss("."+es,t,rs)))}
method findFromDOM (line 1) | static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),n=i&&...
class ms (line 1) | class ms{constructor(t,e,i,n,r,s){this.from=t,this.to=e,this.dir=i,this....
method constructor (line 1) | constructor(t,e,i,n,r,s){this.from=t,this.to=e,this.dir=i,this.isolate...
method update (line 1) | static update(t,e){if(e.empty&&!t.some(t=>t.fresh))return t;let i=[],n...
function gs (line 1) | function gs(t,e,i){for(let n=t.state.facet(e),r=n.length-1;r>=0;r--){let...
function bs (line 1) | function bs(t,e,i){return e.altKey&&(t="Alt-"+t),e.ctrlKey&&(t="Ctrl-"+t...
function Ss (line 1) | function Ss(t){let e=t.facet(ws),i=xs.get(e);return i||xs.set(e,i=functi...
function Ps (line 1) | function Ps(t,e,i,n){$s=e;let r=function(t){var e=!(re&&t.metaKey&&t.shi...
class Ts (line 1) | class Ts{constructor(t,e,i,n,r){this.className=t,this.left=e,this.top=i,...
method constructor (line 1) | constructor(t,e,i,n,r){this.className=t,this.left=e,this.top=i,this.wi...
method draw (line 1) | draw(){let t=document.createElement("div");return t.className=this.cla...
method update (line 1) | update(t,e){return e.className==this.className&&(this.adjust(t),!0)}
method adjust (line 1) | adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",null!=...
method eq (line 1) | eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&...
method forRange (line 1) | static forRange(t,e,i){if(i.empty){let n=t.coordsAtPos(i.head,i.assoc|...
function Cs (line 1) | function Cs(t){let e=t.scrollDOM.getBoundingClientRect();return{left:(t....
function Zs (line 1) | function Zs(t,e,i,n){let r=t.coordsAtPos(e,2*i);if(!r)return n;let s=t.d...
class Xs (line 1) | class Xs{constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.sc...
method constructor (line 1) | constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.scaleX=1,...
method update (line 1) | update(t){t.startState.facet(As)!=t.state.facet(As)&&this.setOrder(t.s...
method docViewUpdate (line 1) | docViewUpdate(t){!1!==this.layer.updateOnDocViewUpdate&&t.requestMeasu...
method setOrder (line 1) | setOrder(t){let e=0,i=t.facet(As);for(;e<i.length&&i[e]!=this.layer;)e...
method measure (line 1) | measure(){return this.layer.markers(this.view)}
method scale (line 1) | scale(){let{scaleX:t,scaleY:e}=this.view;t==this.scaleX&&e==this.scale...
method draw (line 1) | draw(t){if(t.length!=this.drawn.length||t.some((t,e)=>{return i=t,n=th...
method destroy (line 1) | destroy(){this.layer.destroy&&this.layer.destroy(this.dom,this.view),t...
function Ms (line 1) | function Ms(t){return[Wi.define(e=>new Xs(e,t)),As.of(t)]}
function zs (line 1) | function zs(t){return t.startState.facet(Rs)!=t.state.facet(Rs)}
method markers (line 1) | markers(t){let{state:e}=t,i=e.facet(Rs),n=[];for(let r of e.selection.ra...
method update (line 1) | update(t,e){t.transactions.some(t=>t.selection)&&(e.style.animationName=...
method mount (line 1) | mount(t,e){Es(e.state,t)}
function Es (line 1) | function Es(t,e){e.style.animationDuration=t.facet(Rs).cursorBlinkRate+"...
method constructor (line 1) | constructor(t){this.view=t,this.cursor=null,this.measureReq={read:this.r...
method update (line 1) | update(t){var e;let i=t.state.field(Vs);null==i?null!=this.cursor&&(null...
method readPos (line 1) | readPos(){let{view:t}=this,e=t.state.field(Vs),i=null!=e&&t.coordsAtPos(...
method drawCursor (line 1) | drawCursor(t){if(this.cursor){let{scaleX:e,scaleY:i}=this.view;t?(this.c...
method destroy (line 1) | destroy(){this.cursor&&this.cursor.remove()}
method setDropPos (line 1) | setDropPos(t){this.view.state.field(Vs)!=t&&this.view.dispatch({effects:...
method dragover (line 1) | dragover(t){this.setDropPos(this.view.posAtCoords({x:t.clientX,y:t.clien...
method dragleave (line 1) | dragleave(t){t.target!=this.view.contentDOM&&this.view.contentDOM.contai...
method dragend (line 1) | dragend(){this.setDropPos(null)}
method drop (line 1) | drop(){this.setDropPos(null)}
function Ds (line 1) | function Ds(t,e,i,n,r){e.lastIndex=0;for(let s,o=t.iterRange(i,n),a=i;!o...
class Bs (line 1) | class Bs{constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:...
method constructor (line 1) | constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:r,maxLe...
method createDeco (line 1) | createDeco(t){let e=new Et,i=e.add.bind(e);for(let{from:n,to:r}of func...
method updateDeco (line 1) | updateDeco(t,e){let i=1e9,n=-1;return t.docChanged&&t.changes.iterChan...
method updateRange (line 1) | updateRange(t,e,i,n){for(let r of t.visibleRanges){let s=Math.max(r.fr...
method combine (line 1) | combine(t){let e=Zt(t,{render:null,specialChars:Is,addSpecialChars:null}...
class Fs (line 1) | class Fs extends Pe{constructor(t,e){super(),this.options=t,this.code=e}...
method constructor (line 1) | constructor(t,e){super(),this.options=t,this.code=e}
method eq (line 1) | eq(t){return t.code==this.code}
method toDOM (line 1) | toDOM(t){let e=function(t){return t>=32?"•":10==t?"":String.fromCharC...
method ignoreEvent (line 1) | ignoreEvent(){return!1}
class Ks (line 1) | class Ks extends Pe{constructor(t){super(),this.width=t}eq(t){return t.w...
method constructor (line 1) | constructor(t){super(),this.width=t}
method eq (line 1) | eq(t){return t.width==this.width}
method toDOM (line 1) | toDOM(){let t=document.createElement("span");return t.textContent="\t"...
method ignoreEvent (line 1) | ignoreEvent(){return!1}
method constructor (line 1) | constructor(t){this.decorations=this.getDeco(t)}
method update (line 1) | update(t){(t.docChanged||t.selectionSet)&&(this.decorations=this.getDeco...
method getDeco (line 1) | getDeco(t){let e=-1,i=[];for(let n of t.state.selection.ranges){let r=t....
class eo (line 1) | class eo extends Pe{constructor(t){super(),this.content=t}toDOM(t){let e...
method constructor (line 1) | constructor(t){super(),this.content=t}
method toDOM (line 1) | toDOM(t){let e=document.createElement("span");return e.className="cm-p...
method coordsAt (line 1) | coordsAt(t){let e=t.firstChild?Le(t.firstChild):[];if(!e.length)return...
method ignoreEvent (line 1) | ignoreEvent(){return!1}
function io (line 1) | function io(t){let e=Wi.fromClass(class{constructor(e){this.view=e,this....
function ro (line 1) | function ro(t,e){let i=t.posAtCoords({x:e.clientX,y:e.clientY},!1),n=t.s...
function so (line 1) | function so(t,e){let i=ro(t,e),n=t.state.selection;return i?{update(t){i...
class ho (line 1) | class ho{constructor(t,e,i,n){this.facet=e,this.createTooltipView=i,this...
method constructor (line 1) | constructor(t,e,i,n){this.facet=e,this.createTooltipView=i,this.remove...
method update (line 1) | update(t,e){var i;let n=t.state.facet(this.facet),r=n.filter(t=>t);if(...
function co (line 1) | function co(t){let e=t.dom.ownerDocument.documentElement;return{top:0,le...
method constructor (line 1) | constructor(t){this.view=t,this.above=[],this.inView=!0,this.madeAbsolut...
method createContainer (line 1) | createContainer(){this.parent?(this.container=document.createElement("di...
method observeIntersection (line 1) | observeIntersection(){if(this.intersectionObserver){this.intersectionObs...
method measureSoon (line 1) | measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=...
method update (line 1) | update(t){t.transactions.length&&(this.lastTransaction=Date.now());let e...
method createTooltip (line 1) | createTooltip(t,e){let i=t.create(this.view),n=e?e.dom:null;if(i.dom.cla...
method destroy (line 1) | destroy(){var t,e,i;this.view.win.removeEventListener("resize",this.meas...
method readMeasure (line 1) | readMeasure(){let t=1,e=1,i=!1;if("fixed"==this.position&&this.manager.t...
method writeMeasure (line 1) | writeMeasure(t){var e;if(t.makeAbsolute){this.madeAbsolute=!0,this.posit...
method maybeMeasure (line 1) | maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this....
method scroll (line 1) | scroll(){this.maybeMeasure()}
function po (line 1) | function po(t,e){let i=parseInt(t.style.left,10);(isNaN(i)||Math.abs(e-i...
class vo (line 1) | class vo{static create(t){return new vo(t)}constructor(t){this.view=t,th...
method create (line 1) | static create(t){return new vo(t)}
method constructor (line 1) | constructor(t){this.view=t,this.mounted=!1,this.dom=document.createEle...
method createHostedView (line 1) | createHostedView(t,e){let i=t.create(this.view);return i.dom.classList...
method mount (line 1) | mount(t){for(let e of this.manager.tooltipViews)e.mount&&e.mount(t);th...
method positioned (line 1) | positioned(t){for(let e of this.manager.tooltipViews)e.positioned&&e.p...
method update (line 1) | update(t){this.manager.update(t)}
method destroy (line 1) | destroy(){var t;for(let e of this.manager.tooltipViews)null===(t=e.des...
method passProp (line 1) | passProp(t){let e;for(let i of this.manager.tooltipViews){let n=i[t];i...
method offset (line 1) | get offset(){return this.passProp("offset")}
method getCoords (line 1) | get getCoords(){return this.passProp("getCoords")}
method overlap (line 1) | get overlap(){return this.passProp("overlap")}
method resize (line 1) | get resize(){return this.passProp("resize")}
class xo (line 1) | class xo{constructor(t,e,i,n,r){this.view=t,this.source=e,this.field=i,t...
method constructor (line 1) | constructor(t,e,i,n,r){this.view=t,this.source=e,this.field=i,this.set...
method update (line 1) | update(){this.pending&&(this.pending=null,clearTimeout(this.restartTim...
method active (line 1) | get active(){return this.view.state.field(this.field)}
method checkHover (line 1) | checkHover(){if(this.hoverTimeout=-1,this.active.length)return;let t=D...
method startHover (line 1) | startHover(){clearTimeout(this.restartTimeout);let{view:t,lastMove:e}=...
method tooltip (line 1) | get tooltip(){let t=this.view.plugin(Oo),e=t?t.manager.tooltips.findIn...
method mousemove (line 1) | mousemove(t){var e,i;this.lastMove={x:t.clientX,y:t.clientY,target:t.t...
method mouseleave (line 1) | mouseleave(t){clearTimeout(this.hoverTimeout),this.hoverTimeout=-1;let...
method watchTooltipLeave (line 1) | watchTooltipLeave(t){let e=i=>{t.removeEventListener("mouseleave",e),t...
method destroy (line 1) | destroy(){clearTimeout(this.hoverTimeout),clearTimeout(this.restartTim...
function yo (line 1) | function yo(t,e={}){let i=gt.define(),n=N.define({create:()=>[],update(t...
function ko (line 1) | function ko(t,e){let i=t.plugin(Oo);if(!i)return null;let n=i.manager.to...
method combine (line 1) | combine(t){let e,i;for(let n of t)e=e||n.topContainer,i=i||n.bottomConta...
function To (line 1) | function To(t,e){let i=t.plugin(Co),n=i?i.specs.indexOf(e):-1;return n>-...
method constructor (line 1) | constructor(t){this.input=t.state.facet(Ao),this.specs=this.input.filter...
method update (line 1) | update(t){let e=t.state.facet(Po);this.top.container!=e.topContainer&&(t...
method destroy (line 1) | destroy(){this.top.sync([]),this.bottom.sync([])}
class Zo (line 1) | class Zo{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this...
method constructor (line 1) | constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=vo...
method sync (line 1) | sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy(...
method syncDOM (line 1) | syncDOM(){if(0==this.panels.length)return void(this.dom&&(this.dom.rem...
method scrollMargin (line 1) | scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?t...
method syncClasses (line 1) | syncClasses(){if(this.container&&this.classes!=this.view.themeClasses)...
function Xo (line 1) | function Xo(t){let e=t.nextSibling;return t.remove(),e}
class Mo (line 1) | class Mo extends Xt{compare(t){return this==t||this.constructor==t.const...
method compare (line 1) | compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}
method eq (line 1) | eq(t){return!1}
method destroy (line 1) | destroy(t){}
function Yo (line 1) | function Yo(t){return[qo(),Eo.of({..._o,...t})]}
function qo (line 1) | function qo(t){return[Vo]}
method constructor (line 1) | constructor(t){this.view=t,this.domAfter=null,this.prevViewport=t.viewpo...
method getDOMAfter (line 1) | getDOMAfter(){return this.domAfter||(this.domAfter=document.createElemen...
method update (line 1) | update(t){if(this.updateGutters(t)){let e=this.prevViewport,i=t.view.vie...
method syncGutters (line 1) | syncGutters(t){let e=this.dom.nextSibling;t&&(this.dom.remove(),this.dom...
method updateGutters (line 1) | updateGutters(t){let e=t.startState.facet(Eo),i=t.state.facet(Eo),n=t.do...
method destroy (line 1) | destroy(){for(let t of this.gutters)t.destroy();this.dom.remove(),this.d...
function Wo (line 1) | function Wo(t){return Array.isArray(t)?t:[t]}
function Do (line 1) | function Do(t,e,i){for(;t.value&&t.from<=i;)t.from==i&&e.push(t.value),t...
class Bo (line 1) | class Bo{constructor(t,e,i){this.gutter=t,this.height=i,this.i=0,this.cu...
method constructor (line 1) | constructor(t,e,i){this.gutter=t,this.height=i,this.i=0,this.cursor=_t...
method addElement (line 1) | addElement(t,e,i){let{gutter:n}=this,r=(e.top-this.height)/t.scaleY,s=...
method line (line 1) | line(t,e,i){let n=[];Do(this.cursor,n,e.from),i.length&&(n=n.concat(i)...
method widget (line 1) | widget(t,e){let i=this.gutter.config.widgetMarker(t,e.widget,e),n=i?[i...
method finish (line 1) | finish(){let t=this.gutter;for(;t.elements.length>this.i;){let e=t.ele...
class jo (line 1) | class jo{constructor(t,e){this.view=t,this.config=e,this.elements=[],thi...
method constructor (line 1) | constructor(t,e){this.view=t,this.config=e,this.elements=[],this.space...
method update (line 1) | update(t){let e=this.markers;if(this.markers=Wo(this.config.markers(t....
method destroy (line 1) | destroy(){for(let t of this.elements)t.destroy()}
class Io (line 1) | class Io{constructor(t,e,i,n){this.height=-1,this.above=0,this.markers=[...
method constructor (line 1) | constructor(t,e,i,n){this.height=-1,this.above=0,this.markers=[],this....
method update (line 1) | update(t,e,i,n){this.height!=e&&(this.height=e,this.dom.style.height=e...
method setMarkers (line 1) | setMarkers(t,e){let i="cm-gutterElement",n=this.dom.firstChild;for(let...
method destroy (line 1) | destroy(){this.setMarkers(null,[])}
method domEventHandlers (line 1) | domEventHandlers(t,e){let i=Object.assign({},t);for(let n in e){let t=i[...
class Ho (line 1) | class Ho extends Mo{constructor(t){super(),this.number=t}eq(t){return th...
method constructor (line 1) | constructor(t){super(),this.number=t}
method eq (line 1) | eq(t){return this.number==t.number}
method toDOM (line 1) | toDOM(){return document.createTextNode(this.number)}
function Fo (line 1) | function Fo(t,e){return t.state.facet(Uo).formatNumber(e,t.state)}
method updateSpacer (line 1) | updateSpacer(t,e){let i=Fo(e.view,Jo(e.view.state.doc.lines));return i==...
function Jo (line 1) | function Jo(t){let e=9;for(;e<t;)e=10*e+9;return e}
method constructor (line 1) | constructor(){super(...arguments),this.elementClass="cm-activeLineGutter"}
class ra (line 1) | class ra{constructor(t,e){this.from=t,this.to=e}}
method constructor (line 1) | constructor(t,e){this.from=t,this.to=e}
class sa (line 1) | class sa{constructor(t={}){this.id=na++,this.perNode=!!t.perNode,this.de...
method constructor (line 1) | constructor(t={}){this.id=na++,this.perNode=!!t.perNode,this.deseriali...
method add (line 1) | add(t){if(this.perNode)throw new RangeError("Can't add per-node props ...
class oa (line 1) | class oa{constructor(t,e,i,n=!1){this.tree=t,this.overlay=e,this.parser=...
method constructor (line 1) | constructor(t,e,i,n=!1){this.tree=t,this.overlay=e,this.parser=i,this....
method get (line 1) | static get(t){return t&&t.props&&t.props[sa.mounted.id]}
class la (line 1) | class la{constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this....
method constructor (line 1) | constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this.flags=n}
method define (line 1) | static define(t){let e=t.props&&t.props.length?Object.create(null):aa,...
method prop (line 1) | prop(t){return this.props[t.id]}
method isTop (line 1) | get isTop(){return(1&this.flags)>0}
method isSkipped (line 1) | get isSkipped(){return(2&this.flags)>0}
method isError (line 1) | get isError(){return(4&this.flags)>0}
method isAnonymous (line 1) | get isAnonymous(){return(8&this.flags)>0}
method is (line 1) | is(t){if("string"==typeof t){if(this.name==t)return!0;let e=this.prop(...
method match (line 1) | static match(t){let e=Object.create(null);for(let i in t)for(let n of ...
class ha (line 1) | class ha{constructor(t){this.types=t;for(let e=0;e<t.length;e++)if(t[e]....
method constructor (line 1) | constructor(t){this.types=t;for(let e=0;e<t.length;e++)if(t[e].id!=e)t...
method extend (line 1) | extend(...t){let e=[];for(let i of this.types){let n=null;for(let e of...
class Oa (line 1) | class Oa{constructor(t,e,i,n,r){if(this.type=t,this.children=e,this.posi...
method constructor (line 1) | constructor(t,e,i,n,r){if(this.type=t,this.children=e,this.positions=i...
method toString (line 1) | toString(){let t=oa.get(this);if(t&&!t.overlay)return t.tree.toString(...
method cursor (line 1) | cursor(t=0){return new Pa(this.topNode,t)}
method cursorAt (line 1) | cursorAt(t,e=0,i=0){let n=ca.get(this)||this.topNode,r=new Pa(n);retur...
method topNode (line 1) | get topNode(){return new va(this,0,0,null)}
method resolve (line 1) | resolve(t,e=0){let i=Qa(ca.get(this)||this.topNode,t,e,!1);return ca.s...
method resolveInner (line 1) | resolveInner(t,e=0){let i=Qa(ua.get(this)||this.topNode,t,e,!0);return...
method resolveStack (line 1) | resolveStack(t,e=0){return function(t,e,i){let n=t.resolveInner(e,i),r...
method iterate (line 1) | iterate(t){let{enter:e,leave:i,from:n=0,to:r=this.length}=t,s=t.mode||...
method prop (line 1) | prop(t){return t.perNode?this.props?this.props[t.id]:void 0:this.type....
method propValues (line 1) | get propValues(){let t=[];if(this.props)for(let e in this.props)t.push...
method balance (line 1) | balance(t={}){return this.children.length<=8?this:Xa(la.none,this.chil...
method build (line 1) | static build(t){return function(t){var e;let{buffer:i,nodeSet:n,maxBuf...
class pa (line 1) | class pa{constructor(t,e){this.buffer=t,this.index=e}get id(){return thi...
method constructor (line 1) | constructor(t,e){this.buffer=t,this.index=e}
method id (line 1) | get id(){return this.buffer[this.index-4]}
method start (line 1) | get start(){return this.buffer[this.index-3]}
method end (line 1) | get end(){return this.buffer[this.index-2]}
method size (line 1) | get size(){return this.buffer[this.index-1]}
method pos (line 1) | get pos(){return this.index}
method next (line 1) | next(){this.index-=4}
method fork (line 1) | fork(){return new pa(this.buffer,this.index)}
class ma (line 1) | class ma{constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}get t...
method constructor (line 1) | constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}
method type (line 1) | get type(){return la.none}
method toString (line 1) | toString(){let t=[];for(let e=0;e<this.buffer.length;)t.push(this.chil...
method childString (line 1) | childString(t){let e=this.buffer[t],i=this.buffer[t+3],n=this.set.type...
method findChild (line 1) | findChild(t,e,i,n,r){let{buffer:s}=this,o=-1;for(let a=t;a!=e&&!(ga(r,...
method slice (line 1) | slice(t,e,i){let n=this.buffer,r=new Uint16Array(e-t),s=0;for(let o=t,...
function ga (line 1) | function ga(t,e,i,n){switch(t){case-2:return i<e;case-1:return n>=e&&i<e...
function Qa (line 1) | function Qa(t,e,i,n){for(var r;t.from==t.to||(i<1?t.from>=e:t.from>e)||(...
class ba (line 1) | class ba{cursor(t=0){return new Pa(this,t)}getChild(t,e=null,i=null){let...
method cursor (line 1) | cursor(t=0){return new Pa(this,t)}
method getChild (line 1) | getChild(t,e=null,i=null){let n=wa(this,t,e,i);return n.length?n[0]:null}
method getChildren (line 1) | getChildren(t,e=null,i=null){return wa(this,t,e,i)}
method resolve (line 1) | resolve(t,e=0){return Qa(this,t,e,!1)}
method resolveInner (line 1) | resolveInner(t,e=0){return Qa(this,t,e,!0)}
method matchContext (line 1) | matchContext(t){return xa(this.parent,t)}
method enterUnfinishedNodesBefore (line 1) | enterUnfinishedNodesBefore(t){let e=this.childBefore(t),i=this;for(;e;...
method node (line 1) | get node(){return this}
method next (line 1) | get next(){return this.parent}
class va (line 1) | class va extends ba{constructor(t,e,i,n){super(),this._tree=t,this.from=...
method constructor (line 1) | constructor(t,e,i,n){super(),this._tree=t,this.from=e,this.index=i,thi...
method type (line 1) | get type(){return this._tree.type}
method name (line 1) | get name(){return this._tree.type.name}
method to (line 1) | get to(){return this.from+this._tree.length}
method nextChild (line 1) | nextChild(t,e,i,n,r=0){var s;for(let o=this;;){for(let{children:a,posi...
method firstChild (line 1) | get firstChild(){return this.nextChild(0,1,0,4)}
method lastChild (line 1) | get lastChild(){return this.nextChild(this._tree.children.length-1,-1,...
method childAfter (line 1) | childAfter(t){return this.nextChild(0,1,t,2)}
method childBefore (line 1) | childBefore(t){return this.nextChild(this._tree.children.length-1,-1,t...
method prop (line 1) | prop(t){return this._tree.prop(t)}
method enter (line 1) | enter(t,e,i=0){let n;if(!(i&da.IgnoreOverlays)&&(n=oa.get(this._tree))...
method nextSignificantParent (line 1) | nextSignificantParent(){let t=this;for(;t.type.isAnonymous&&t._parent;...
method parent (line 1) | get parent(){return this._parent?this._parent.nextSignificantParent():...
method nextSibling (line 1) | get nextSibling(){return this._parent&&this.index>=0?this._parent.next...
method prevSibling (line 1) | get prevSibling(){return this._parent&&this.index>=0?this._parent.next...
method tree (line 1) | get tree(){return this._tree}
method toTree (line 1) | toTree(){return this._tree}
method toString (line 1) | toString(){return this._tree.toString()}
function wa (line 1) | function wa(t,e,i,n){let r=t.cursor(),s=[];if(!r.firstChild())return s;i...
function xa (line 1) | function xa(t,e,i=e.length-1){for(let n=t;i>=0;n=n.parent){if(!n)return!...
class Sa (line 1) | class Sa{constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,t...
method constructor (line 1) | constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,this.sta...
class ya (line 1) | class ya extends ba{get name(){return this.type.name}get from(){return t...
method name (line 1) | get name(){return this.type.name}
method from (line 1) | get from(){return this.context.start+this.context.buffer.buffer[this.i...
method to (line 1) | get to(){return this.context.start+this.context.buffer.buffer[this.ind...
method constructor (line 1) | constructor(t,e,i){super(),this.context=t,this._parent=e,this.index=i,...
method child (line 1) | child(t,e,i){let{buffer:n}=this.context,r=n.findChild(this.index+4,n.b...
method firstChild (line 1) | get firstChild(){return this.child(1,0,4)}
method lastChild (line 1) | get lastChild(){return this.child(-1,0,4)}
method childAfter (line 1) | childAfter(t){return this.child(1,t,2)}
method childBefore (line 1) | childBefore(t){return this.child(-1,t,-2)}
method prop (line 1) | prop(t){return this.type.prop(t)}
method enter (line 1) | enter(t,e,i=0){if(i&da.ExcludeBuffers)return null;let{buffer:n}=this.c...
method parent (line 1) | get parent(){return this._parent||this.context.parent.nextSignificantP...
method externalSibling (line 1) | externalSibling(t){return this._parent?null:this.context.parent.nextCh...
method nextSibling (line 1) | get nextSibling(){let{buffer:t}=this.context,e=t.buffer[this.index+3];...
method prevSibling (line 1) | get prevSibling(){let{buffer:t}=this.context,e=this._parent?this._pare...
method tree (line 1) | get tree(){return null}
method toTree (line 1) | toTree(){let t=[],e=[],{buffer:i}=this.context,n=this.index+4,r=i.buff...
method toString (line 1) | toString(){return this.context.buffer.childString(this.index)}
function ka (line 1) | function ka(t){if(!t.length)return null;let e=0,i=t[0];for(let s=1;s<t.l...
class $a (line 1) | class $a{constructor(t,e){this.heads=t,this.node=e}get next(){return ka(...
method constructor (line 1) | constructor(t,e){this.heads=t,this.node=e}
method next (line 1) | get next(){return ka(this.heads)}
class Pa (line 1) | class Pa{get name(){return this.type.name}constructor(t,e=0){if(this.buf...
method name (line 1) | get name(){return this.type.name}
method constructor (line 1) | constructor(t,e=0){if(this.buffer=null,this.stack=[],this.index=0,this...
method yieldNode (line 1) | yieldNode(t){return!!t&&(this._tree=t,this.type=t.type,this.from=t.fro...
method yieldBuf (line 1) | yieldBuf(t,e){this.index=t;let{start:i,buffer:n}=this.buffer;return th...
method yield (line 1) | yield(t){return!!t&&(t instanceof va?(this.buffer=null,this.yieldNode(...
method toString (line 1) | toString(){return this.buffer?this.buffer.buffer.childString(this.inde...
method enterChild (line 1) | enterChild(t,e,i){if(!this.buffer)return this.yield(this._tree.nextChi...
method firstChild (line 1) | firstChild(){return this.enterChild(1,0,4)}
method lastChild (line 1) | lastChild(){return this.enterChild(-1,0,4)}
method childAfter (line 1) | childAfter(t){return this.enterChild(1,t,2)}
method childBefore (line 1) | childBefore(t){return this.enterChild(-1,t,-2)}
method enter (line 1) | enter(t,e,i=this.mode){return this.buffer?!(i&da.ExcludeBuffers)&&this...
method parent (line 1) | parent(){if(!this.buffer)return this.yieldNode(this.mode&da.IncludeAno...
method sibling (line 1) | sibling(t){if(!this.buffer)return!!this._tree._parent&&this.yield(this...
method nextSibling (line 1) | nextSibling(){return this.sibling(1)}
method prevSibling (line 1) | prevSibling(){return this.sibling(-1)}
method atLastNode (line 1) | atLastNode(t){let e,i,{buffer:n}=this;if(n){if(t>0){if(this.index<n.bu...
method move (line 1) | move(t,e){if(e&&this.enterChild(t,0,4))return!0;for(;;){if(this.siblin...
method next (line 1) | next(t=!0){return this.move(1,t)}
method prev (line 1) | prev(t=!0){return this.move(-1,t)}
method moveTo (line 1) | moveTo(t,e=0){for(;(this.from==this.to||(e<1?this.from>=t:this.from>t)...
method node (line 1) | get node(){if(!this.buffer)return this._tree;let t=this.bufferNode,e=n...
method tree (line 1) | get tree(){return this.buffer?null:this._tree._tree}
method iterate (line 1) | iterate(t,e){for(let i=0;;){let n=!1;if(this.type.isAnonymous||!1!==t(...
method matchContext (line 1) | matchContext(t){if(!this.buffer)return xa(this.node.parent,t);let{buff...
function Ta (line 1) | function Ta(t){return t.children.some(t=>t instanceof ma||!t.type.isAnon...
function Za (line 1) | function Za(t,e){if(!t.isAnonymous||e instanceof ma||e.type!=t)return 1;...
function Xa (line 1) | function Xa(t,e,i,n,r,s,o,a,l){let h=0;for(let f=n;f<r;f++)h+=Za(t,e[f])...
class Aa (line 1) | class Aa{constructor(){this.map=new WeakMap}setBuffer(t,e,i){let n=this....
method constructor (line 1) | constructor(){this.map=new WeakMap}
method setBuffer (line 1) | setBuffer(t,e,i){let n=this.map.get(t);n||this.map.set(t,n=new Map),n....
method getBuffer (line 1) | getBuffer(t,e){let i=this.map.get(t);return i&&i.get(e)}
method set (line 1) | set(t,e){t instanceof ya?this.setBuffer(t.context.buffer,t.index,e):t ...
method get (line 1) | get(t){return t instanceof ya?this.getBuffer(t.context.buffer,t.index)...
method cursorSet (line 1) | cursorSet(t,e){t.buffer?this.setBuffer(t.buffer.buffer,t.index,e):this...
method cursorGet (line 1) | cursorGet(t){return t.buffer?this.getBuffer(t.buffer.buffer,t.index):t...
class Ma (line 1) | class Ma{constructor(t,e,i,n,r=!1,s=!1){this.from=t,this.to=e,this.tree=...
method constructor (line 1) | constructor(t,e,i,n,r=!1,s=!1){this.from=t,this.to=e,this.tree=i,this....
method openStart (line 1) | get openStart(){return(1&this.open)>0}
method openEnd (line 1) | get openEnd(){return(2&this.open)>0}
method addTree (line 1) | static addTree(t,e=[],i=!1){let n=[new Ma(0,t.length,t,0,!1,i)];for(le...
method applyChanges (line 1) | static applyChanges(t,e,i=128){if(!e.length)return t;let n=[],r=1,s=t....
class Ra (line 1) | class Ra{startParse(t,e,i){return"string"==typeof t&&(t=new za(t)),i=i?i...
method startParse (line 1) | startParse(t,e,i){return"string"==typeof t&&(t=new za(t)),i=i?i.length...
method parse (line 1) | parse(t,e,i){let n=this.startParse(t,e,i);for(;;){let t=n.advance();if...
class za (line 1) | class za{constructor(t){this.string=t}get length(){return this.string.le...
method constructor (line 1) | constructor(t){this.string=t}
method length (line 1) | get length(){return this.string.length}
method chunk (line 1) | chunk(t){return this.string.slice(t)}
method lineChunks (line 1) | get lineChunks(){return!1}
method read (line 1) | read(t,e){return this.string.slice(t,e)}
function _a (line 1) | function _a(t){return(e,i,n,r)=>new Va(e,t,i,n,r)}
class Ea (line 1) | class Ea{constructor(t,e,i,n,r,s){this.parser=t,this.parse=e,this.overla...
method constructor (line 1) | constructor(t,e,i,n,r,s){this.parser=t,this.parse=e,this.overlay=i,thi...
function Ya (line 1) | function Ya(t){if(!t.length||t.some(t=>t.from>=t.to))throw new RangeErro...
class La (line 1) | class La{constructor(t,e,i,n,r,s,o,a){this.parser=t,this.predicate=e,thi...
method constructor (line 1) | constructor(t,e,i,n,r,s,o,a){this.parser=t,this.predicate=e,this.mount...
class Va (line 1) | class Va{constructor(t,e,i,n,r){this.nest=e,this.input=i,this.fragments=...
method constructor (line 1) | constructor(t,e,i,n,r){this.nest=e,this.input=i,this.fragments=n,this....
method advance (line 1) | advance(){if(this.baseParse){let t=this.baseParse.advance();if(!t)retu...
method parsedPos (line 1) | get parsedPos(){if(this.baseParse)return 0;let t=this.input.length;for...
method stopAt (line 1) | stopAt(t){if(this.stoppedAt=t,this.baseParse)this.baseParse.stopAt(t);...
method startInner (line 1) | startInner(){let t=new Ia(this.fragments),e=null,i=null,n=new Pa(new v...
function Wa (line 1) | function Wa(t,e,i){for(let n of t){if(n.from>=i)break;if(n.to>e)return n...
function Da (line 1) | function Da(t,e,i,n,r,s){if(e<i){let o=t.buffer[e+1];n.push(t.slice(e,i,...
function Ba (line 1) | function Ba(t){let{node:e}=t,i=[],n=e.context.buffer;do{i.push(t.index),...
class ja (line 1) | class ja{constructor(t,e){this.offset=e,this.done=!1,this.cursor=t.curso...
method constructor (line 1) | constructor(t,e){this.offset=e,this.done=!1,this.cursor=t.cursor(da.In...
method moveTo (line 1) | moveTo(t){let{cursor:e}=this,i=t-this.offset;for(;!this.done&&e.from<i...
method hasNode (line 1) | hasNode(t){if(this.moveTo(t.from),!this.done&&this.cursor.from+this.of...
method constructor (line 1) | constructor(t){var e;if(this.fragments=t,this.curTo=0,this.fragI=0,t.len...
method hasNode (line 1) | hasNode(t){for(;this.curFrag&&t.from>=this.curTo;)this.nextFrag();return...
method nextFrag (line 1) | nextFrag(){var t;if(this.fragI++,this.fragI==this.fragments.length)this....
method findMounts (line 1) | findMounts(t,e){var i;let n=[];if(this.inner){this.inner.cursor.moveTo(t...
function Ga (line 1) | function Ga(t,e){let i=null,n=e;for(let r=1,s=0;r<t.length;r++){let o=t[...
function Na (line 1) | function Na(t,e,i,n){let r=0,s=0,o=!1,a=!1,l=-1e9,h=[];for(;;){let c=r==...
function Ua (line 1) | function Ua(t,e){let i=[];for(let{pos:n,mount:r,frag:s}of t){let t=n+(r....
class Fa (line 1) | class Fa{constructor(t,e,i,n){this.name=t,this.set=e,this.base=i,this.mo...
method constructor (line 1) | constructor(t,e,i,n){this.name=t,this.set=e,this.base=i,this.modified=...
method toString (line 1) | toString(){let{name:t}=this;for(let e of this.modified)e.name&&(t=`${e...
method define (line 1) | static define(t,e){let i="string"==typeof t?t:"?";if(t instanceof Fa&&...
method defineModifier (line 1) | static defineModifier(t){let e=new Ja(t);return t=>t.modified.indexOf(...
class Ja (line 1) | class Ja{constructor(t){this.name=t,this.instances=[],this.id=Ka++}stati...
method constructor (line 1) | constructor(t){this.name=t,this.instances=[],this.id=Ka++}
method get (line 1) | static get(t,e){if(!e.length)return t;let i=e[0].instances.find(i=>{re...
function tl (line 1) | function tl(t){let e=Object.create(null);for(let i in t){let n=t[i];Arra...
method combine (line 1) | combine(t,e){let i,n,r;for(;t||e;){if(!t||e&&t.depth>=e.depth?(r=e,e=e.n...
class il (line 1) | class il{constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,thi...
method constructor (line 1) | constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,this.next=n}
method opaque (line 1) | get opaque(){return 0==this.mode}
method inherit (line 1) | get inherit(){return 1==this.mode}
method sort (line 1) | sort(t){return!t||t.depth<this.depth?(this.next=t,this):(t.next=this.s...
method depth (line 1) | get depth(){return this.context?this.context.length:0}
function nl (line 1) | function nl(t,e){let i=Object.create(null);for(let s of t)if(Array.isArr...
function rl (line 1) | function rl(t,e,i,n=0,r=t.length){let s=new sl(n,Array.isArray(e)?e:[e],...
class sl (line 1) | class sl{constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,th...
method constructor (line 1) | constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,this.clas...
method startSpan (line 1) | startSpan(t,e){e!=this.class&&(this.flush(t),t>this.at&&(this.at=t),th...
method flush (line 1) | flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}
method highlightRange (line 1) | highlightRange(t,e,i,n,r){let{type:s,from:o,to:a}=t;if(o>=i||a<=e)retu...
function yl (line 1) | function yl(t){return V.define({combine:t?e=>e.concat(t):void 0})}
class $l (line 1) | class $l{constructor(t,e,i=[],n=""){this.data=t,this.name=n,Ct.prototype...
method constructor (line 1) | constructor(t,e,i=[],n=""){this.data=t,this.name=n,Ct.prototype.hasOwn...
method isActiveAt (line 1) | isActiveAt(t,e,i=-1){return Pl(t,e,i).type.prop(Sl)==this.data}
method findRegions (line 1) | findRegions(t){let e=t.facet(Yl);if((null==e?void 0:e.data)==this.data...
method allowsNesting (line 1) | get allowsNesting(){return!0}
function Pl (line 1) | function Pl(t,e,i){let n=t.facet(Yl),r=Cl(t).topNode;if(!n||n.allowsNest...
class Tl (line 1) | class Tl extends $l{constructor(t,e,i){super(t,e,[],i),this.parser=e}sta...
method constructor (line 1) | constructor(t,e,i){super(t,e,[],i),this.parser=e}
method define (line 1) | static define(t){let e=yl(t.languageData);return new Tl(e,t.parser.con...
method configure (line 1) | configure(t,e){return new Tl(this.data,this.parser.configure(t),e||thi...
method allowsNesting (line 1) | get allowsNesting(){return this.parser.hasWrappers()}
function Cl (line 1) | function Cl(t){let e=t.field($l.state,!1);return e?e.tree:Oa.empty}
class Zl (line 1) | class Zl{constructor(t){this.doc=t,this.cursorPos=0,this.string="",this....
method constructor (line 1) | constructor(t){this.doc=t,this.cursorPos=0,this.string="",this.cursor=...
method length (line 1) | get length(){return this.doc.length}
method syncTo (line 1) | syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,...
method chunk (line 1) | chunk(t){return this.syncTo(t),this.string}
method lineChunks (line 1) | get lineChunks(){return!0}
method read (line 1) | read(t,e){let i=this.cursorPos-this.string.length;return t<i||e>=this....
class Al (line 1) | class Al{constructor(t,e,i=[],n,r,s,o,a){this.parser=t,this.state=e,this...
method constructor (line 1) | co
Condensed preview — 547 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (7,143K chars).
[
{
"path": ".github/workflows/alpha-release.yml",
"chars": 16509,
"preview": "name: Go-Alpha-Release\n\n# 触发条件配置\non:\n push:\n branches-ignore:\n - \"master\"\n paths:\n - \"internal/app/vers"
},
{
"path": ".github/workflows/mirror-to-cnb.yml",
"chars": 998,
"preview": "# Mirror repository to CNB.cool\n# 将当前仓库的所有分支和标签同步到 cnb.cool 远程仓库\nname: Mirror to CNB\n\non:\n push:\n branches:\n - "
},
{
"path": ".github/workflows/release.yml",
"chars": 16581,
"preview": "name: Go-Release\n\n# 触发条件配置\non:\n push:\n # 针对 master 分支的普通提交也触发\n branches:\n - \"master\"\n paths:\n - \"int"
},
{
"path": ".gitignore",
"chars": 1081,
"preview": "# -- Composer -----------------------------------------\n/composer.phar\n/composer.lock\n\n# -- Editores -------------------"
},
{
"path": "LICENSE",
"chars": 11344,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 5369,
"preview": "# Docker 登录示例\n# docker login --username=xxxxxx registry.cn-shanghai.aliyuncs.com\n\n# 环境变量(可选)\n# include .env\n# export $(s"
},
{
"path": "README.md",
"chars": 13930,
"preview": "[简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github."
},
{
"path": "cmd/bootstrap.go",
"chars": 1124,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// bootstrapLogger bootstrap stage logger\n"
},
{
"path": "cmd/gorm_gen/gen.go",
"chars": 6105,
"preview": "package main\n\n// gorm gen configure\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/haierkeys/fast-note-sync-ser"
},
{
"path": "cmd/mfmt/main.go",
"chars": 4228,
"preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/format\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"io/io"
},
{
"path": "cmd/model_gen/gen.go",
"chars": 1525,
"preview": "package main\n\n// gorm gen configure\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/"
},
{
"path": "cmd/reset_password.go",
"chars": 3644,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\tinternalApp \"github.com/haierkeys/fast-note-sync-service/internal/app\"\n\t"
},
{
"path": "cmd/root.go",
"chars": 589,
"preview": "package cmd\n\nimport (\n\t\"embed\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/zap\"\n)\n\nvar frontendFiles embed.FS\nvar con"
},
{
"path": "cmd/run.go",
"chars": 6976,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\tinternalApp \"github.com/haierk"
},
{
"path": "cmd/run_server.go",
"chars": 12843,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\tinternalApp \""
},
{
"path": "cmd/upgrade.go",
"chars": 2355,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\n\tinternalApp \"github.com/haierkeys/fast-note-sync-service/internal/app\"\n\t\"github.com/haierk"
},
{
"path": "cmd/version.go",
"chars": 418,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/app\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n"
},
{
"path": "config/config.yaml",
"chars": 9854,
"preview": "server:\n # 运行模式: release | debug\n # Running mode: release | debug\n run-mode: release\n # HTTP 端口 (默认 :9000)。格式为 :port"
},
{
"path": "docker/Dockerfile",
"chars": 1949,
"preview": "FROM woahbase/alpine-glibc:latest\nMAINTAINER HaierKeys <haierkeys@gmail.com>\nARG TARGETOS\nARG TARGETARCH\nARG VERSION\nARG"
},
{
"path": "docker/docker-compose.yaml",
"chars": 375,
"preview": "services:\n fast-note-sync-service:\n image: haierkeys/fast-note-sync-service:latest\n container_name: fast-note-syn"
},
{
"path": "docker/docker_image_clean.sh",
"chars": 512,
"preview": "#!/bin/sh\necho \"docker images clean shell\"\n\nprojectName=$(basename \"$(pwd)\")\n\n# 删除匹配 repository 名称(任意 tag)的镜像\ndockerrm=$"
},
{
"path": "docker/docker_redeploy.sh",
"chars": 753,
"preview": "#!/bin/bash\n\nProjectRegistry=\"registry.cn-shanghai.aliyuncs.com/xxx/xxxxx\"\nProjectPath=`pwd`\nProjectName=\"xxxxx\"\n\nUsage("
},
{
"path": "docker/entrypoint.sh",
"chars": 558,
"preview": "#!/bin/sh\n\n# 检查环境变量\nif [ -z \"$P_NAME\" ] || [ -z \"$P_BIN\" ]; then\n echo \"Error: P_NAME or P_BIN not set\"\n exit 1\nfi"
},
{
"path": "docs/API-EXTENSIONS.md",
"chars": 4731,
"preview": "# API Extensions for Fast Note Sync Service\n\nThis document describes the new API endpoints added in the `feature/api-ext"
},
{
"path": "docs/CHANGELOG.en.md",
"chars": 14798,
"preview": "# CHANGELOG\n\nAll notable changes to this project will be documented in this file.\n\nThe project adheres to [Keep a Change"
},
{
"path": "docs/CHANGELOG.ja.md",
"chars": 9707,
"preview": "# 更新履歴 (CHANGELOG)\n\nこのプロジェクトのすべての重要な変更がこのファイルに記録されます。\n\nこのプロジェクトは [Keep a Changelog](https://keepachangelog.com/ja/0.3.0/"
},
{
"path": "docs/CHANGELOG.ko.md",
"chars": 10491,
"preview": "# 변경 로그 (CHANGELOG)\n\n이 프로젝트의 모든 주요 변경 사항은 이 파일에 기록됩니다.\n\n이 프로젝트는 [Keep a Changelog](https://keepachangelog.com/en/0.3.0/)"
},
{
"path": "docs/CHANGELOG.zh-CN.md",
"chars": 8421,
"preview": "# 更新日志 (CHANGELOG)\n\n本项目的所有重大变更都将记录在此文件中。\n\n本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/0.3.0/) 规范。\n\n---\n\n##"
},
{
"path": "docs/CHANGELOG.zh-TW.md",
"chars": 8444,
"preview": "# 更新日誌 (CHANGELOG)\n\n本專案的所有重大變更都將記錄在此文件中。\n\n本專案遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/0.3.0/) 規範。\n\n---\n\n##"
},
{
"path": "docs/CHANGELOG_GUIDELINE.md",
"chars": 1275,
"preview": "# CHANGELOG 维护规范\n\n本文档定义了本项目 `docs/CHANGELOG.md` 的维护标准,确保变更记录清晰、专业且符合行业通用规范。\n\n## 1. 基本准则\n- **面向用户**:变更日志应让用户(开发者或最终用户)轻松了"
},
{
"path": "docs/PR-DESCRIPTION.md",
"chars": 4259,
"preview": "# PR: Folder Tree Endpoint + Folder API Bug Fixes\n\n## Summary\n\nAdds a new `GET /api/folder/tree` endpoint that returns t"
},
{
"path": "docs/README.ja.md",
"chars": 10351,
"preview": "[简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github."
},
{
"path": "docs/README.ko.md",
"chars": 11447,
"preview": "[简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github."
},
{
"path": "docs/README.zh-CN.md",
"chars": 8871,
"preview": "[简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github."
},
{
"path": "docs/README.zh-TW.md",
"chars": 8942,
"preview": "[简体中文](https://github.com/haierkeys/fast-note-sync-service/blob/master/docs/README.zh-CN.md) / [English](https://github."
},
{
"path": "docs/REST_API.md",
"chars": 36041,
"preview": "# Fast Note Sync Service - REST API Documentation\n\nThis document is generated from `swagger.json` and provides the lates"
},
{
"path": "docs/Support.csv",
"chars": 6758,
"preview": "收款时间,收款项,金额,单位,留言,昵称\n2026/03/27 00:36:52,任意打赏,128.00,¥,特别棒,一直在用,希望越做越好。,Geeson\n2026/04/18 21:15:46,四杯咖啡☕,100.00,¥,支持一下[抱"
},
{
"path": "docs/Support.en.json",
"chars": 25678,
"preview": "[\n {\n \"time\": \"2026/03/27 00:36:52\",\n \"item\": \"Tip as you like\",\n \"amount\": \"128.00\",\n \"unit\": \"¥\",\n \"me"
},
{
"path": "docs/Support.en.md",
"chars": 14641,
"preview": "# Supporters List\n\n> Thank you very much for supporting this project! Every donation is the driving force for my continu"
},
{
"path": "docs/Support.ja.json",
"chars": 21672,
"preview": "[\n {\n \"time\": \"2026/03/27 00:36:52\",\n \"item\": \"チップはお好みで\",\n \"amount\": \"128.00\",\n \"unit\": \"¥\",\n \"message\":"
},
{
"path": "docs/Support.ja.md",
"chars": 10518,
"preview": "# サポーターリスト\n\n> このプロジェクトを応援していただき、誠にありがとうございます!皆様からのご支援は、継続的なメンテナンスと開発の原動力となっています。 ❤️\n\n### 📜 謝辞リスト\n\n| 受領时间 | 项目 | 金额 | 昵称 "
},
{
"path": "docs/Support.ko.json",
"chars": 21914,
"preview": "[\n {\n \"time\": \"2026/03/27 00:36:52\",\n \"item\": \"원하는 대로 팁을 주세요\",\n \"amount\": \"128.00\",\n \"unit\": \"¥\",\n \"mess"
},
{
"path": "docs/Support.ko.md",
"chars": 10757,
"preview": "# 후원자 명단\n\n> 이 프로젝트를 지원해 주셔서 정말 감사합니다! 여러분의 모든 후원은 지속적인 유지보수와 개발의 원동력이 됩니다. ❤️\n\n### 📜 감사 명단\n\n| 수령 시간 | 항목 | 금액 | 닉네임 | 메시"
},
{
"path": "docs/Support.zh-CN.json",
"chars": 20024,
"preview": "[\n {\n \"time\": \"2026/03/27 00:36:52\",\n \"item\": \"任意打赏\",\n \"amount\": \"128.00\",\n \"unit\": \"¥\",\n \"message\": \"特别"
},
{
"path": "docs/Support.zh-CN.md",
"chars": 8857,
"preview": "# 支持者名单 (Thanks to Supporters)\n\n> 非常感谢大家对本项目的支持!每一份打赏都是我持续维护和迭代的动力。 ❤️\n\n### 📜 致谢列表\n\n| 收款时间 | 收款项 | 金额 | 昵称 | 留言 |\n| :---"
},
{
"path": "docs/Support.zh-TW.json",
"chars": 20007,
"preview": "[\n {\n \"time\": \"2026/03/27 00:36:52\",\n \"item\": \"任一打賞\",\n \"amount\": \"128.00\",\n \"unit\": \"¥\",\n \"message\": \"特別"
},
{
"path": "docs/Support.zh-TW.md",
"chars": 5578,
"preview": "# 支持者名單 (Thanks to Supporters)\n\n> 非常感謝大家對本項目的支持!每一份打賞都是我持續維護和迭代的動力。 ❤️\n\n### 📜 致謝列表\n\n| 收款時間 | 收款項 | 金額 | 昵稱 | 留言 | 備註 |\n|"
},
{
"path": "docs/SyncProtocol.md",
"chars": 2411,
"preview": "# WebSocket 同步协议前端对接说明 (版本 1.1)\n\n本协议描述了最新调整后的同步流程,前端在对接 `NoteSync`, `FolderSync`, `SettingSync` 和 `FileSync` 时需遵循以下规范。\n\n"
},
{
"path": "docs/admin_config_api.md",
"chars": 3507,
"preview": "# 管理员配置接口文档 (`/api/admin/config`)\n\n本文档描述了管理员用于获取和更新系统配置的 API 接口。配置参数同步自 `config/config.yaml`。\n\n---\n\n## 1. 概述\n\n该接口允许具有管理员"
},
{
"path": "docs/docs.go",
"chars": 294708,
"preview": "// Package docs Code generated by swaggo/swag. DO NOT EDIT\npackage docs\n\nimport \"github.com/swaggo/swag\"\n\nconst docTempl"
},
{
"path": "docs/skills/fns-mcp/SKILL.md",
"chars": 3924,
"preview": "---\nname: fns-mcp\ndescription: Fast Note Sync Service MCP SSE Skill (Bilingual). Allows agents to access and manage note"
},
{
"path": "docs/skills/fns-mcp/configs/cherry-studio.md",
"chars": 1240,
"preview": "# Cherry Studio MCP Configuration Guide / 配置指南\n\nFollow these steps to use Fast Note Sync Service in Cherry Studio:\n按照以下步"
},
{
"path": "docs/skills/fns-mcp/configs/hermes.yaml",
"chars": 269,
"preview": "mcp_servers:\n fns-service:\n url: \"http://<YOUR_DOMAIN>:9000/api/mcp/sse\"\n headers:\n Authorization: \"Bearer <"
},
{
"path": "docs/skills/fns-mcp/configs/openclaw.json",
"chars": 341,
"preview": "{\n \"mcpServers\": {\n \"fns-service\": {\n \"url\": \"http://<YOUR_DOMAIN>:9000/api/mcp/sse\",\n \"headers\": {\n "
},
{
"path": "docs/swagger.json",
"chars": 294041,
"preview": "{\n \"swagger\": \"2.0\",\n \"info\": {\n \"description\": \"This is the Fast Note Sync Service HTTP API.\",\n \"ti"
},
{
"path": "docs/swagger.yaml",
"chars": 136440,
"preview": "basePath: /\ndefinitions:\n api_router.HealthResponse:\n properties:\n database:\n description: '\"connected\" "
},
{
"path": "docs/test_ws_debug.html",
"chars": 36277,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "docs/websocket_integration.md",
"chars": 1990,
"preview": "# WebSocket 同步协议更新说明 (2026-03-05)\n\n本文档详细说明了近期 WebSocket 协议的变更,主要涉及笔记、附件和配置同步消息中 `lastTime` 字段的补充。\n\n## 1. 核心变更:引入 `lastTi"
},
{
"path": "docs/ws_api.md",
"chars": 6978,
"preview": "# WebSocket API 全量对接文档 (100% 完整版)\n\n本手册为前端开发人员提供服务端 WebSocket 接口的**完全定义**。涵盖所有模块(笔记、文件夹、文件、设置)的请求、响应、推送消息及详细字段结构。\n\n---\n\n#"
},
{
"path": "docs/ws_setting_clear_api.md",
"chars": 1113,
"preview": "# WebSocket API: 清理配置 (SettingClear) 对接文档\n\n本文档描述了如何通过 WebSocket 接口清理指定笔记库(Vault)的所有配置信息。\n\n## 1. 客户端请求 (Client -> Server)"
},
{
"path": "frontend/assets/alert-dialog-CfMssux5.js",
"chars": 2817,
"preview": "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-l"
},
{
"path": "frontend/assets/auth-form-BjZ9qVzL.js",
"chars": 8188,
"preview": "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-C"
},
{
"path": "frontend/assets/badge-C63ATniC.js",
"chars": 715,
"preview": "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"
},
{
"path": "frontend/assets/canvas-viewer-Bt8OKmt9.css",
"chars": 3200,
"preview": ".canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.25rem;margin-bottom:.25rem}@media(min"
},
{
"path": "frontend/assets/canvas-viewer-Cxwbo1vR.js",
"chars": 17459,
"preview": "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\"./t"
},
{
"path": "frontend/assets/checkbox-DhTHgmeh.js",
"chars": 3867,
"preview": "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."
},
{
"path": "frontend/assets/circle-alert-EFzISefA.js",
"chars": 425,
"preview": "import{c as e}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/circle-alert-EFzISefA.js.br",
"chars": 145,
"preview": "\u001b\u0001@lN_.\n\u0004\u0004\u0012pL}\"KӁtQzFY6xxm\u0018\u0016<tKp\u000f]Qz,\u0017UK-DQ\u0012RBڟ0I[¥<\u001b<dQ7mQ\u0006\u0001eFAb-4S\u0001(-8$E\"NWe\u0015Wxtf~+f]LJGx\u0007%Y{!C\u0014_#%/\u0017\u0015\u0015y撋\u0002rۗt\u0018\u0006qh'^\n"
},
{
"path": "frontend/assets/clock-C9LPHszx.js",
"chars": 359,
"preview": "import{c as o}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/copy-CEhXannp.js",
"chars": 687,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/database-eyf5nvY6.js",
"chars": 816,
"preview": "import{c as e}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/download-CKtDCbjj.js",
"chars": 438,
"preview": "import{c as o}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/en-vU35wTjd.js",
"chars": 39731,
"preview": "const e={\"ui.common.title\":\"Fast Note Sync\",\"ui.common.subtitle\":\"High-performance, low-latency note synchronization, ma"
},
{
"path": "frontend/assets/eye-DrvrOb4o.js",
"chars": 976,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/file-manager-Bz0QGSbU.js",
"chars": 26185,
"preview": "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"
},
{
"path": "frontend/assets/file-type-DbD_pFnN.js",
"chars": 513,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/font-loader-B-ynJ_1p.css",
"chars": 121815,
"preview": "/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) "
},
{
"path": "frontend/assets/font-loader-CIrh3KnA.js",
"chars": 1021306,
"preview": "function _mergeNamespaces(A,t){for(var e=0;e<t.length;e++){const a=t[e];if(\"string\"!=typeof a&&!Array.isArray(a))for(con"
},
{
"path": "frontend/assets/format-CdHm7RWL.js",
"chars": 20128,
"preview": "import{c as t}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/git-automation-tBJ0Wppw.js",
"chars": 27153,
"preview": "import{c as e,u as s,r as t,b as a,a as r,e as i,t as n,j as l,a0 as o,f as c,B as d,C as m,a6 as x,I as u,a7 as h}from\""
},
{
"path": "frontend/assets/git-branch-B1vNHBXG.js",
"chars": 450,
"preview": "import{c}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is licensed"
},
{
"path": "frontend/assets/github-Bzk-4SPC.js",
"chars": 578,
"preview": "import{c}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is licensed"
},
{
"path": "frontend/assets/hard-drive-Dw58lXyp.js",
"chars": 566,
"preview": "import{c as y}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/history-BseqF3eb.js",
"chars": 413,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/image-BFJJNQpe.js",
"chars": 879,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/index-JfsWWBj_.js",
"chars": 603889,
"preview": "import{r as t,j as e}from\"./font-loader-CIrh3KnA.js\";let i=[],n=[];function r(t){if(t<768)return!1;for(let e=0,r=i.lengt"
},
{
"path": "frontend/assets/ja-Q5acyAjl.js",
"chars": 31641,
"preview": "const e={\"ui.common.title\":\"Fast Note Sync\",\"ui.common.subtitle\":\"高性能、低遅延のノート同期、管理、REST サービス\",\"ui.common.footerTitle\":\"G"
},
{
"path": "frontend/assets/ko-CMKMFQrR.js",
"chars": 31034,
"preview": "const e={\"ui.common.title\":\"Fast Note Sync\",\"ui.common.subtitle\":\"고성능, 저지연 노트 동기화, 관리, REST 서비스\",\"ui.common.footerTitle\""
},
{
"path": "frontend/assets/main-BIi-kGYY.js",
"chars": 154281,
"preview": "const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[\"assets/note-manager-DjJcxkCE.js\",\"assets/font-loader-CIrh3KnA."
},
{
"path": "frontend/assets/markdown-editor-CX5kQlgI.js",
"chars": 703327,
"preview": "var e,t;import{c as n,a9 as r,j as a,aa as i,ab as s,r as o,G as l,u as c,t as u,f as d,a8 as p,e as h,X as f,ac as m,ad"
},
{
"path": "frontend/assets/markdown-editor-DMUawZD_.css",
"chars": 1912,
"preview": ".cm-editor{background-color:transparent!important;font-family:inherit!important}.cm-editor .cm-content{font-family:inher"
},
{
"path": "frontend/assets/monitor-BGNS5Y9j.js",
"chars": 784,
"preview": "import{c as e}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/note-handle-IK8dQjtF.js",
"chars": 8600,
"preview": "import{r as e,b as o,e as t,a,t as s}from\"./font-loader-CIrh3KnA.js\";function r(){const r=localStorage.getItem(\"token\"),"
},
{
"path": "frontend/assets/note-manager-DjJcxkCE.js",
"chars": 67403,
"preview": "const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[\"assets/markdown-editor-CX5kQlgI.js\",\"assets/font-loader-CIrh3K"
},
{
"path": "frontend/assets/pencil-DqQhr35g.js",
"chars": 452,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/plus-BBfuNxDX.js",
"chars": 329,
"preview": "import{c as s}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/refresh-cw-BxIJAPy3.js",
"chars": 496,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/search-DdihTHF8.js",
"chars": 343,
"preview": "import{c}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is licensed"
},
{
"path": "frontend/assets/select-CJF_alSt.js",
"chars": 21224,
"preview": "import{r as e,j as t,aj as n,aD as o,af as r,ag as a,aE as l,ah as s,ai as i,aF as c,aB as d,aG as u,aH as p,an as f,aI "
},
{
"path": "frontend/assets/server-DzJVVqse.js",
"chars": 514,
"preview": "import{c as e}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/server-DzJVVqse.js.br",
"chars": 133,
"preview": "\u001b\u0001\u0002 ^yt6QS\u0005\u00029Y9\u0012K\u0005(i>XwQS\u001ce}}\u0016nN(\u0001ŊmPB\t4\u0015ﵶ\u0019\u0013}GI,K\u0001\u000bmwks+{q>-i2\bpQZeM@t\u0004͙\u000f1j\u001c\u0011TWP2F~\u0001X\t7g@Ư\u0011!\u00169sFU6*P%$+Gƪ\u000b{Գ\u000b\u0014ץq\u0014zhf R"
},
{
"path": "frontend/assets/setting-manager-DaP9o-yD.js",
"chars": 14797,
"preview": "import{c as e,r as s,b as t,a,e as r,t as n,u as o,a7 as i,j as l,B as c,C as d,I as m,X as h}from\"./font-loader-CIrh3Kn"
},
{
"path": "frontend/assets/share-2-BVJjAadJ.js",
"chars": 527,
"preview": "import{c}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is licensed"
},
{
"path": "frontend/assets/share-CN7oeKGv.js",
"chars": 12127,
"preview": "const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[\"assets/font-loader-CIrh3KnA.js\",\"assets/font-loader-B-ynJ_1p.c"
},
{
"path": "frontend/assets/shield-check-CH_gKEpx.js",
"chars": 744,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/sync-backup-Bp7n2yHp.js",
"chars": 42644,
"preview": "import{u as e,a7 as s,r as t,a,e as r,b as o,t as i,j as n,ae as l,a0 as c,f as d,B as u,C as m,I as x,ad as p,a6 as h,a"
},
{
"path": "frontend/assets/sync-log-manager-Zjq-lA99.js",
"chars": 15513,
"preview": "import{c as e,r as s,b as a,a as l,e as r,t,u as n,j as i,ae as c,f as d,B as o,C as m,a6 as x,aA as u}from\"./font-loade"
},
{
"path": "frontend/assets/system-settings-DSUsRYMo.js",
"chars": 70684,
"preview": "import{c as e,r as s,af as t,ag as a,j as r,ah as n,ai as i,aj as l,ak as o,al as d,am as c,an as m,ao as u,f as x,a as "
},
{
"path": "frontend/assets/table-D9wbHMTA.js",
"chars": 1458,
"preview": "import{r as a,j as e,f as s}from\"./font-loader-CIrh3KnA.js\";const r=a.forwardRef(({className:a,...r},l)=>e.jsx(\"div\",{cl"
},
{
"path": "frontend/assets/text-cursor-input-Bphfsfyn.js",
"chars": 573,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/tooltip-Dr-qRlmI.js",
"chars": 2042,
"preview": "import{r as e,j as t,f as r,g as o}from\"./font-loader-CIrh3KnA.js\";function n({children:n,content:s,delay:a=300,side:c=\""
},
{
"path": "frontend/assets/trash-2-ad7PiUnC.js",
"chars": 533,
"preview": "import{c as e}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/vault-list-BzYzvdPK.js",
"chars": 58360,
"preview": "import{c as e,r as t,V as n,g as r,u as o,a7 as i,t as a,e as s,j as l,I as c,X as d,B as u,a5 as h}from\"./font-loader-C"
},
{
"path": "frontend/assets/zap-CLLhzk_y.js",
"chars": 438,
"preview": "import{c as a}from\"./font-loader-CIrh3KnA.js\";\n/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is lic"
},
{
"path": "frontend/assets/zh-CN-BZhLE8JW.js",
"chars": 27781,
"preview": "const e={\"ui.common.title\":\"Fast Note Sync\",\"ui.common.subtitle\":\"高性能、低延迟的笔记同步, 管理, REST 服务\",\"ui.common.footerTitle\":\"基于"
},
{
"path": "frontend/assets/zh-TW-DGtNjFz9.js",
"chars": 27774,
"preview": "const e={\"ui.common.title\":\"Fast Note Sync\",\"ui.common.subtitle\":\"高性能、低延遲的筆記同步, 管理, REST 服務\",\"ui.common.footerTitle\":\"基於"
},
{
"path": "frontend/assets/zod-B54Zg8Xp.js",
"chars": 78279,
"preview": "import{V as e}from\"./font-loader-CIrh3KnA.js\";var t,s,r;(s=t||(t={})).assertEqual=e=>{},s.assertIs=function(e){},s.asser"
},
{
"path": "frontend/index.html",
"chars": 830,
"preview": "<!DOCTYPE html>\n<html translate=\"no\">\n\n<head>\n <meta charset=\"UTF-8\" />\n <link id=\"favicon\" rel=\"icon\" type=\"image/svg"
},
{
"path": "frontend/share.html",
"chars": 1637,
"preview": "<!DOCTYPE html>\n<html translate=\"no\">\n\n<head>\n <meta charset=\"UTF-8\" />\n <link id=\"favicon\" rel=\"icon\" type=\"image"
},
{
"path": "frontend/static/fonts/local.css",
"chars": 453,
"preview": "@font-face {\n font-family: \"CustomFont\";\n src: url(\"./font.woff2\") format(\"woff2\");\n font-weight: normal;\n font-styl"
},
{
"path": "frontend/static/fonts/remote.css",
"chars": 496,
"preview": "@font-face {\n font-family: \"CustomFont\";\n src: url(\"https://ik.imagekit.io/haierkeys/LXGWWenKai-Light.woff2\") format(\""
},
{
"path": "frontend/static/images/site.webmanifest",
"chars": 273,
"preview": "{\n \"name\": \"Fast Note Sync\",\n \"short_name\": \"FastNoteSync\",\n \"icons\": [\n {\n \"src\": \"/static/images/icon.svg\","
},
{
"path": "go.mod",
"chars": 8479,
"preview": "module github.com/haierkeys/fast-note-sync-service\n\ngo 1.26.2\n\nrequire (\n\tgithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+inc"
},
{
"path": "go.sum",
"chars": 106025,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
},
{
"path": "internal/app/app.go",
"chars": 17546,
"preview": "// Package app provides application container, encapsulates all dependencies and services\n// Package app 提供应用容器,封装所有依赖和服"
},
{
"path": "internal/app/config.go",
"chars": 4568,
"preview": "// Package app provides application container, encapsulates all dependencies and services\n// Package app 提供应用容器,封装所有依赖和服"
},
{
"path": "internal/app/infra.go",
"chars": 1916,
"preview": "package app\n\nimport (\n\t\"context\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/dao\"\n\tpkgapp \"github.com/haierk"
},
{
"path": "internal/app/repos.go",
"chars": 1607,
"preview": "package app\n\nimport (\n\t\"github.com/haierkeys/fast-note-sync-service/internal/dao\"\n\t\"github.com/haierkeys/fast-note-sync-"
},
{
"path": "internal/app/restart_linux.go",
"chars": 31,
"preview": "//go:build ignore\n\npackage app\n"
},
{
"path": "internal/app/restart_unix.go",
"chars": 276,
"preview": "//go:build !windows\n\npackage app\n\nimport (\n\t\"syscall\"\n)\n\n// RestartProcess restarts the current process using syscall.Ex"
},
{
"path": "internal/app/restart_windows.go",
"chars": 433,
"preview": "//go:build windows\n\npackage app\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n)\n\n// RestartProcess restarts the current process by starting"
},
{
"path": "internal/app/services.go",
"chars": 3981,
"preview": "package app\n\nimport (\n\t\"github.com/haierkeys/fast-note-sync-service/internal/service\"\n\t\"go.uber.org/zap\"\n)\n\n// Services "
},
{
"path": "internal/app/testing.go",
"chars": 762,
"preview": "// Package app provides application container, encapsulates all dependencies and services\n// Package app 提供应用容器,封装所有依赖和服"
},
{
"path": "internal/app/version.go",
"chars": 480,
"preview": "// Package app provides application container, encapsulates all dependencies and services\n// Package app 提供应用容器,封装所有依赖和服"
},
{
"path": "internal/config/app.go",
"chars": 3604,
"preview": "package config\n\n// AppSettings application settings\n// AppSettings 应用设置\ntype AppSettings struct {\n\t// DefaultPageSize de"
},
{
"path": "internal/config/database.go",
"chars": 2694,
"preview": "package config\n\n// DatabaseConfig database configuration\n// DatabaseConfig 数据库配置\ntype DatabaseConfig struct {\n\tType "
},
{
"path": "internal/config/git.go",
"chars": 321,
"preview": "package config\n\n// GitConfig Git commit configuration\n// GitConfig Git 提交配置\ntype GitConfig struct {\n\t// Name author name"
},
{
"path": "internal/config/log.go",
"chars": 450,
"preview": "package config\n\n// LogConfig log configuration\n// LogConfig 日志配置\ntype LogConfig struct {\n\t// Level log level, see zapcor"
},
{
"path": "internal/config/security.go",
"chars": 567,
"preview": "package config\n\n// SecurityConfig security configuration\n// SecurityConfig 安全配置\ntype SecurityConfig struct {\n\tAuthTokenK"
},
{
"path": "internal/config/server.go",
"chars": 829,
"preview": "package config\n\n// ServerConfig server configuration\n// ServerConfig 服务器配置\ntype ServerConfig struct {\n\t// RunMode run mo"
},
{
"path": "internal/config/short_link.go",
"chars": 329,
"preview": "package config\n\n// ShortLinkConfig short link configuration\n// ShortLinkConfig 短链配置\ntype ShortLinkConfig struct {\n\tBaseU"
},
{
"path": "internal/config/storage.go",
"chars": 1041,
"preview": "package config\n\n// StorageConfig Storage configuration\n// StorageConfig 存储配置\ntype StorageConfig struct {\n\tLocalFS S"
},
{
"path": "internal/config/tracer.go",
"chars": 369,
"preview": "package config\n\n// TracerConfig request tracing configuration\n// TracerConfig 请求追踪配置\ntype TracerConfig struct {\n\t// Enab"
},
{
"path": "internal/config/tunnel.go",
"chars": 719,
"preview": "package config\n\n// NgrokConfig ngrok configuration\n// NgrokConfig ngrok 配置\ntype NgrokConfig struct {\n\t// Enabled whether"
},
{
"path": "internal/config/user.go",
"chars": 369,
"preview": "package config\n\n// UserConfig user configuration\n// UserConfig 用户配置\ntype UserConfig struct {\n\t// RegisterIsEnable whethe"
},
{
"path": "internal/config/webgui.go",
"chars": 170,
"preview": "package config\n\n// WebGUIConfig Web GUI configuration\n// WebGUIConfig Web GUI 配置\ntype WebGUIConfig struct {\n\tFontSet str"
},
{
"path": "internal/dao/backup_repository.go",
"chars": 9905,
"preview": "package dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/domain\"\n\t\"github"
},
{
"path": "internal/dao/dao.go",
"chars": 22206,
"preview": "package dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/haier"
},
{
"path": "internal/dao/dao_helper.go",
"chars": 2222,
"preview": "package dao\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/pkg/fileurl\"\n)\n\n// ge"
},
{
"path": "internal/dao/file_repository.go",
"chars": 20498,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/fil"
},
{
"path": "internal/dao/folder_repository.go",
"chars": 7134,
"preview": "package dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/domain\"\n\t\"gi"
},
{
"path": "internal/dao/git_sync_repository.go",
"chars": 9323,
"preview": "package dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/domain\"\n\t\"gi"
},
{
"path": "internal/dao/note_fts_repository.go",
"chars": 6856,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"gi"
},
{
"path": "internal/dao/note_history_repository.go",
"chars": 12161,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"s"
},
{
"path": "internal/dao/note_link_repository.go",
"chars": 5288,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"tim"
},
{
"path": "internal/dao/note_repository.go",
"chars": 30195,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"s"
},
{
"path": "internal/dao/setting_repository.go",
"chars": 12379,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"s"
},
{
"path": "internal/dao/storage_repository.go",
"chars": 5819,
"preview": "package dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/domain\"\n\t\"gi"
},
{
"path": "internal/dao/sync_log_repository.go",
"chars": 5441,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"syn"
},
{
"path": "internal/dao/user_repository.go",
"chars": 4305,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"githu"
},
{
"path": "internal/dao/user_share_repository.go",
"chars": 12141,
"preview": "package dao\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/inte"
},
{
"path": "internal/dao/vault_repository.go",
"chars": 6169,
"preview": "// Package dao implements the data access layer\n// Package dao 实现数据访问层\npackage dao\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"tim"
},
{
"path": "internal/domain/domain_backup.go",
"chars": 2813,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\nconst (\n\tBackupStatusIdle = 0\n\tBackupStatusRunning = 1\n\tBackupStatu"
},
{
"path": "internal/domain/domain_file.go",
"chars": 4346,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// FileAction 定义文件操作类型\ntype FileAction string\n\nconst (\n\tFileActionCreate"
},
{
"path": "internal/domain/domain_folder.go",
"chars": 1882,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// FolderAction 定义文件夹操作类型\ntype FolderAction string\n\nconst (\n\tFolderActio"
},
{
"path": "internal/domain/domain_git_sync.go",
"chars": 2803,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\nconst (\n\tGitSyncStatusIdle = 0\n\tGitSyncStatusRunning = 1\n\tGitSyncSt"
},
{
"path": "internal/domain/domain_note.go",
"chars": 5462,
"preview": "// Package domain 定义领域模型和接口\npackage domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// NoteAction 定义笔记操作类型\ntype NoteAction strin"
},
{
"path": "internal/domain/domain_note_fts.go",
"chars": 1092,
"preview": "package domain\n\nimport (\n\t\"context\"\n)\n\n// NoteFTSRepository FTS full-text search repository interface\n// NoteFTSReposito"
},
{
"path": "internal/domain/domain_note_history.go",
"chars": 1758,
"preview": "// Package domain 定义领域模型和接口\npackage domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// NoteHistory 笔记历史领域模型\ntype NoteHistory str"
},
{
"path": "internal/domain/domain_note_link.go",
"chars": 1495,
"preview": "// Package domain defines domain models and interfaces\npackage domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// NoteLink repre"
},
{
"path": "internal/domain/domain_setting.go",
"chars": 2497,
"preview": "// Package domain 定义领域模型和接口\npackage domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// SettingAction 定义配置操作类型\ntype SettingAction"
},
{
"path": "internal/domain/domain_storage.go",
"chars": 997,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// Storage 存储配置领域模型\ntype Storage struct {\n\tID int64\n\tUID "
},
{
"path": "internal/domain/domain_sync_log.go",
"chars": 3983,
"preview": "// Package domain defines the core business domain models and repository interfaces\n// Package domain 定义核心业务领域模型和仓储接口\npa"
},
{
"path": "internal/domain/domain_user.go",
"chars": 1106,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// User 用户领域模型\ntype User struct {\n\tUID int64\n\tEmail string\n\tUs"
},
{
"path": "internal/domain/domain_user_share.go",
"chars": 3507,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n)\n\nvar (\n\tErrShareCancelled = errors.New(\"share has been ca"
},
{
"path": "internal/domain/domain_vault.go",
"chars": 1357,
"preview": "package domain\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n\n// Vault 仓库领域模型\ntype Vault struct {\n\tID int64\n\tUID int64\n\tN"
},
{
"path": "internal/domain/mocks/mock_backup_repository.go",
"chars": 3407,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_file_repository.go",
"chars": 7282,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_folder_repository.go",
"chars": 3408,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_git_sync_repository.go",
"chars": 3634,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_note_fts_repository.go",
"chars": 1475,
"preview": "package mocks\n\nimport (\n\t\"context\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/domain\"\n\t\"github.com/stretchr"
},
{
"path": "internal/domain/mocks/mock_note_history_repository.go",
"chars": 3154,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_note_link_repository.go",
"chars": 2119,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_note_repository.go",
"chars": 8204,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_setting_repository.go",
"chars": 3655,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_storage_repository.go",
"chars": 1865,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_sync_log_repository.go",
"chars": 1310,
"preview": "package mocks\n\nimport (\n\t\"context\"\n\n\t\"github.com/haierkeys/fast-note-sync-service/internal/domain\"\n\t\"github.com/stretchr"
},
{
"path": "internal/domain/mocks/mock_user_repository.go",
"chars": 2377,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_user_share_repository.go",
"chars": 4369,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/domain/mocks/mock_vault_repository.go",
"chars": 2962,
"preview": "// Package mocks provides testify/mock implementations for domain Repository interfaces.\n// Package mocks 提供 domain Repo"
},
{
"path": "internal/dto/admin_dto.go",
"chars": 11051,
"preview": "package dto\n\nimport \"time\"\n\n// AdminWebGUIConfig WebGUI configuration response structure (public interface)\n// AdminWebG"
},
{
"path": "internal/dto/app_dto.go",
"chars": 1893,
"preview": "// Package dto Defines data transfer objects (request parameters and response structs)\n// Package dto 定义数据传输对象(请求参数和响应结构"
},
{
"path": "internal/dto/backup.go",
"chars": 4682,
"preview": "package dto\n\nimport \"github.com/haierkeys/fast-note-sync-service/pkg/timex\"\n\n// BackupConfigRequest backup configuration"
},
{
"path": "internal/dto/conflict_dto.go",
"chars": 1558,
"preview": "// Package dto Defines data transfer objects (request parameters and response structs)\n// Package dto 定义数据传输对象(请求参数和响应结构"
},
{
"path": "internal/dto/file_dto.go",
"chars": 9208,
"preview": "// Package dto Defines data transfer objects (request parameters and response structs)\n// Package dto 定义数据传输对象(请求参数和响应结构"
},
{
"path": "internal/dto/file_dto_ws.go",
"chars": 6911,
"preview": "package dto\n\n// FileSyncModifyMessage message content for file modification or creation\n// FileSyncModifyMessage 文件修改或创建"
},
{
"path": "internal/dto/folder_dto.go",
"chars": 7319,
"preview": "package dto\n\nimport \"github.com/haierkeys/fast-note-sync-service/pkg/timex\"\n\n// FolderGetRequest Request parameters for "
},
{
"path": "internal/dto/folder_dto_ws.go",
"chars": 3610,
"preview": "package dto\n\n// FolderSyncEndMessage defines the folder sync end message structure\n// FolderSyncEndMessage 定义文件夹同步结束的消息结"
},
{
"path": "internal/dto/git_sync_dto.go",
"chars": 3830,
"preview": "package dto\n\nimport \"github.com/haierkeys/fast-note-sync-service/pkg/timex\"\n\n// GitSyncConfigRequest git repository sync"
},
{
"path": "internal/dto/note_dto.go",
"chars": 22408,
"preview": "// Package dto Defines data transfer objects (request parameters and response structs)\n// Package dto 定义数据传输对象(请求参数和响应结构"
}
]
// ... and 347 more files (download for full content)
About this extraction
This page contains the full source code of the haierkeys/fast-note-sync-service GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 547 files (6.2 MB), approximately 1.6M tokens, and a symbol index with 7808 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.