cotutor 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +44 -2
- package/dist/app/cli/backup.d.ts +68 -0
- package/dist/app/cli/backup.js +243 -0
- package/dist/app/cli/doctor-fix.d.ts +18 -0
- package/dist/app/cli/doctor-fix.js +70 -0
- package/dist/app/cli/doctor.js +8 -0
- package/dist/app/cli/init.d.ts +2 -0
- package/dist/app/cli/init.js +6 -3
- package/dist/app/cli/main.js +103 -0
- package/dist/app/cli/report.d.ts +11 -0
- package/dist/app/cli/report.js +110 -0
- package/dist/app/cli/root-ca.d.ts +8 -0
- package/dist/app/cli/root-ca.js +33 -0
- package/dist/app/cli/serve.d.ts +11 -2
- package/dist/app/cli/serve.js +58 -3
- package/dist/app/cli/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +2 -2
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +3 -1
- package/dist/app/http/access.js +1 -1
- package/dist/app/http/app.js +4 -0
- package/dist/app/http/parent-home.js +9 -0
- package/dist/app/http/parent-setup.d.ts +19 -0
- package/dist/app/http/parent-setup.js +96 -0
- package/dist/app/http/stage.js +2 -2
- package/dist/app/mock/mock.js +3 -0
- package/dist/memory/vault-notes.d.ts +4 -0
- package/dist/memory/vault-notes.js +11 -0
- package/dist/shared/upgrade.d.ts +2 -0
- package/dist/shared/upgrade.js +5 -1
- package/dist/stage/fonts/Assistant/Assistant-Bold.woff2 +0 -0
- package/dist/stage/fonts/Assistant/Assistant-Medium.woff2 +0 -0
- package/dist/stage/fonts/Assistant/Assistant-Regular.woff2 +0 -0
- package/dist/stage/fonts/Assistant/Assistant-SemiBold.woff2 +0 -0
- package/dist/stage/fonts/Cascadia/CascadiaCode-Regular.woff2 +0 -0
- package/dist/stage/fonts/ComicShanns/ComicShanns-Regular-279a7b317d12eb88de06167bd672b4b4.woff2 +0 -0
- package/dist/stage/fonts/ComicShanns/ComicShanns-Regular-6e066e8de2ac57ea9283adb9c24d7f0c.woff2 +0 -0
- package/dist/stage/fonts/ComicShanns/ComicShanns-Regular-dc6a8806fa96795d7b3be5026f989a17.woff2 +0 -0
- package/dist/stage/fonts/ComicShanns/ComicShanns-Regular-fcb0fc02dcbee4c9846b3e2508668039.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-349fac6ca4700ffec595a7150a0d1e1d.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-3f2c5db56cc93c5a6873b1361d730c16.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-41b173a47b57366892116a575a43e2b6.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-623ccf21b21ef6b3a0d87738f77eb071.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-b9dcf9d2e50a1eaf42fc664b50a3fd0d.woff2 +0 -0
- package/dist/stage/fonts/Excalifont/Excalifont-Regular-be310b9bcd4f1a43f571c46df7809174.woff2 +0 -0
- package/dist/stage/fonts/Liberation/LiberationSans-Regular.woff2 +0 -0
- package/dist/stage/fonts/Lilita/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 +0 -0
- package/dist/stage/fonts/Lilita/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 +0 -0
- package/dist/stage/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 +0 -0
- package/dist/stage/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 +0 -0
- package/dist/stage/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 +0 -0
- package/dist/stage/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 +0 -0
- package/dist/stage/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 +0 -0
- package/dist/stage/fonts/Virgil/Virgil-Regular.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-019d66dcad46dc156b162d267f981c20.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-04b718e5623574919c8b0dea5f301444.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-069e77aac84590e2e991d0a0176d34f2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-06c77b8c66e51ed6c63ccb502dd8b8af.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-08e0dc436ad0ad61ba5558db0674d762.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-093b9ef39a46ceae95a1df18a0a3a326.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-095c169f3314805276f603a362766abd.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-09850c4077f3fffe707905872e0e2460.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-0986d134c05864f5025962eef9f994a0.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-0b5d723fdc4e249c140f0909e87d03b4.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-0f626226ba1272e832aea87bafd9720e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-0f7fb1e0d5015bb1371343153ecf7ce3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-0fa55a080fcd0f9dc2e0b0058b793df8.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-0facdf1ea213ba40261022f5d5ed4493.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-10a7ae9a371830a80c3d844acf1c02d7.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-112c051027b2d766c19a519f6ee1f4f7.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-11c345711937f0ba4b8f7b6b919c8440.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-12b52b58eb3df36804b9a654ec9ee194.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-13ae07ed2e272d26d59bc0691cd7117a.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-13d2887ec8ee73c43acdabc52a05af7b.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-145aa02cdd91946e67dc934e1acffe75.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-15dc6d811c9cd078f9086a740d5a1038.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-173945821411c09f70c95f98d590e697.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-1b611157cd46bb184d4fa4dae2d6a2b8.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-1e6fd68f1f3902ce48ce8c69df385622.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-1ee544f0f1dac422545c505baa788992.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-1fdc0c67ed57263a80fd108c1f6ccf24.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-203b0e569e3b14aac86a003dc3fa523e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-20cc1bbf50e7efb442756cb605672c1f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-20e7bf72fa05de9adf7dbcc7bf51dde6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-21430ee05a1248901da8d0de08744d47.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-23686f7f29da6e8008c36dd3a80c83d6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-23ad2d71b280f00b1363b95b7bea94eb.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-23f228f3999c01983860012330e4be08.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-24476a126f129212beb33f66853ea151.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-24a21c1e4449222e8d1898d69ff3a404.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-25b7f38e18f035f96cb5e547bd2bd08c.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-29cec36cd205b211da97acabaa62f055.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2a26d20a23b00898ce82f09d2ee47c3f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2adbc89c11e65905393d3dfc468b9d5b.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2b7441d46298788ac94e610ffcc709b6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2b77e8ebfb2367ab2662396a60e7d320.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2cf96d082d35ea3d8106851223ad0d16.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2d43040e86ff03ba677f6f9c04cd0805.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-2e33e8dc771ef5e1d9127d60a6b73679.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-33432927cd87d40cfe393c7482bf221f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-353f33792a8f60dc69323ddf635a269e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-36925dfe329a45086cbb7fc5c20d45ac.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3717077e38f98d89eae729b6c14e56dc.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3756e81d3e149cf6099163ee79944fec.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-395c35dd584b56b0789f58a0559beaf1.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3c9de2ae0ea4bc91a510942dfa4be8d2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3cc70dbb64df5b21f1326cc24dee2195.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3e1f8f654357353bf0e04ba5c34b5f7f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3e63ed8162808a9e425ed80a8bc79114.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-3eaa538115d76932653c21d8dc28f207.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4095eb84ef3874e2600247bee0b04026.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-41521fade99856108931b4768b1b2648.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-450da755d5bcb70906e1295e559b9602.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-452225341522a7942f0f6aab1a5c91a3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4535823663ad81405188a528d8f2b1a2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4806e761d750087c2d734fc64596eaff.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4a0fdb40036e87b40aa08dd30584cb85.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4a38cc3e9cf104e69ba246d37f8cf135.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4aca6a43e59aceee2166b0c7e4e85ef1.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4bfaa8ffa64c5ee560aa2daba7c9cbd3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4dc6d5f188d5c96d44815cd1e81aa885.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4ddc14ed3eb0c3e46364317dfc0144a3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4ee10ae43505e2e0bc62656ced49c0fa.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-4f50e5136e136527280bc902c5817561.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-51502f1206be09c565f1547c406e9558.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-51a0e808bbc8361236ac521a119758a3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-52a84a22fd1369bffeaf21da2d6158dc.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5330a2119a716e4e7224ed108b085dac.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-543fa46ace099a7099dad69123399400.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-544fc28abe2c5c30e62383fd4dac255f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-54acdfc2166ad7fcbd074f75fd4a56ba.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5572b3513ba8df57a3d5d7303ee6b11b.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-56467a5c8840c4d23a60b2f935114848.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-56a32a7689abd0326e57c10c6c069bb4.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-57862b464a55b18c7bf234ce22907d73.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-583d166e56ba0de4b77eabb47ef67839.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5882ffa04f32584d26109137e2da4352.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-58fd02350d0bc52cf1ca3bb32ce9766e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5935a5775af3d5c6307ac667bd9ae74e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-59e9ff77b0efaf684bc09274fb6908c9.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5a1ce3117cfe90c48e8fb4a9a00f694d.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5a45d991244d4c7140217e1e5f5ca4f4.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5a7fac4b8b23a6e4e5ba0c9bf1756c91.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5b0ed6971aaab9c8ad563230bd5471a7.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-5d2898fbc097a7e24c6f38d80587621e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-603aefd23e350ba7eb124273e3c9bcf1.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-60a3089806700d379f11827ee9843b6b.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-60a41c7e1c68f22424e6d22df544bc82.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-642b26e2e5f5fb780b51b593dbc8c851.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-66493ba5a8367f2928812f446f47b56a.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-670ba603758d94268e8606f240a42e12.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-671a2c20b1eb9e4ef8a192833940e319.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-684d65f1793cac449dde5d59cb3c47fb.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-69c09cc5fa3e55c74fc4821f76909cc3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-6ae5b42180ad70b971c91e7eefb8eba2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-6e092f71c1e634059ada0e52abadce67.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-6f3256af8454371776bc46670d33cc65.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-6fe5c5973cc06f74b2387a631ea36b88.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-70c2eb8d64e71a42a834eb857ea9df51.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-70e811fd7994e61f408c923de6ddd078.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7197d6fda6cba7c3874c53d6381ca239.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-72252d73220fa3cd856677888cee1635.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-72536a3d71b694a0d53dd90ddceae41e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-726303e0774b4e678bff8c2deb6ca603.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-733171b4ffcd17ea1fe1c0ba627173bf.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-739bc1a567439c7cffcd1614644593d2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-73e309718fd16cea44b4d54a33581811.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7494dc504ae00ee9cd0505f990f88c5d.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-74e2263a91439c25b91d5132ce9f4d62.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-761d05e3cd968cf574166867998ef06a.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7718fe60986d8b42b1be9c5ace5ccf25.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-774d4f764a1299da5d28ec2f2ffe0d69.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-79d494361ae093b69e74ee9dbe65bfd4.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-79f007c1c6d07557120982951ea67998.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7a07ddc0f0c0f5f4a9bad6ee3dda66b5.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7ab2bed91166a9dca83a5ebfbe2a7f38.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7ccce86603f80a099ddb0cb21d4ae3e3.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7e4bde7e9c7f84cd34d8a845e384c746.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7e929f262f30c8ee78bf398150b1a7cd.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7eb9fffd1aa890d07d0f88cc82e6cfe4.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-7f855356ab893b0d2b9c1c83b8116f0e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-829615148e6357d826b9242eb7fbbd1e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-866fa7613df6b3fd272bcfd4530c0bb9.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-87599f94b6cc129d505b375798d0d751.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-8c2f33cee3993174f7e87c28e4bf42ee.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-8d3bcabb847b56243b16afe62adaaf21.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-8de5b863cb50dfefdd07cb11c774d579.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-8e9f97f01034820170065b2921b4fb5e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-8f476c4c99813d57cbe6eca4727388ad.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-903bb6865f3452e2fda42e3a25547bc5.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-91ddb2969bf2d31ba02ad82998d1314c.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-938d90c10ff8c20386af7f242c05d6b0.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-93fc8f28a33234bcadf1527cafabd502.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-95429962233afd82db1c27df1500a28c.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-9544732d2e62d1a429674f8ee41b5d3a.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-9592bfc861f07bcb8d75c196b370e548.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-95bfd249da4902577b4b7d76ebdd0b44.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-968cffdc8ee679da094e77ebf50f58ef.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-97f7f48ce90c9429bf32ae51469db74d.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-982b630266d87db93d2539affb1275c6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-98f2ad84457de7f3740d9920b8fa8667.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-99a16ef6a64934d5781933dbd9c46b2e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-9cfb2a77a4e45025105ad29a1748b90d.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-9d81066dd2b337c938df6e90380a00dc.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-9eb5a99df4e76ac3363453ac9ca288b1.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a004ddfcb26e67bd6e678c8ed19e25ce.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a0ca5df4258213d7fc9fce80f65ce760.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a1f916d6039285c4ffb900cd654e418f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a203b91dad570bf05a58c3c3ddb529bf.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a365e82ed54697a52f27adcea1315fe8.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a4c34be6d42152e64b0df90bc4607f64.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-a7accba310e821da5505f71c03b76bdb.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-aa0d470430e6391eca720c7cfa44446f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-aa5c9ca6cf4fba00433b7aa3fa10671a.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-ac9ceb44437becc3e9c4dbfebab7fc2d.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b1220a3c61f85cc0408deedb4c5f57a2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b358f7a51ece39a3247942b1feabdb29.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b57aaedfd8ebdf3931f25119dc6a5eb2.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b5c1596551c256e0e9cf02028595b092.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b6d128682ee29e471486354d486a1b90.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b6fd38ca30869792244804b04bc058da.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b7d203b051eff504ff59ddca7576b6a9.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-b96d9226ce77ec94ceca043d712182e6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-ba3de316d63c7e339987b16f41a0b879.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-bafff7a14c27403dcc6cf1432e8ea836.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-bd77e3c7f9e0b072d96af37f73d1aa32.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-be549ab72f0719d606a5c01e2c0219b6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c16ed9740b85badf16e86ea782a3062f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c1f94158256bb1f3bf665b053d895af9.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c40533fdf4cc57177b12803598af7e59.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c4a687ac4f0c2766eefc9f77ed99cddf.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c69f61a4ab18d0488c8d1fc12e7028e8.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c8b71798409ccc126ee264a00aadcf21.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-c99eda15fc26a2941579560f76c3a5cf.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cb17fc3db95f6d139afc9d31a8e93293.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cbaaefaaf326668277aa24dfa93c4d28.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cd145ce4a0ea18469358df53c207bc1b.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cdbce89e82cc1ab53a2decbf5819278f.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-ce4884f96f11589608b76b726a755803.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cf2cc71752631e579e35b0e423bf2638.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cf6ff4e0f491ca0cf3038187a997b9b4.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-cfb211578629b7e8153b37240de6a9d5.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-d0cf73942fea1c74edbdf0b3011f4656.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-d2666cbed13462c5dc36fa2f15c202ca.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-d3716376641d615e2995605b29bca7b6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-dac48066b5883d8b4551fc584f0c2a3e.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-dbea1af6dcd9860be40c3d18254338f5.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e11567fd2accf9957cd0d3c2be937d87.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e2ead7ea7da0437f085f42ffc05f8d13.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e3fcf5180fd466c8915c4e8069491054.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e480d9c614742d05f0e78f274f1e69e6.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e4bca6cfa53e499cae0a6be4894a90e9.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e51ef413167c6e14e0c0fdcc585f2fc9.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e5f453bb04da18eed01675eeebd88bf8.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-e656f091b9dc4709722c9f4b84d3c797.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-ec181b795ac1fb5a50d700b6e996d745.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-ee8bae97908d5147b423f77ad0d3c1bb.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-f0f13b5c60e0af5553bd359f5513be1b.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-f2b54d4e7be0eaefe1c2c56836fa5368.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-f56414bf9bced67990def8660e306759.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-f5d079153c99a25b9be5b8583c4cc8a7.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-f6032fc06eb20480f096199713f70885.woff2 +0 -0
- package/dist/stage/fonts/Xiaolai/Xiaolai-Regular-f8ee5d36068a42b51d0e4a1116cfcec1.woff2 +0 -0
- package/dist/ui/kid/kid-page.js +2 -0
- package/dist/ui/kid/offline-sw.d.ts +1 -0
- package/dist/ui/kid/offline-sw.js +42 -0
- package/dist/ui/parent/parent-home-page.js +49 -5
- package/dist/ui/parent/parent-settings-page.js +65 -1
- package/install.ps1 +66 -0
- package/install.sh +84 -0
- package/package.json +13 -10
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +1 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 更新说明
|
|
2
|
+
|
|
3
|
+
写给家长:每个版本改了什么。升级跑 `cotutor update`(Windows 上 `cotutor.cmd update`),详见 README 的「数据与升级」。
|
|
4
|
+
|
|
5
|
+
## 0.4.4
|
|
6
|
+
|
|
7
|
+
- 装得快多了:安装从 275 个包变成 15 个,Windows 上第一次装从 3 分钟左右变成 1 分钟左右
|
|
8
|
+
- 平板第一次用:在家长端「扫码打开」点「装证书」,用平板相机扫一下就能装根证书
|
|
9
|
+
- 电脑关了或睡着时,孩子在平板上打开看到「老师们还没到」,不再是浏览器的报错;电脑好了自己回来
|
|
10
|
+
- 有了一行安装命令(Mac、Windows 都有),README 里也写了怎么卸载
|
|
11
|
+
|
|
12
|
+
## 0.4.3
|
|
13
|
+
|
|
14
|
+
- 自动备份:服务开着时每天一份,升级前一份,可以恢复;录音占地方,不在备份里
|
|
15
|
+
- 家长端首页多了「还差几步」,第一次打开就知道接下来做什么;选一下年级老师就知道孩子上几年级
|
|
16
|
+
- 遇到问题时,家长端「设置」能生成诊断信息,不含孩子的对话和 key,先看再发
|
|
17
|
+
- `cotutor init --grade` 自动算好学期,`cotutor doctor --fix` 一次修掉能自动修的
|
|
18
|
+
|
|
19
|
+
## 0.4.2
|
|
20
|
+
|
|
21
|
+
- 百炼欠费、key 失效或连不上时,图标和家长端会说清楚,孩子那边会请爸爸妈妈来看,不再一直转
|
|
22
|
+
- 升级只要一条命令 cotutor update,几个孩子一次升好,孩子在通话时不动
|
|
23
|
+
- 用 Homebrew 装 Node 的电脑:Node 升级后服务和图标不会再起不来
|
|
24
|
+
- 有新版本时能直接看到这次改了什么
|
|
25
|
+
|
|
26
|
+
## 0.4.1
|
|
27
|
+
|
|
28
|
+
- 有新版本时,图标菜单和家长端首页会提醒,孩子端不说,也不会自动升级
|
|
29
|
+
- 第一次装好后台服务时自动打开家长端
|
|
30
|
+
- 升级后图标不再误报“要重启”
|
|
31
|
+
- Windows 上给家长看的命令带 `.cmd`,新电脑的 PowerShell 不会拦
|
|
32
|
+
- `cotutor cert` 发现这台电脑没信任根证书时会提醒
|
|
33
|
+
- 新增 `INSTALL.md`:可以让 AI 助手照着替你安装
|
|
34
|
+
|
|
35
|
+
## 0.4.0
|
|
36
|
+
|
|
37
|
+
- 后台服务 `cotutor service`:登录后自己起来,崩了自己再起,关了终端照样跑
|
|
38
|
+
- 菜单栏(任务栏)图标 `cotutor tray`:不开终端也能看状态、打开家长端、重启
|
|
39
|
+
- 端口只在 `init` 时选一次,以后不变;地址认电脑名,换网络不用重扫
|
package/README.md
CHANGED
|
@@ -12,7 +12,21 @@
|
|
|
12
12
|
请阅读 https://raw.githubusercontent.com/TarsLab/cotutor/main/INSTALL.md ,照着在这台电脑上安装 cotutor
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
**自己装:** 需要一台电脑(Mac 最省事)、Node ≥ 22.18,和一个百炼账号(北京地域,按量付费)。老师的嘴和脑都走百炼,由应用直接调用,不用另装模型客户端:嘴是 `qwen3.8-omni-flash-realtime`,听孩子说、照着讲稿念;脑是 `qwen3.8-max`,写讲稿和板书。
|
|
15
|
+
**自己装:** 需要一台电脑(Mac 最省事)、[Node.js](https://nodejs.org) ≥ 22.18(装 LTS 版),和一个百炼账号(北京地域,按量付费)。老师的嘴和脑都走百炼,由应用直接调用,不用另装模型客户端:嘴是 `qwen3.8-omni-flash-realtime`,听孩子说、照着讲稿念;脑是 `qwen3.8-max`,写讲稿和板书。
|
|
16
|
+
|
|
17
|
+
一行装好(会查 Node 的版本、连不上 npm 就用国内镜像;不用 sudo、不改系统设置)。最后那三个是孩子的短名(小写字母,不用真名)、称呼、年级,不写就只装程序:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# Mac(终端里)
|
|
21
|
+
curl -fsSL https://raw.githubusercontent.com/TarsLab/cotutor/main/install.sh | sh -s -- ming 小明 2
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
# Windows(PowerShell 里)
|
|
26
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/TarsLab/cotutor/main/install.ps1))) ming 小明 2
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
连不上 GitHub 就把地址换成 `https://cdn.jsdelivr.net/npm/cotutor@latest/install.sh`(Windows 是 `install.ps1`)。也可以一条条自己来:
|
|
16
30
|
|
|
17
31
|
```sh
|
|
18
32
|
npm i -g cotutor
|
|
@@ -102,12 +116,23 @@ import { loadWorkspace } from 'cotutor/workspace'; // 解
|
|
|
102
116
|
|
|
103
117
|
百炼欠费、key 失效或这台电脑连不上百炼时,图标出橙点并通知,家长端首页说清楚是哪一种;要家长动手的那几种,孩子端显示「老师们还没到,叫爸爸妈妈来看一下」。处理好以后几分钟内自己恢复,也可以在家长端点「试连」。
|
|
104
118
|
|
|
105
|
-
`cotutor doctor` 检查缺失配置并给修复建议;`--live` 会实际连接服务。页面上的老师起不来时,先看 doctor 和 `conversations/<老师>/<日期>.<job>.err.log`。家长端与工作台需要访问锁时,运行 `cotutor pin` 设置本机 PIN。
|
|
119
|
+
`cotutor doctor` 检查缺失配置并给修复建议;`--fix` 把能自动修的修掉,`--live` 会实际连接服务。页面上的老师起不来时,先看 doctor 和 `conversations/<老师>/<日期>.<job>.err.log`。家长端与工作台需要访问锁时,运行 `cotutor pin` 设置本机 PIN。
|
|
120
|
+
|
|
121
|
+
## 遇到问题
|
|
122
|
+
|
|
123
|
+
家长端「首页」顶部的「还差几步」和橙色提示会说要做什么。还解决不了,到家长端「设置」点「生成诊断信息」(或在电脑上跑 `cotutor report`),看一遍再复制,到 [GitHub Issues](https://github.com/TarsLab/cotutor/issues) 反馈,或者贴给帮你的 AI 助手。里面没有孩子的对话、照片、录音、档案和 key,名字和 IP 已经抹掉。
|
|
106
124
|
|
|
107
125
|
## 数据与升级
|
|
108
126
|
|
|
109
127
|
默认 workspace 是 `~/cotutor/<孩子>/`:配置在 `cotutor.json`,老师在 `agents/<老师>.md`,activity 在 `agents/<老师>/activities/`,技能在 `.agents/skills/`,对话在 `conversations/`,账本在 `ledger/`,照片在 `captures/`,课包在 `bundles/`,首页在 `home/`。长期档案、记忆和日记在配置的 Obsidian vault,规则见 [obsidian 仓库设计](docs/obsidian仓库设计.md)。备份时保留整个 workspace 与 vault。
|
|
110
128
|
|
|
129
|
+
**备份**放在孩子文件夹旁边的 `.backups/<孩子>/`,一份就是一个普通目录。服务开着时每天自动备一份(留 14 份),`cotutor upgrade` 前也备一份(留 5 份),没变的文件不占第二份地方;`cotutor backup` 随时手动备。**录音(wav)不在备份里**,它占了绝大部分地方;想留就自己拷 `conversations/`。硬盘坏了这些也跟着没了,重要的话把 `.backups` 再拷到别处。
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
cotutor backup list # 看有哪些
|
|
133
|
+
cotutor backup restore <名字> --yes # 恢复:先把现在的样子也备一份,再拷回去
|
|
134
|
+
```
|
|
135
|
+
|
|
111
136
|
**有新版本时**,后台服务每天问一次 npm(只发一个普通请求,不带孩子或使用的任何信息),菜单栏(任务栏)图标的菜单和家长端「首页」顶部会说,通知一次,孩子端不说。不会自动升级,因为要重启服务,孩子可能正在上课。不想让它问:在 `~/.config/cotutor/update.json` 写 `{"off": true}`。
|
|
112
137
|
|
|
113
138
|
升级挑孩子不用的时候,运行一条命令就够了(Windows 上写成 `cotutor.cmd update`,因为 PowerShell 缺省禁止运行脚本,提示里给的就是这样):
|
|
@@ -151,6 +176,23 @@ cotutor send math-tutor 你好 --sandbox
|
|
|
151
176
|
cotutor serve --sandbox --fresh # 丢掉试过的,从真 workspace 重新分(真的那边改了老师文件、vault 也用这个带过来)
|
|
152
177
|
```
|
|
153
178
|
|
|
179
|
+
## 卸载
|
|
180
|
+
|
|
181
|
+
孩子的文件夹(`~/cotutor/<孩子>/`)和备份(`~/cotutor/.backups/`)是家里的记录,删了找不回来,卸载程序不会碰它们;真不要了自己删。Windows 上命令写成 `cotutor.cmd`、`npm.cmd`。
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
cotutor service uninstall --workspace ~/cotutor/<孩子> # 每个孩子一次;最后一个卸掉时,菜单栏(任务栏)图标也一起卸
|
|
185
|
+
npm rm -g cotutor
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
还留在电脑上的,不要了可以删:
|
|
189
|
+
|
|
190
|
+
- `~/.config/cotutor/`:百炼的 key、证书、家长端密码、几项设置
|
|
191
|
+
- 日志:Mac 在 `~/Library/Logs/cotutor/`,Windows 在 `%LOCALAPPDATA%\cotutor\`(里面还有图标的文件),Linux 在 `~/.local/state/cotutor/`;Mac 的图标文件在 `~/Library/Application Support/cotutor/`
|
|
192
|
+
- 签证书时装的根证书:跑 `mkcert -uninstall` 从这台电脑的信任里拿掉,再删 `mkcert -CAROOT` 说的那个目录
|
|
193
|
+
- 平板上:设置 › 通用 › VPN 与设备管理里删掉那个描述文件,主屏幕上的图标长按删掉
|
|
194
|
+
- Windows 防火墙里给 Node.js 开的「允许」:Windows 安全中心 › 防火墙和网络保护 › 允许应用通过防火墙,别的程序也用 Node.js 时留着
|
|
195
|
+
|
|
154
196
|
## 开发
|
|
155
197
|
|
|
156
198
|
```sh
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const BACKUP_DIR = ".backups";
|
|
2
|
+
/** 磁盘快满了不备份:备份不能把孩子正在用的电脑撑爆 */
|
|
3
|
+
export declare const MIN_FREE_BYTES: number;
|
|
4
|
+
export type BackupWhy = 'daily' | 'upgrade' | 'manual' | 'restore';
|
|
5
|
+
export interface BackupMeta {
|
|
6
|
+
name: string;
|
|
7
|
+
at: string;
|
|
8
|
+
why: BackupWhy;
|
|
9
|
+
version: string | null;
|
|
10
|
+
files: number;
|
|
11
|
+
/** 这一份看上去多大 */
|
|
12
|
+
bytes: number;
|
|
13
|
+
/** 真正新占的地方(其余和上一份硬链接) */
|
|
14
|
+
newBytes: number;
|
|
15
|
+
/** 没备份的录音有几个 */
|
|
16
|
+
audioSkipped: number;
|
|
17
|
+
vault: 'inside' | 'copied' | 'unreadable' | 'none';
|
|
18
|
+
}
|
|
19
|
+
export interface BackupTarget {
|
|
20
|
+
root: string;
|
|
21
|
+
vault?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const backupsOf: (root: string) => string;
|
|
24
|
+
/** 要不要进备份:rel 是相对 workspace 根的路径,统一成 / */
|
|
25
|
+
export declare function wanted(rel: string): boolean;
|
|
26
|
+
export declare function listBackups(root: string): Promise<BackupMeta[]>;
|
|
27
|
+
export interface BackupOptions {
|
|
28
|
+
why: BackupWhy;
|
|
29
|
+
version?: string | null;
|
|
30
|
+
now?: Date;
|
|
31
|
+
/** 测试:这块盘还剩多少字节 */
|
|
32
|
+
freeBytes?: () => Promise<number>;
|
|
33
|
+
}
|
|
34
|
+
/** 备份一份。写到临时目录再改名,中途断了不会留下半份 */
|
|
35
|
+
export declare function makeBackup(t: BackupTarget, o: BackupOptions): Promise<BackupMeta>;
|
|
36
|
+
/** 每种留最近几份;刚做的那份不删。硬链接删了不影响别的份 */
|
|
37
|
+
export declare function pruneBackups(root: string, keepName?: string): Promise<string[]>;
|
|
38
|
+
/** 该不该做今天这份 */
|
|
39
|
+
export declare function dailyDue(list: readonly BackupMeta[], now: number): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* 恢复:先把现在的样子也备份一份(恢复错了还能回来),再把那一份拷回去。只覆盖备份里有的文件,之后新多出来的不删。
|
|
42
|
+
* 服务要先停(main.ts 管)。外面的 vault 不动,回 vaultCopy 告诉家长在哪。
|
|
43
|
+
*/
|
|
44
|
+
export declare function restoreBackup(t: BackupTarget, name: string, o?: {
|
|
45
|
+
version?: string | null;
|
|
46
|
+
now?: Date;
|
|
47
|
+
freeBytes?: () => Promise<number>;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
files: number;
|
|
50
|
+
safety: string;
|
|
51
|
+
vaultCopy: string | null;
|
|
52
|
+
}>;
|
|
53
|
+
/** 一份给人看的样子 */
|
|
54
|
+
export declare function describeBackup(b: BackupMeta): string;
|
|
55
|
+
/**
|
|
56
|
+
* serve 起来时调:隔一阵看一眼,该备份了就备一份。孩子在通话时往后推(拷文件占磁盘,不和通话抢)。
|
|
57
|
+
* 备份失败不影响服务,只进日志,doctor 看「最近一份是多久以前」就知道了。定时器不拦进程退出。
|
|
58
|
+
*/
|
|
59
|
+
export declare function startDailyBackups(o: {
|
|
60
|
+
target: () => BackupTarget | null;
|
|
61
|
+
busy: () => boolean;
|
|
62
|
+
version: string | null;
|
|
63
|
+
now?: () => number;
|
|
64
|
+
firstMs?: number;
|
|
65
|
+
everyMs?: number;
|
|
66
|
+
note?: (text: string) => void;
|
|
67
|
+
freeBytes?: () => Promise<number>;
|
|
68
|
+
}): () => void;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 备份:孩子的记录删了找不回来。升级前留一份,每天留一份,放在 workspace 旁边的 .backups/<目录名>/<名字>/,一份就是一个普通目录,打开就能看。
|
|
3
|
+
* 防的是升级弄坏、手滑改坏、误删文件夹;硬盘坏了防不住,要防就把 .backups 再拷到别处。
|
|
4
|
+
*
|
|
5
|
+
* 不备份录音(wav):2026-11-07 量真的 workspace,1.28 GB 里 1.21 GB 是录音,其余约 70 MB;这台电脑磁盘还只剩 4%。
|
|
6
|
+
* 也不备份能重新生成的(视频课 mp4、评测 evals、配音缓存)。保住的是文字记录、账本、照片、老师文件、配置。
|
|
7
|
+
*
|
|
8
|
+
* 没变的文件和上一份硬链接,不占第二份地方:每天一份也几乎不涨。一份里的文件不再改,所以硬链接是安全的;恢复时一律拷出去,不链回 workspace。
|
|
9
|
+
* vault 在 workspace 外面(Obsidian)的,只拷 md 到 _vault/,恢复时不动它:那是家长的,要不要拿回来家长定。macOS 上后台进程读不了「文稿」时记一笔,不算失败。
|
|
10
|
+
*/
|
|
11
|
+
import { copyFile, link, lstat, mkdir, readFile, readdir, rename, rm, stat, statfs, utimes, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import { UsageError } from "../workspace.js";
|
|
14
|
+
export const BACKUP_DIR = '.backups';
|
|
15
|
+
const META = 'backup.json';
|
|
16
|
+
const VAULT_DIR = '_vault';
|
|
17
|
+
const TOP_SKIP = new Set(['.git', '.claude', '.sandbox', 'evals', 'node_modules']);
|
|
18
|
+
/** .cotutor 里能重新生成的缓存;别的(设备表、孩子端流水、出厂 hash)留着 */
|
|
19
|
+
const COTUTOR_SKIP = new Set(['qwen', 'live-preview', 'qr', 'board']);
|
|
20
|
+
const SKIP_EXT = /\.(wav|mp4|tmp)$/i;
|
|
21
|
+
const AUDIO = /\.wav$/i;
|
|
22
|
+
const VAULT_MAX_FILES = 5000;
|
|
23
|
+
const KEEP = { daily: 14, upgrade: 5, manual: 5, restore: 5 };
|
|
24
|
+
/** 磁盘快满了不备份:备份不能把孩子正在用的电脑撑爆 */
|
|
25
|
+
export const MIN_FREE_BYTES = 1024 ** 3;
|
|
26
|
+
const DAILY_EVERY_MS = 20 * 3600_000;
|
|
27
|
+
export const backupsOf = (root) => join(dirname(resolve(root)), BACKUP_DIR, basename(resolve(root)));
|
|
28
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
29
|
+
const stamp = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
30
|
+
/** 要不要进备份:rel 是相对 workspace 根的路径,统一成 / */
|
|
31
|
+
export function wanted(rel) {
|
|
32
|
+
const parts = rel.split('/');
|
|
33
|
+
if (TOP_SKIP.has(parts[0]))
|
|
34
|
+
return false;
|
|
35
|
+
if (parts[0] === '.cotutor' && parts.length > 1 && COTUTOR_SKIP.has(parts[1]))
|
|
36
|
+
return false;
|
|
37
|
+
return !SKIP_EXT.test(rel);
|
|
38
|
+
}
|
|
39
|
+
async function* walk(dir, base) {
|
|
40
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
41
|
+
const path = join(dir, e.name);
|
|
42
|
+
const rel = relative(base, path).split(sep).join('/');
|
|
43
|
+
// 只认普通文件和目录:链接(.claude/skills)不跟进去,不然会拷到 workspace 外面
|
|
44
|
+
if (e.isDirectory()) {
|
|
45
|
+
if (rel.split('/')[0] === BACKUP_DIR)
|
|
46
|
+
continue;
|
|
47
|
+
if (TOP_SKIP.has(rel) || (rel.startsWith('.cotutor/') && COTUTOR_SKIP.has(rel.slice('.cotutor/'.length))))
|
|
48
|
+
continue;
|
|
49
|
+
yield* walk(path, base);
|
|
50
|
+
}
|
|
51
|
+
else if (e.isFile()) {
|
|
52
|
+
const st = await lstat(path).catch(() => null);
|
|
53
|
+
if (st)
|
|
54
|
+
yield { path, rel, st };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 放一个文件:和上一份一样就硬链接,不一样(或链不了:跨盘、文件系统不支持)就拷。回新占了多少字节 */
|
|
59
|
+
async function place(src, st, dest, prev) {
|
|
60
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
61
|
+
if (prev) {
|
|
62
|
+
const p = await stat(prev).catch(() => null);
|
|
63
|
+
if (p && p.size === st.size && Math.abs(p.mtimeMs - st.mtimeMs) < 2) {
|
|
64
|
+
try {
|
|
65
|
+
await link(prev, dest);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
catch { /* 链不了就拷 */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await copyFile(src, dest);
|
|
72
|
+
await utimes(dest, st.atime, st.mtime).catch(() => undefined);
|
|
73
|
+
return st.size;
|
|
74
|
+
}
|
|
75
|
+
export async function listBackups(root) {
|
|
76
|
+
const dir = backupsOf(root);
|
|
77
|
+
const names = await readdir(dir).catch(() => []);
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const name of names) {
|
|
80
|
+
if (name.startsWith('.'))
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
out.push({ ...JSON.parse(await readFile(join(dir, name, META), 'utf8')), name });
|
|
84
|
+
}
|
|
85
|
+
catch { /* 不是一份完整的备份 */ }
|
|
86
|
+
}
|
|
87
|
+
return out.sort((a, b) => b.at.localeCompare(a.at) || b.name.localeCompare(a.name));
|
|
88
|
+
}
|
|
89
|
+
const freeOf = async (dir) => {
|
|
90
|
+
const s = await statfs(dir);
|
|
91
|
+
return Number(s.bavail) * Number(s.bsize);
|
|
92
|
+
};
|
|
93
|
+
/** 备份一份。写到临时目录再改名,中途断了不会留下半份 */
|
|
94
|
+
export async function makeBackup(t, o) {
|
|
95
|
+
const root = resolve(t.root);
|
|
96
|
+
const dir = backupsOf(root);
|
|
97
|
+
await mkdir(dir, { recursive: true });
|
|
98
|
+
const free = await (o.freeBytes ?? (() => freeOf(dir)))().catch(() => Number.POSITIVE_INFINITY);
|
|
99
|
+
if (free < MIN_FREE_BYTES)
|
|
100
|
+
throw new UsageError(`磁盘只剩 ${(free / 1024 ** 3).toFixed(1)} GB,这次没备份:先腾点地方,备份不能把这台电脑撑爆`);
|
|
101
|
+
const now = o.now ?? new Date();
|
|
102
|
+
const list = await listBackups(root);
|
|
103
|
+
const prevDir = list[0] ? join(dir, list[0].name) : null;
|
|
104
|
+
let name = `${stamp(now)}-${o.why}`;
|
|
105
|
+
for (let i = 2; list.some((b) => b.name === name); i++)
|
|
106
|
+
name = `${stamp(now)}-${o.why}-${i}`;
|
|
107
|
+
const tmp = join(dir, `.tmp-${process.pid}-${now.getTime()}`);
|
|
108
|
+
await rm(tmp, { recursive: true, force: true });
|
|
109
|
+
await mkdir(tmp, { recursive: true });
|
|
110
|
+
const meta = { name, at: now.toISOString(), why: o.why, version: o.version ?? null, files: 0, bytes: 0, newBytes: 0, audioSkipped: 0, vault: 'none' };
|
|
111
|
+
try {
|
|
112
|
+
for await (const f of walk(root, root)) {
|
|
113
|
+
if (!wanted(f.rel)) {
|
|
114
|
+
if (AUDIO.test(f.rel))
|
|
115
|
+
meta.audioSkipped++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
meta.newBytes += await place(f.path, f.st, join(tmp, ...f.rel.split('/')), prevDir && join(prevDir, ...f.rel.split('/')));
|
|
119
|
+
meta.files++;
|
|
120
|
+
meta.bytes += f.st.size;
|
|
121
|
+
}
|
|
122
|
+
const vault = t.vault ? resolve(t.vault) : null;
|
|
123
|
+
if (vault && (vault === root || vault.startsWith(root + sep)))
|
|
124
|
+
meta.vault = 'inside';
|
|
125
|
+
else if (vault) {
|
|
126
|
+
try {
|
|
127
|
+
let n = 0;
|
|
128
|
+
for await (const f of walk(vault, vault)) {
|
|
129
|
+
if (!f.rel.endsWith('.md') || f.rel.split('/').some((p) => p.startsWith('.')))
|
|
130
|
+
continue;
|
|
131
|
+
if (++n > VAULT_MAX_FILES)
|
|
132
|
+
break;
|
|
133
|
+
meta.newBytes += await place(f.path, f.st, join(tmp, VAULT_DIR, ...f.rel.split('/')), prevDir && join(prevDir, VAULT_DIR, ...f.rel.split('/')));
|
|
134
|
+
meta.files++;
|
|
135
|
+
meta.bytes += f.st.size;
|
|
136
|
+
}
|
|
137
|
+
meta.vault = 'copied';
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const code = err.code;
|
|
141
|
+
// macOS 不让后台进程读「文稿」、iCloud 云盘;vault 不在了:都不拦这一份
|
|
142
|
+
if (code === 'EPERM' || code === 'EACCES' || code === 'ENOENT')
|
|
143
|
+
meta.vault = 'unreadable';
|
|
144
|
+
else
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
await writeFile(join(tmp, META), `${JSON.stringify(meta, null, 2)}\n`);
|
|
149
|
+
await rename(tmp, join(dir, name));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
await rm(tmp, { recursive: true, force: true }).catch(() => undefined);
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
await pruneBackups(root, name);
|
|
156
|
+
return meta;
|
|
157
|
+
}
|
|
158
|
+
/** 每种留最近几份;刚做的那份不删。硬链接删了不影响别的份 */
|
|
159
|
+
export async function pruneBackups(root, keepName) {
|
|
160
|
+
const dir = backupsOf(root);
|
|
161
|
+
const list = await listBackups(root);
|
|
162
|
+
const gone = [];
|
|
163
|
+
const seen = {};
|
|
164
|
+
for (const b of list) {
|
|
165
|
+
seen[b.why] = (seen[b.why] ?? 0) + 1;
|
|
166
|
+
if (seen[b.why] > (KEEP[b.why] ?? 5) && b.name !== keepName) {
|
|
167
|
+
await rm(join(dir, b.name), { recursive: true, force: true });
|
|
168
|
+
gone.push(b.name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 上次断在半路留下的临时目录
|
|
172
|
+
for (const n of await readdir(dir).catch(() => [])) {
|
|
173
|
+
if (!n.startsWith('.tmp-'))
|
|
174
|
+
continue;
|
|
175
|
+
const st = await stat(join(dir, n)).catch(() => null);
|
|
176
|
+
if (st && Date.now() - st.mtimeMs > 24 * 3600_000)
|
|
177
|
+
await rm(join(dir, n), { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
return gone;
|
|
180
|
+
}
|
|
181
|
+
/** 该不该做今天这份 */
|
|
182
|
+
export function dailyDue(list, now) {
|
|
183
|
+
const last = list.find((b) => b.why === 'daily');
|
|
184
|
+
return !last || now - Date.parse(last.at) >= DAILY_EVERY_MS;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 恢复:先把现在的样子也备份一份(恢复错了还能回来),再把那一份拷回去。只覆盖备份里有的文件,之后新多出来的不删。
|
|
188
|
+
* 服务要先停(main.ts 管)。外面的 vault 不动,回 vaultCopy 告诉家长在哪。
|
|
189
|
+
*/
|
|
190
|
+
export async function restoreBackup(t, name, o = {}) {
|
|
191
|
+
const root = resolve(t.root);
|
|
192
|
+
const target = (await listBackups(root)).find((b) => b.name === name);
|
|
193
|
+
if (!target)
|
|
194
|
+
throw new UsageError(`没有这份备份:${name}(cotutor backup list 看有哪些)`);
|
|
195
|
+
const from = join(backupsOf(root), name);
|
|
196
|
+
const safety = await makeBackup(t, { why: 'restore', version: o.version, now: o.now, freeBytes: o.freeBytes });
|
|
197
|
+
let files = 0;
|
|
198
|
+
for await (const f of walk(from, from)) {
|
|
199
|
+
if (f.rel === META || f.rel.split('/')[0] === VAULT_DIR)
|
|
200
|
+
continue;
|
|
201
|
+
const dest = join(root, ...f.rel.split('/'));
|
|
202
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
203
|
+
await copyFile(f.path, dest);
|
|
204
|
+
await utimes(dest, f.st.atime, f.st.mtime).catch(() => undefined);
|
|
205
|
+
files++;
|
|
206
|
+
}
|
|
207
|
+
return { files, safety: safety.name, vaultCopy: target.vault === 'copied' ? join(from, VAULT_DIR) : null };
|
|
208
|
+
}
|
|
209
|
+
/** 一份给人看的样子 */
|
|
210
|
+
export function describeBackup(b) {
|
|
211
|
+
const mb = (n) => (n < 1024 * 1024 ? `${Math.max(1, Math.round(n / 1024))} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`);
|
|
212
|
+
const why = { daily: '每天', upgrade: '升级前', manual: '手动', restore: '恢复前' }[b.why] ?? b.why;
|
|
213
|
+
return `${b.name} ${why} · ${b.files} 个文件 · ${mb(b.bytes)}(新占 ${mb(b.newBytes)})${b.version ? ` · ${b.version}` : ''}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* serve 起来时调:隔一阵看一眼,该备份了就备一份。孩子在通话时往后推(拷文件占磁盘,不和通话抢)。
|
|
217
|
+
* 备份失败不影响服务,只进日志,doctor 看「最近一份是多久以前」就知道了。定时器不拦进程退出。
|
|
218
|
+
*/
|
|
219
|
+
export function startDailyBackups(o) {
|
|
220
|
+
let running = false;
|
|
221
|
+
const now = o.now ?? Date.now;
|
|
222
|
+
const check = async () => {
|
|
223
|
+
const t = o.target();
|
|
224
|
+
if (running || !t || o.busy())
|
|
225
|
+
return;
|
|
226
|
+
running = true;
|
|
227
|
+
try {
|
|
228
|
+
if (dailyDue(await listBackups(t.root), now()))
|
|
229
|
+
await makeBackup(t, { why: 'daily', version: o.version, now: new Date(now()), freeBytes: o.freeBytes });
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
(o.note ?? ((s) => console.error(s)))(`备份没成:${err instanceof Error ? err.message : String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
running = false;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const first = setTimeout(() => { void check(); }, o.firstMs ?? 5 * 60_000);
|
|
239
|
+
first.unref();
|
|
240
|
+
const every = setInterval(() => { void check(); }, o.everyMs ?? 3600_000);
|
|
241
|
+
every.unref();
|
|
242
|
+
return () => { clearTimeout(first); clearInterval(every); };
|
|
243
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DoctorReport } from './doctor.ts';
|
|
2
|
+
/** 按空格拆,双引号里的算一段;$HOME 换成真的家目录 */
|
|
3
|
+
export declare function parseCmd(cmd: string, home?: string): string[];
|
|
4
|
+
export interface FixDeps {
|
|
5
|
+
doctor(): Promise<DoctorReport>;
|
|
6
|
+
run(argv: string[]): Promise<{
|
|
7
|
+
code: number;
|
|
8
|
+
out: string;
|
|
9
|
+
}>;
|
|
10
|
+
print(line: string): void;
|
|
11
|
+
}
|
|
12
|
+
/** 修到没有可修的;同一条命令只跑一次,修不好的不反复试。回最后那次体检 */
|
|
13
|
+
export declare function runFixes(d: FixDeps): Promise<DoctorReport>;
|
|
14
|
+
/** 真的跑:cotutor 用现在这个 node 和 bin,不靠 PATH */
|
|
15
|
+
export declare function runArgv(argv: string[]): Promise<{
|
|
16
|
+
code: number;
|
|
17
|
+
out: string;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cotutor doctor --fix:把体检里能自动修的(带 cmd 的那些:幂等、不碰孩子的数据、不用人输东西)一条条跑掉,修完再查,直到没有可修的;要人来的留给人。
|
|
3
|
+
* cmd 是给人和 agent 看的字符串(路径写成 $HOME,用户名不传出去),这里拆成参数直接起进程,不经 shell:Windows 的 cmd.exe 不认 $HOME,脚本策略也拦不到这里。
|
|
4
|
+
* 只跑 cotutor 和 git:cmd 是我们自己写的,但万一哪天有一条写错了,不该变成任意命令。
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { binPath } from "./service.js";
|
|
9
|
+
/** 按空格拆,双引号里的算一段;$HOME 换成真的家目录 */
|
|
10
|
+
export function parseCmd(cmd, home = homedir()) {
|
|
11
|
+
const out = [];
|
|
12
|
+
let cur = '';
|
|
13
|
+
let quoted = false;
|
|
14
|
+
let has = false;
|
|
15
|
+
for (const ch of cmd) {
|
|
16
|
+
if (ch === '"') {
|
|
17
|
+
quoted = !quoted;
|
|
18
|
+
has = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (ch === ' ' && !quoted) {
|
|
22
|
+
if (has)
|
|
23
|
+
out.push(cur);
|
|
24
|
+
cur = '';
|
|
25
|
+
has = false;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
cur += ch;
|
|
29
|
+
has = true;
|
|
30
|
+
}
|
|
31
|
+
if (has)
|
|
32
|
+
out.push(cur);
|
|
33
|
+
return out.map((a) => a.replaceAll('$HOME', home));
|
|
34
|
+
}
|
|
35
|
+
const ROUNDS = 12;
|
|
36
|
+
/** 修到没有可修的;同一条命令只跑一次,修不好的不反复试。回最后那次体检 */
|
|
37
|
+
export async function runFixes(d) {
|
|
38
|
+
const tried = new Set();
|
|
39
|
+
let report = await d.doctor();
|
|
40
|
+
for (let i = 0; i < ROUNDS; i++) {
|
|
41
|
+
const next = report.checks.find((c) => !c.ok && c.cmd && !tried.has(c.cmd));
|
|
42
|
+
if (!next?.cmd)
|
|
43
|
+
break;
|
|
44
|
+
tried.add(next.cmd);
|
|
45
|
+
const argv = parseCmd(next.cmd);
|
|
46
|
+
if (argv[0] !== 'cotutor' && argv[0] !== 'git') {
|
|
47
|
+
d.print(`! ${next.name}:不认识这条命令,没跑:${next.cmd}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const r = await d.run(argv);
|
|
51
|
+
d.print(`${r.code === 0 ? '✓' : '✗'} ${next.name}:${next.cmd}`);
|
|
52
|
+
if (r.code !== 0)
|
|
53
|
+
for (const l of r.out.trim().split('\n').slice(-4))
|
|
54
|
+
d.print(` ${l}`);
|
|
55
|
+
report = await d.doctor();
|
|
56
|
+
}
|
|
57
|
+
return report;
|
|
58
|
+
}
|
|
59
|
+
/** 真的跑:cotutor 用现在这个 node 和 bin,不靠 PATH */
|
|
60
|
+
export function runArgv(argv) {
|
|
61
|
+
const [cmd, args] = argv[0] === 'cotutor' ? [process.execPath, [binPath(), ...argv.slice(1)]] : [argv[0], argv.slice(1)];
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
64
|
+
let out = '';
|
|
65
|
+
child.stdout?.on('data', (c) => { out += c.toString('utf8'); });
|
|
66
|
+
child.stderr?.on('data', (c) => { out += c.toString('utf8'); });
|
|
67
|
+
child.on('error', (err) => resolve({ code: 1, out: `${out}${err.message}\n` }));
|
|
68
|
+
child.on('close', (code) => resolve({ code: code ?? 1, out }));
|
|
69
|
+
});
|
|
70
|
+
}
|
package/dist/app/cli/doctor.js
CHANGED
|
@@ -569,6 +569,14 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
569
569
|
if (sec !== null)
|
|
570
570
|
push({ name: 'windows.sleep', ok: sec === 0, required: false, detail: sec === 0 ? '接着电源时不闲置睡眠' : `接着电源 ${Math.round(sec / 60)} 分钟没人碰就睡,睡着时平板连不上(有通话时 cotutor 拦着它睡,别的时候拦不了)`, fix: sec === 0 ? undefined : '设置 › 系统 › 电源和电池 › 屏幕、睡眠和休眠超时,「插入电源时,使设备在以下时间后进入睡眠状态」选「从不」;合盖看「电源按钮和盖子」那一项' });
|
|
571
571
|
}
|
|
572
|
+
// ---- 备份:服务开着时每天一份;好几天没有说明服务没在跑、或者备份一直失败(磁盘满、读不了) ----
|
|
573
|
+
if (probeEnv && ws && !ws.sandbox) {
|
|
574
|
+
const { backupsOf, listBackups } = await import("./backup.js");
|
|
575
|
+
const list = await listBackups(root).catch(() => []);
|
|
576
|
+
const last = list[0];
|
|
577
|
+
const ageMs = last ? (opts.now ?? new Date()).getTime() - Date.parse(last.at) : Number.POSITIVE_INFINITY;
|
|
578
|
+
push({ name: 'backup', ok: ageMs < 3 * 24 * 3600_000, required: false, detail: last ? `最近一份 ${last.at.slice(0, 10)},共 ${list.length} 份(${redactHome(backupsOf(root))})` : '还没有备份', fix: 'cotutor backup 备一份;服务开着时每天自己备', cmd: `cotutor backup${flag}` });
|
|
579
|
+
}
|
|
572
580
|
// ---- 有没有新版本:只看服务问来存下的那份,体检不上网;什么时候升级家长定(要重起服务,孩子可能在上课),所以没有 cmd ----
|
|
573
581
|
{
|
|
574
582
|
const { readUpdateRecord } = await import("../machine/update.js");
|
package/dist/app/cli/init.d.ts
CHANGED
package/dist/app/cli/init.js
CHANGED
|
@@ -9,6 +9,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
9
9
|
import { DIRS, GITIGNORE, LEDGER_FILES, REFERENCE_README, configTemplate, profileTemplate, shippedAgents, writeSchemaFile } from "./skeleton.js";
|
|
10
10
|
import { installTutors, readManifest, writeManifest } from "./tutors.js";
|
|
11
11
|
import { DEFAULT_GRADE, LEGACY_GRADE, requireGradePack } from "./grades.js";
|
|
12
|
+
import { schoolStartFor } from "../../memory/vault-notes.js";
|
|
12
13
|
import { installActivities } from "./activities.js";
|
|
13
14
|
import { installSkills, linkClaudeSkills } from "./skills.js";
|
|
14
15
|
import { migrateLayout } from "./migrate.js";
|
|
@@ -102,8 +103,10 @@ export async function initWorkspace(opts) {
|
|
|
102
103
|
steps.push({ item: redactHome(profile), action: 'kept', note: '没有 cotutor: profile 属性,老师读不到;在 frontmatter 加上(cotutor doctor 有写法)' });
|
|
103
104
|
else {
|
|
104
105
|
await mkdir(dirname(profile), { recursive: true });
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// 给了年级就算好读一年级的年月,老师第一天就知道是几年级哪个学期;没给就不猜(教学包的缺省年级不等于孩子的年级)
|
|
107
|
+
const start = opts.grade === undefined ? undefined : schoolStartFor(opts.grade, opts.now ?? new Date());
|
|
108
|
+
await writeFile(profile, profileTemplate(ws.config.kid.name ?? ws.config.kid.slug, start));
|
|
109
|
+
steps.push({ item: redactHome(profile), action: 'created', note: start ? `档案:按 ${opts.grade} 年级算,读一年级是 ${start};孩子的情况写在正文,每个话题开头整篇进上下文包` : '档案:填 school_start 与孩子的情况,每个话题开头整篇进上下文包' });
|
|
107
110
|
}
|
|
108
111
|
const readme = join(ws.paths.reference, 'README.md');
|
|
109
112
|
if (await exists(readme))
|
|
@@ -149,7 +152,7 @@ export async function initWorkspace(opts) {
|
|
|
149
152
|
const suggestions = [];
|
|
150
153
|
if (!(await exists(join(root, '.git'))))
|
|
151
154
|
suggestions.push('git init(建议:老师记忆、账本、政策都是不可再生状态,git 是破坏后的兜底)');
|
|
152
|
-
suggestions.push('档案里填 school_start(读一年级的年月,如 2025-09)
|
|
155
|
+
suggestions.push(`${opts.grade === undefined ? '档案里填 school_start(读一年级的年月,如 2025-09),老师才知道现在是几年级哪个学期;' : ''}想用自己的 Obsidian 库,改 cotutor.json 的 paths.vault`);
|
|
153
156
|
suggestions.push('cotutor service install 装成后台服务(登录后自己起来、关了终端照样跑;在终端里调试用 cotutor serve),第一次装会打开家长端(以后 cotutor open),在「首页」填百炼的按量 API Key 与业务空间 ID(老师的嘴和脑都用它;没填孩子端只出「老师们还没到」)');
|
|
154
157
|
suggestions.push('在平板、手机上用:cotutor cert 签证书(要 mkcert),根证书装到设备上并打开信任,iPad / iPhone 的步骤见 README 里的《iPad 与 iPhone》');
|
|
155
158
|
suggestions.push('可选:点读、单词卡的配音用百炼(和老师的嘴同一把 key),到家长端首页点老师的「样子与声音」挑声音;cotutor pin 给家长端与工作台加锁');
|