1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-16 20:24:47 +02:00

Compare commits

..

1138 Commits

Author SHA1 Message Date
syuilo
0fa97263b6 Update babylonRuntime.ts 2026-06-24 13:32:31 +09:00
syuilo
1903607659 thumbs 2026-06-17 16:55:13 +09:00
syuilo
7d750668c0 thumbs 2026-06-17 16:51:17 +09:00
syuilo
db6708bfbd Merge branch 'develop' into room 2026-06-17 16:42:41 +09:00
syuilo
bb06278e97 wip 2026-06-15 20:13:43 +09:00
syuilo
3fd8becdc5 typo 2026-06-15 18:51:26 +09:00
syuilo
de94c15c8f typo 2026-06-15 18:50:05 +09:00
syuilo
68719db32f typo 2026-06-15 18:49:26 +09:00
syuilo
1e4539a168 typo 2026-06-15 18:48:21 +09:00
syuilo
ef6c5dfc95 typo 2026-06-15 18:47:33 +09:00
syuilo
4fc9e38e0d typo 2026-06-15 18:46:54 +09:00
syuilo
60f60b6bac Merge branch 'develop' into room 2026-06-15 18:42:48 +09:00
syuilo
a7a7b8bc69 Update babylonRuntime.ts 2026-06-15 17:42:36 +09:00
syuilo
253dd052f8 refactoe 2026-06-15 16:22:57 +09:00
syuilo
c1eb67a4ce typo 2026-06-15 16:18:20 +09:00
syuilo
a26d78d573 stickyPlaneId 2026-06-15 16:16:33 +09:00
syuilo
9e33507f01 wip 2026-06-14 20:10:00 +09:00
syuilo
bde03b8259 wip 2026-06-14 17:21:05 +09:00
syuilo
5690d28d4e Update engine.ts 2026-06-14 17:04:17 +09:00
syuilo
64a4283c35 Update engine.ts 2026-06-14 15:31:05 +09:00
syuilo
9de48030fc Update engine.ts 2026-06-14 15:28:38 +09:00
syuilo
1dbf9f9f85 wip 2026-06-14 13:35:21 +09:00
syuilo
37c3145859 shadow 2026-06-13 20:34:04 +09:00
syuilo
600dae3d68 wip 2026-06-13 15:59:23 +09:00
syuilo
ab9d6947ae 🎨 2026-06-13 12:31:35 +09:00
syuilo
56da1e5084 🎨 2026-06-13 11:35:24 +09:00
syuilo
63d314750f Update room.core.vue 2026-06-12 18:43:02 +09:00
syuilo
1e10b67476 herbarium 2026-06-12 17:42:11 +09:00
syuilo
cf2139d515 wip 2026-06-12 13:39:41 +09:00
syuilo
3d2941bd8f wip 2026-06-12 12:30:39 +09:00
syuilo
c06eef6b58 username label 2026-06-11 21:27:32 +09:00
syuilo
fa7a8b1282 up 2026-06-11 20:13:42 +09:00
syuilo
319826fe76 Merge branch 'develop' into room 2026-06-11 20:12:32 +09:00
syuilo
54cfa4a13e Update README.md 2026-06-11 18:35:54 +09:00
syuilo
e507d41640 Update lavaLamp.ts 2026-06-11 18:24:58 +09:00
syuilo
d9cac0fa56 🎨 2026-06-11 14:47:22 +09:00
syuilo
2e51cde16e 🎨 2026-06-11 11:58:43 +09:00
syuilo
98ac383b44 🎨 2026-06-11 11:25:32 +09:00
syuilo
e1290584f2 kakejiku 2026-06-10 20:51:41 +09:00
syuilo
923d95d078 locale 2026-06-10 18:59:13 +09:00
syuilo
47a5f0cdd9 image rotation 2026-06-10 18:50:20 +09:00
syuilo
961d611cd4 bolt 2026-06-10 15:50:47 +09:00
syuilo
2bc10ccc34 lavaLamp 2026-06-10 13:58:20 +09:00
syuilo
38dae938c0 Update haniwa.ts 2026-06-09 20:15:02 +09:00
syuilo
1f2b635920 clean up 2026-06-09 19:51:03 +09:00
syuilo
39c6baf6f2 room light 2026-06-09 18:50:46 +09:00
syuilo
79541bcbe3 downlight 2026-06-09 15:29:26 +09:00
syuilo
ba66b5d609 wip 2026-06-09 14:09:10 +09:00
syuilo
a2c96c29c7 wip 2026-06-09 12:06:50 +09:00
syuilo
60db0a6bbd wip 2026-06-09 11:40:35 +09:00
syuilo
07f17820b3 Update simple.ts 2026-06-08 21:50:06 +09:00
syuilo
66cd678cd3 wip 2026-06-08 21:35:29 +09:00
syuilo
1f604db203 wip 2026-06-08 15:45:27 +09:00
syuilo
06def2995b throttle 2026-06-08 11:45:59 +09:00
syuilo
c9107e5d2b Update MkWorldMonoOptionsForm.vue 2026-06-08 11:41:16 +09:00
syuilo
cbac208262 Update README.md 2026-06-08 10:37:44 +09:00
syuilo
b7c7323b97 Update README.md 2026-06-08 10:28:59 +09:00
syuilo
602d034e94 cardboard-box 2026-06-08 10:24:23 +09:00
syuilo
22f78ab175 haniwa 2026-06-07 20:46:01 +09:00
syuilo
d59afb5a29 ceilingFan 2026-06-07 18:31:01 +09:00
syuilo
cc8e6a2e92 update ceiling-fan-light 2026-06-07 16:59:09 +09:00
syuilo
004ef368c2 update ceiling-fan-light 2026-06-07 16:47:04 +09:00
syuilo
ac06c1700a Update babylonRuntime.ts
add RegisterAnimation and RegisterAnimatable
2026-06-07 11:24:43 +09:00
syuilo
2d0fb674d5 wip 2026-06-06 17:29:08 +09:00
syuilo
61b8b0aac3 .js 2026-06-06 17:17:15 +09:00
syuilo
bd6e443cec Update haniwa.ts 2026-06-06 17:00:20 +09:00
syuilo
897199a807 .js 2026-06-06 16:45:49 +09:00
syuilo
1f64579516 wip 2026-06-06 16:34:16 +09:00
syuilo
84203630a1 wip 2026-06-05 20:48:20 +09:00
syuilo
561e0c3813 Update room.add-furniture-dialog.vue 2026-06-04 22:50:17 +09:00
syuilo
169d655717 Update room.add-furniture-dialog.vue 2026-06-04 22:27:30 +09:00
syuilo
ade652dd3e wallClock 2026-06-04 20:22:09 +09:00
syuilo
138353c84d up 2026-06-04 16:53:02 +09:00
syuilo
4b30bfeb3c wip 2026-06-04 16:22:31 +09:00
syuilo
aae2039df2 Update default.blend 2026-06-04 02:28:39 +09:00
syuilo
850e156687 🎨 2026-06-04 02:28:30 +09:00
syuilo
844196cfea wip 2026-06-03 22:07:04 +09:00
syuilo
1925840f3f Merge branch 'develop' into room 2026-06-03 21:54:33 +09:00
syuilo
d5df006463 Update babylonRuntime.ts 2026-06-03 21:46:22 +09:00
syuilo
2593652510 Update babylonRuntime.ts 2026-06-03 21:38:52 +09:00
syuilo
2e22b34c40 Create haniwa.blend 2026-06-03 21:01:51 +09:00
syuilo
8d3c3284b5 wip 2026-06-03 19:15:08 +09:00
syuilo
7d7a125299 wip 2026-06-03 18:46:18 +09:00
syuilo
aac51a171c wip 2026-06-03 17:12:08 +09:00
syuilo
908e0022e9 wip 2026-06-03 15:14:25 +09:00
syuilo
b56a2a8bf7 wip 2026-06-03 14:50:32 +09:00
syuilo
61de0fb279 wip 2026-06-03 14:17:23 +09:00
syuilo
b43ff3d820 wip 2026-06-03 14:06:41 +09:00
syuilo
23b2f4c7a0 Update env.ts 2026-06-03 13:53:56 +09:00
syuilo
827895b548 wip 2026-06-03 13:00:53 +09:00
syuilo
8b0dd1ea68 wip 2026-06-03 12:03:54 +09:00
syuilo
ec3517ba58 wip 2026-06-03 09:55:12 +09:00
syuilo
c24515044e Update babylonRuntime.ts 2026-06-03 08:48:33 +09:00
syuilo
f55adc19d3 registerBabylonRuntime 2026-06-03 08:42:07 +09:00
syuilo
32af098cd9 pure 2026-06-02 21:41:11 +09:00
syuilo
cfea88dce3 Update defineFurnitureUi.ts 2026-06-02 20:31:27 +09:00
syuilo
3a97e26f80 Update engine.ts 2026-06-02 18:50:58 +09:00
syuilo
7bcb1b1732 wip 2026-06-02 18:00:39 +09:00
syuilo
8684cf14cc wip 2026-06-02 17:43:20 +09:00
syuilo
686a847697 wip 2026-06-02 17:06:10 +09:00
syuilo
0294ef74e4 wip 2026-06-02 14:53:23 +09:00
syuilo
42af31fbbd Update room.core.vue 2026-06-02 14:47:20 +09:00
syuilo
b9ea42531a wip 2026-06-02 14:39:15 +09:00
syuilo
39c4618a35 Merge branch 'develop' into room 2026-06-02 14:12:58 +09:00
syuilo
b025ce0ae6 wip 2026-06-02 10:50:24 +09:00
syuilo
07e660df01 Update MkWorldAvatarEditDialog.vue 2026-06-01 20:28:21 +09:00
syuilo
e07eed4a4b wip 2026-06-01 18:33:50 +09:00
syuilo
19ad95cfa6 Update room.core.vue 2026-06-01 13:13:56 +09:00
syuilo
8f5f595073 Update room.core.vue 2026-06-01 13:13:32 +09:00
syuilo
6a536d5c92 refactor 2026-06-01 13:12:39 +09:00
syuilo
c1e6e6a871 wip 2026-06-01 13:05:35 +09:00
syuilo
039bce590f wip 2026-06-01 12:44:46 +09:00
syuilo
24dac68475 Update MkWorldAvatarManager.vue 2026-06-01 11:21:23 +09:00
syuilo
8217a3a42c wip 2026-06-01 11:08:50 +09:00
syuilo
dbcb856638 wip 2026-06-01 11:07:05 +09:00
syuilo
dbbb197816 wip 2026-06-01 10:58:13 +09:00
syuilo
40263b0af5 wip 2026-06-01 09:42:07 +09:00
syuilo
b978df8990 wip 2026-06-01 09:26:07 +09:00
syuilo
3a0da02b50 wip 2026-06-01 09:17:24 +09:00
syuilo
af7d1a763a wip 2026-06-01 09:15:58 +09:00
syuilo
a7e16c09dd up 2026-05-31 20:07:47 +09:00
syuilo
351872f47b Update engine.ts 2026-05-31 18:57:23 +09:00
syuilo
e3c6c3025a wip 2026-05-31 18:52:54 +09:00
syuilo
273bc59cdb Update room.core.vue 2026-05-31 18:49:24 +09:00
syuilo
7a3e27d653 Update PlayerContainer.ts 2026-05-31 18:39:57 +09:00
syuilo
6a8aacfb95 wip 2026-05-31 17:07:39 +09:00
syuilo
99659a89b4 Update room.core.vue 2026-05-31 16:56:40 +09:00
syuilo
4591b940c3 Update room.core.vue 2026-05-31 16:52:23 +09:00
syuilo
f01e9aa2ac wip 2026-05-31 16:50:33 +09:00
syuilo
bba1605801 Update WorldRoomMultiplayService.ts 2026-05-31 16:48:06 +09:00
syuilo
6d6dbb2584 Update PlayerContainer.ts 2026-05-31 16:47:07 +09:00
syuilo
51a08bbf34 Update room.core.vue 2026-05-31 16:17:27 +09:00
syuilo
7fa8bcc9fd wip 2026-05-31 16:09:58 +09:00
syuilo
9d5818f7b5 wip 2026-05-31 15:24:55 +09:00
syuilo
bb028fda00 wip 2026-05-31 14:56:40 +09:00
syuilo
56ae76e994 Update OverlayPanel.vue 2026-05-31 14:39:20 +09:00
syuilo
69bc12201e wip 2026-05-31 14:35:15 +09:00
syuilo
313e730fbb Update room.core.vue 2026-05-31 14:24:24 +09:00
syuilo
c14986b43d Update room.core.vue 2026-05-31 14:22:59 +09:00
syuilo
69c75fc464 Update room.core.vue 2026-05-31 13:39:47 +09:00
syuilo
1b13c945ff wip 2026-05-31 13:37:40 +09:00
syuilo
6e763d4daa Update OverlayPanel.vue 2026-05-31 13:28:12 +09:00
syuilo
d1e5b04a10 Update OverlayPanel.vue 2026-05-31 13:27:32 +09:00
syuilo
7fec78b70e wip 2026-05-31 13:22:54 +09:00
syuilo
c962cd33b3 wip 2026-05-31 12:47:24 +09:00
syuilo
814bf357ca wip 2026-05-31 11:30:32 +09:00
syuilo
222d914544 Update room.core.vue 2026-05-31 11:29:22 +09:00
syuilo
b81b9e1da7 wip 2026-05-31 11:16:56 +09:00
syuilo
52cb23da63 wip 2026-05-31 10:09:25 +09:00
syuilo
6b4c2c9141 Update room.core.vue 2026-05-31 10:02:09 +09:00
syuilo
7af9069962 wip 2026-05-31 09:58:26 +09:00
syuilo
8f9be8c735 wip 2026-05-31 09:40:20 +09:00
syuilo
cdbf5c843f wip 2026-05-31 09:26:19 +09:00
syuilo
27fff648b6 Update README.md 2026-05-30 21:17:27 +09:00
syuilo
5cdc5f53b4 Update MkWorldMonoOptionsForm.vue 2026-05-30 20:45:26 +09:00
syuilo
c67e231ff5 wip 2026-05-30 19:18:43 +09:00
syuilo
f564b7a29b object -> furniture 2026-05-30 19:14:45 +09:00
syuilo
1eea31aacd object -> furniture 2026-05-30 19:00:43 +09:00
syuilo
5da458ca54 Update engine.ts 2026-05-30 18:32:39 +09:00
syuilo
c343ccbdff wip 2026-05-30 18:25:16 +09:00
syuilo
f8a981a1fc wip 2026-05-30 18:20:04 +09:00
syuilo
72df3d29ce wip 2026-05-29 10:57:17 +09:00
syuilo
75454b8075 wip 2026-05-29 10:08:08 +09:00
syuilo
559846fd7c wip 2026-05-29 09:56:22 +09:00
syuilo
8c1fa69800 Update MkWorldAvatarEditDialog.vue 2026-05-29 09:44:11 +09:00
syuilo
810d437a2c Update MkWorldAvatarEditDialog.vue 2026-05-29 09:40:33 +09:00
syuilo
3fca2cb664 wip 2026-05-29 08:01:39 +09:00
syuilo
e95552b264 wip 2026-05-29 07:42:48 +09:00
syuilo
781f6135fd wip 2026-05-29 07:39:52 +09:00
syuilo
e90830c738 Update MkWorldAvatarEditDialog.vue 2026-05-28 21:02:26 +09:00
syuilo
ab32ab6746 wip 2026-05-28 20:52:10 +09:00
syuilo
86c4f3072f wip 2026-05-28 18:27:44 +09:00
syuilo
8bb8b2fc97 wip 2026-05-28 17:50:04 +09:00
syuilo
fad53328be wip 2026-05-28 17:42:54 +09:00
syuilo
0a6bfce548 wip 2026-05-28 17:32:45 +09:00
syuilo
5998b01ffc wip 2026-05-28 17:24:22 +09:00
syuilo
9a91170839 wip 2026-05-28 15:03:06 +09:00
syuilo
a0aa64cd9f wip 2026-05-28 13:12:35 +09:00
syuilo
95864071a0 Update PlayerContainer.ts 2026-05-28 10:31:01 +09:00
syuilo
ff9d997cec Update edit-world-avatar-dialog.vue 2026-05-28 10:16:33 +09:00
syuilo
c6d0855a2e wip 2026-05-28 10:15:02 +09:00
syuilo
624b858758 wip 2026-05-28 09:56:40 +09:00
syuilo
d722b8e9d7 wip 2026-05-28 09:40:02 +09:00
syuilo
fd02ea906f wip 2026-05-28 09:24:03 +09:00
syuilo
aed9a4a1b4 Update preferences.vue 2026-05-28 08:54:35 +09:00
syuilo
ace51f7605 wip 2026-05-28 08:53:14 +09:00
syuilo
c10e1c4b31 wip 2026-05-28 08:51:23 +09:00
syuilo
4c80e92522 wip 2026-05-28 08:04:50 +09:00
syuilo
71c3f921cc wip 2026-05-28 07:53:30 +09:00
syuilo
6bc9eb9d84 wip 2026-05-27 21:12:34 +09:00
syuilo
fbf3c02392 wip 2026-05-27 21:00:05 +09:00
syuilo
0c8b1055d4 wip 2026-05-27 19:54:40 +09:00
syuilo
0e7b517554 wip 2026-05-27 19:18:55 +09:00
syuilo
5cefdd224b wip 2026-05-27 18:58:50 +09:00
syuilo
1a5a4c834f wip 2026-05-27 17:58:34 +09:00
syuilo
2df145c458 wip 2026-05-27 17:28:32 +09:00
syuilo
43958fc70d wip 2026-05-27 16:57:11 +09:00
syuilo
8f94b04edc Update WorldRoomService.ts 2026-05-27 10:11:03 +09:00
syuilo
c7d07d0364 Update WorldRoomService.ts 2026-05-27 10:06:59 +09:00
syuilo
5d974e8242 wip 2026-05-27 09:56:17 +09:00
syuilo
807ade4c37 wip 2026-05-27 09:55:12 +09:00
syuilo
2efd52f400 wip 2026-05-26 15:00:14 +09:00
syuilo
f0375aa420 wip 2026-05-26 14:42:19 +09:00
syuilo
04c2703ace wip 2026-05-26 10:51:07 +09:00
syuilo
65022ed342 wip 2026-05-26 10:44:40 +09:00
syuilo
0d025d99f2 Update ObjectContainer.ts 2026-05-26 10:43:20 +09:00
syuilo
e68847e563 wip 2026-05-26 10:42:40 +09:00
syuilo
409e9f744c wip 2026-05-26 10:01:50 +09:00
syuilo
9f81a88466 wip 2026-05-26 09:59:11 +09:00
syuilo
564d2446cb wip 2026-05-26 09:48:29 +09:00
syuilo
cb605a3978 wip 2026-05-26 09:45:40 +09:00
syuilo
4f89010aca wip 2026-05-26 09:44:05 +09:00
syuilo
8933ac119a wip 2026-05-26 09:38:58 +09:00
syuilo
982897f1b5 wip 2026-05-26 09:36:14 +09:00
syuilo
dd69662603 wip 2026-05-26 09:34:55 +09:00
syuilo
9485b3fba9 wip 2026-05-26 09:30:06 +09:00
syuilo
245f673a78 wip 2026-05-26 09:29:48 +09:00
syuilo
71bb388b8b wip: separate package 2026-05-25 22:17:58 +09:00
syuilo
39f2614f9a Update steelRack.schema.ts 2026-05-25 20:27:33 +09:00
syuilo
46efad5b12 wip 2026-05-25 20:26:36 +09:00
syuilo
fda845cb63 wip 2026-05-25 18:28:46 +09:00
syuilo
cccc66bbbf Create object-schema-defs.ts 2026-05-25 16:28:47 +09:00
syuilo
32822d6d07 mi-objet 2026-05-25 16:25:13 +09:00
syuilo
7bca3ca179 wip 2026-05-25 16:24:07 +09:00
syuilo
6961f277b5 temporary del 2026-05-25 15:57:37 +09:00
syuilo
eb33c8a93f wip 2026-05-25 15:51:51 +09:00
syuilo
43232d5baa wip 2026-05-25 15:07:47 +09:00
syuilo
b8f67a3e69 wip 2026-05-25 15:07:22 +09:00
syuilo
748b571856 wip i18n separation 2026-05-25 14:58:28 +09:00
syuilo
f0bf3cda75 wip i18n separation 2026-05-25 14:33:22 +09:00
syuilo
bf28b303d7 clean 2026-05-25 14:01:13 +09:00
syuilo
1a064ea430 wip 2026-05-25 13:51:54 +09:00
syuilo
68431f62f1 Update icosahedron.blend 2026-05-25 13:49:13 +09:00
syuilo
2c8abf8239 icosahedron 2026-05-25 13:48:08 +09:00
syuilo
822b567742 wip 2026-05-25 13:21:14 +09:00
syuilo
a89475e820 Update room.add-object-dialog.vue 2026-05-25 13:12:06 +09:00
syuilo
2d8c6ca585 Update room.object-customize-form.vue 2026-05-25 12:48:04 +09:00
syuilo
fc6a840132 chair 2026-05-25 12:40:46 +09:00
syuilo
9a44c69177 wip 2026-05-25 12:33:17 +09:00
syuilo
0be946a10b Update ja-JP.yml 2026-05-25 12:17:54 +09:00
syuilo
a201f72c8a wip 2026-05-25 12:13:16 +09:00
syuilo
83a4281a30 wip 2026-05-25 11:59:23 +09:00
syuilo
7546bf7dc2 wip 2026-05-25 10:53:47 +09:00
syuilo
2942234778 Update cardboardBox.ts 2026-05-25 10:31:37 +09:00
syuilo
540e20864c wip 2026-05-25 10:21:57 +09:00
syuilo
6ee6894184 Update woodRingsPendantLight.ts 2026-05-25 09:37:48 +09:00
syuilo
2a880164f8 Update woodRingsPendantLight.ts 2026-05-25 09:24:09 +09:00
syuilo
c002752d65 Update woodRingsPendantLight.ts 2026-05-25 09:17:44 +09:00
syuilo
05e1a43a1c wip 2026-05-24 05:48:13 +09:00
syuilo
ec79d6bee7 Update room.object-customize-form.vue 2026-05-24 05:25:29 +09:00
syuilo
a3b6dc0899 wip 2026-05-24 05:09:52 +09:00
syuilo
2fffa94eb2 wip 2026-05-24 04:48:56 +09:00
syuilo
0fade25574 Update object.ts 2026-05-24 04:35:33 +09:00
syuilo
aa4fa9c54e wip 2026-05-24 04:33:42 +09:00
syuilo
fdba915288 Update object.ts 2026-05-23 20:24:07 +09:00
syuilo
773077470e Update engine.ts 2026-05-23 20:20:51 +09:00
syuilo
3eafdbe57a Update cardboardBox.ts 2026-05-23 20:10:42 +09:00
syuilo
8138fd2c34 Update cardboardBox.ts 2026-05-23 20:10:11 +09:00
syuilo
ce72d10491 Update engine.ts 2026-05-23 19:43:36 +09:00
syuilo
f8851c0f2e wip 2026-05-23 19:43:10 +09:00
syuilo
d3a7dc0ea5 Update engine.ts 2026-05-23 18:15:39 +09:00
syuilo
32bdb77e97 Update room.core.vue 2026-05-23 18:14:21 +09:00
syuilo
8ac3c77ae1 Update object.ts 2026-05-23 18:09:36 +09:00
syuilo
8611d55ffc wip 2026-05-23 17:57:35 +09:00
syuilo
14b648faa6 wip 2026-05-23 17:48:39 +09:00
syuilo
c6a755c0a7 wip 2026-05-23 16:53:37 +09:00
syuilo
1154bb5370 wip 2026-05-23 15:54:07 +09:00
syuilo
1701e993cf Update cardboardBox.ts 2026-05-23 13:38:52 +09:00
syuilo
99f998cf60 wip 2026-05-23 13:32:51 +09:00
syuilo
05a8dcf897 wip 2026-05-22 18:59:21 +09:00
syuilo
90ea7902eb Update engine.ts 2026-05-22 17:43:44 +09:00
syuilo
644c16aee6 Update engine.ts 2026-05-22 17:42:29 +09:00
syuilo
ce5857b265 Update engine.ts 2026-05-22 17:32:20 +09:00
syuilo
fbae160442 Update README.md 2026-05-22 11:15:35 +09:00
syuilo
2daad2223d Update engine.ts 2026-05-22 10:40:50 +09:00
syuilo
6cbae336d0 Update env.ts 2026-05-22 10:39:42 +09:00
syuilo
47f9f38a40 wip 2026-05-22 10:31:40 +09:00
syuilo
73d6764a0b wip 2026-05-21 21:23:05 +09:00
syuilo
c85ee681bb Update engine.ts 2026-05-21 21:03:19 +09:00
syuilo
61d5a5620a Update issyoubin.ts 2026-05-21 18:38:04 +09:00
syuilo
2873768cd2 Update engine.ts 2026-05-21 18:08:15 +09:00
syuilo
4c957ecd9b wip 2026-05-21 17:29:37 +09:00
syuilo
8c61fc2be0 refactor 2026-05-21 16:52:44 +09:00
syuilo
57057f56f5 wip 2026-05-21 16:37:54 +09:00
syuilo
985dd72a7f Merge branch 'develop' into room 2026-05-21 16:15:27 +09:00
syuilo
dfbe765baa ObjectContainer 2026-05-21 16:15:18 +09:00
syuilo
555fa80709 wip 2026-05-21 14:26:27 +09:00
syuilo
7546e7c800 wip 2026-05-21 14:17:03 +09:00
syuilo
ce36de7e4f Update steelRack.ts 2026-05-21 14:09:00 +09:00
syuilo
1cb96731e6 Update steelRack.ts 2026-05-21 14:07:03 +09:00
syuilo
4bf0ba3716 semi-procedual steel-rack 2026-05-21 13:29:55 +09:00
syuilo
d2e48b707b wip 2026-05-21 12:04:43 +09:00
syuilo
8e70550c84 wip 2026-05-21 11:52:47 +09:00
syuilo
199c0f533b wip 2026-05-21 10:46:15 +09:00
syuilo
ad44cc3446 wip 2026-05-21 09:08:14 +09:00
syuilo
8f73c22df3 Update engine.ts 2026-05-20 19:54:23 +09:00
syuilo
20eb342eff wip 2026-05-20 19:09:41 +09:00
syuilo
5d17b557c1 wip 2026-05-20 15:21:02 +09:00
syuilo
43cb18b3e9 wip 2026-05-19 20:26:33 +09:00
syuilo
75dfcf48b6 Update electronicDisplayBoard.ts 2026-05-19 19:23:45 +09:00
syuilo
9e9ae54c26 Update package.json 2026-05-19 16:08:43 +09:00
syuilo
e78694f11b up 2026-05-19 16:04:48 +09:00
syuilo
5397beb965 Merge branch 'develop' into room 2026-05-19 15:56:21 +09:00
syuilo
92bc50cb90 Merge branch 'develop' into room 2026-05-19 15:23:21 +09:00
syuilo
45d8b656cb Update EngineBase.ts 2026-05-19 13:41:08 +09:00
syuilo
a5e0c594b2 Update engineControllerBase.ts 2026-05-19 13:15:40 +09:00
syuilo
646b0ca041 wip 2026-05-19 13:15:20 +09:00
syuilo
de27cc92dc Update engine.ts 2026-05-18 13:27:48 +09:00
syuilo
09d5502eb6 Update utility.ts 2026-05-18 10:44:05 +09:00
syuilo
dc6ac2529f Update utility.ts 2026-05-18 10:43:46 +09:00
syuilo
a766609963 wip 2026-05-18 10:24:18 +09:00
syuilo
e1aa62d2b7 Update engine.ts 2026-05-18 09:40:07 +09:00
syuilo
c62d92d88f Update engine.ts 2026-05-18 07:51:14 +09:00
syuilo
1a456998bc Merge branch 'develop' into room 2026-05-18 07:21:45 +09:00
syuilo
14fe51c0a4 wip 2026-05-16 16:40:43 +09:00
syuilo
f203ebe4d5 Update engineControllerBase.ts 2026-05-16 11:36:27 +09:00
syuilo
de979d3a98 wip 2026-05-16 11:26:46 +09:00
syuilo
7c2e3adf25 wip 2026-05-16 11:15:27 +09:00
syuilo
e24cf73a83 update text 2026-05-16 10:52:43 +09:00
syuilo
d3eb3867bd handle lost event 2026-05-16 10:49:25 +09:00
syuilo
fcfd3331dd miWorldRoomTemp 2026-05-16 09:05:19 +09:00
syuilo
5aa1f0d562 wip 2026-05-16 07:27:50 +09:00
syuilo
491510a11e refactor 2026-05-16 07:15:27 +09:00
syuilo
e0382cdf86 duplicateSelectedObject 2026-05-16 07:12:11 +09:00
syuilo
9ae6a9f426 tweak anim 2026-05-16 07:00:19 +09:00
syuilo
a8ca0cf9ed refactor 2026-05-16 07:00:00 +09:00
syuilo
45fc8994f0 Merge branch 'room' of https://github.com/misskey-dev/misskey into room 2026-05-16 06:56:40 +09:00
syuilo
85ae7c7efc Update engine.ts 2026-05-16 06:56:38 +09:00
syuilo
2867f03f50 Update engineControllerBase.ts 2026-05-15 22:03:01 +09:00
syuilo
1cd75fe5a8 wip 2026-05-15 19:59:41 +09:00
syuilo
7f2286f1fd Update room.core.vue 2026-05-15 19:45:17 +09:00
syuilo
840e2c3744 wip 2026-05-15 18:09:14 +09:00
syuilo
af1dbd3a71 Update room.core.vue 2026-05-15 17:47:16 +09:00
syuilo
82cdaeb56e wip 2026-05-15 17:45:20 +09:00
syuilo
ea732458ab Update room.core.vue 2026-05-15 17:23:42 +09:00
syuilo
b6e92cdef9 Update engine.ts 2026-05-15 17:12:55 +09:00
syuilo
11b699a180 wip 2026-05-15 16:06:52 +09:00
syuilo
e90a53a851 wip 2026-05-15 15:05:15 +09:00
syuilo
1469f890dd Update engine.ts 2026-05-15 14:40:37 +09:00
syuilo
6c82e0fef8 Update engine.ts 2026-05-15 14:34:26 +09:00
syuilo
6ee52259c4 lazy create putParticleSystem 2026-05-15 13:23:05 +09:00
syuilo
414b522d0a Update engine.ts 2026-05-15 13:15:54 +09:00
syuilo
a95f3c9467 Update engine.ts 2026-05-15 13:06:15 +09:00
syuilo
ecb2ae379e Update engine.ts 2026-05-15 12:56:39 +09:00
syuilo
2e34e6f454 wip 2026-05-15 12:52:54 +09:00
syuilo
bee415625a Update WorldRoomService.ts 2026-05-15 09:45:52 +09:00
syuilo
d0d3aef76c Merge branch 'develop' into room 2026-05-15 09:41:51 +09:00
syuilo
93a46ae2d6 Merge branch 'develop' into room 2026-05-15 09:39:21 +09:00
syuilo
8b5d006248 Update room.core.vue 2026-05-14 21:39:02 +09:00
syuilo
bcc965a19c Update room.core.vue 2026-05-14 21:26:24 +09:00
syuilo
2c5ce5bfae Update room.core.vue 2026-05-14 21:25:55 +09:00
syuilo
170980a0a6 Update WorldRoomService.ts 2026-05-14 21:19:35 +09:00
syuilo
216fb56a4b wip 2026-05-14 21:08:50 +09:00
syuilo
8defcd9f9f wip 2026-05-14 19:35:50 +09:00
syuilo
d38f04c97f wip 2026-05-14 19:08:36 +09:00
syuilo
ecedd71192 Update engine.ts 2026-05-14 18:47:51 +09:00
syuilo
b582895ac6 up 2026-05-14 17:45:40 +09:00
syuilo
267dc5fb64 Update object.ts 2026-05-14 17:20:11 +09:00
syuilo
ab02a99a0e wip 2026-05-14 17:19:59 +09:00
syuilo
03481322a9 wip 2026-05-14 17:10:28 +09:00
syuilo
fb130466ae wip 2026-05-14 15:41:12 +09:00
syuilo
43f20e44c3 Update room.add-object-dialog.vue 2026-05-14 15:04:37 +09:00
syuilo
2966d2a862 Merge branch 'develop' into room 2026-05-14 14:43:34 +09:00
syuilo
8f23991723 Update engineControllerBase.ts 2026-05-14 14:38:59 +09:00
syuilo
bfdda83a6d wip 2026-05-14 14:38:00 +09:00
syuilo
ddce5ff526 wip 2026-05-14 14:30:14 +09:00
syuilo
8dd62e57e2 Update object.ts 2026-05-14 14:27:45 +09:00
syuilo
0b17663eeb wip 2026-05-14 14:25:17 +09:00
syuilo
41c5e7242d add note 2026-05-14 12:44:52 +09:00
syuilo
ef8f593de8 attachments 2026-05-14 12:43:30 +09:00
syuilo
410cc8ac50 Update engineControllerBase.ts 2026-05-13 21:13:26 +09:00
syuilo
a905eeef03 Update engine.ts 2026-05-13 19:53:34 +09:00
syuilo
ebb74eff92 Update room.add-object-dialog.vue 2026-05-13 19:44:03 +09:00
syuilo
b69efeeb79 wip 2026-05-13 19:26:55 +09:00
syuilo
1a57b8177b Update room.add-object-dialog.vue 2026-05-13 18:36:09 +09:00
syuilo
0e300a9795 wip 2026-05-13 18:33:04 +09:00
syuilo
13d35f8124 fix 2026-05-13 18:23:48 +09:00
syuilo
b53e4f6742 wip 2026-05-13 15:48:34 +09:00
syuilo
9d102c2a70 wip 2026-05-13 14:09:43 +09:00
syuilo
84f389aebd wip 2026-05-13 12:29:34 +09:00
syuilo
8072dbef8b wip 2026-05-13 10:30:42 +09:00
syuilo
8195f5257f support pinch/zoom 2026-05-12 20:23:20 +09:00
syuilo
b8f8ac031c wip 2026-05-11 20:40:21 +09:00
syuilo
57f1adb402 wip 2026-05-11 15:45:16 +09:00
syuilo
48ce2e09ab Update utility.ts 2026-05-11 15:35:04 +09:00
syuilo
dca5450340 wip 2026-05-11 15:28:46 +09:00
syuilo
202c9e8f25 wip 2026-05-11 15:07:44 +09:00
syuilo
8e1e69e60e Update engine.ts 2026-05-11 14:40:34 +09:00
syuilo
f3d0edf546 wip: sr in edit mode 2026-05-11 13:35:32 +09:00
syuilo
08dfd23c19 Update engine.ts 2026-05-11 11:01:17 +09:00
syuilo
b231357ae8 Update engine.ts 2026-05-11 10:30:40 +09:00
syuilo
102cf03213 tweak 2026-05-11 10:19:15 +09:00
syuilo
d1da15128e wip 2026-05-11 09:50:27 +09:00
syuilo
0d96f418ab wip 2026-05-11 09:34:36 +09:00
syuilo
b879f2f1e3 Update engine.ts 2026-05-09 16:36:59 +09:00
syuilo
fb77eb4349 Update previewEngine.ts 2026-05-09 14:41:39 +09:00
syuilo
79ad7e274c Update previewEngine.ts 2026-05-09 14:29:04 +09:00
syuilo
4890673013 Update previewEngine.ts 2026-05-09 14:28:14 +09:00
syuilo
1f204f572f Update room.vue 2026-05-09 14:15:15 +09:00
syuilo
9976588025 Update room.add-object-dialog.vue 2026-05-09 14:13:49 +09:00
syuilo
414a28fb19 wip 2026-05-09 14:06:07 +09:00
syuilo
3605ffdafc wip 2026-05-09 13:52:45 +09:00
syuilo
179c9fc70a Update previewEngine.ts 2026-05-09 13:10:30 +09:00
syuilo
f3a7f10319 wip 2026-05-09 13:03:57 +09:00
syuilo
17333fd7e5 Merge branch 'develop' into room 2026-05-08 18:26:18 +09:00
syuilo
14f4d2c228 wip 2026-05-08 18:23:16 +09:00
syuilo
18b4210eef Update room.add-object-dialog.vue 2026-05-08 18:10:59 +09:00
syuilo
54c2c4dd53 wip 2026-05-07 17:38:49 +09:00
syuilo
42da479026 Update tabletopLcdButtonsController.ts 2026-05-07 16:00:49 +09:00
syuilo
adc487ef78 wip 2026-05-07 15:17:49 +09:00
syuilo
b08b3a2500 Update curtain.blend 2026-05-07 14:56:14 +09:00
syuilo
4324b6def2 wip 2026-05-07 14:17:09 +09:00
syuilo
21fe0f5e67 wip 2026-05-07 12:56:18 +09:00
syuilo
3ede04c563 Merge branch 'develop' into room 2026-05-07 11:43:07 +09:00
syuilo
b750d69065 Merge branch 'develop' into room 2026-05-07 11:33:46 +09:00
syuilo
0c4b36e2d1 wip 2026-05-07 10:56:26 +09:00
syuilo
5cb9474494 wip 2026-05-06 19:17:46 +09:00
syuilo
3be075d281 Update room.add-object-dialog.vue 2026-05-06 18:30:57 +09:00
syuilo
d7c94fbf86 handheldGameConsole 2026-05-06 18:25:07 +09:00
syuilo
c6d7aa7be8 Update room.add-object-dialog.vue 2026-05-06 14:31:55 +09:00
syuilo
d56c6dfe57 wip 2026-05-06 14:24:38 +09:00
syuilo
f9be5d8c47 wip 2026-05-06 11:49:16 +09:00
syuilo
ac023668a7 Update engine.ts 2026-05-06 11:42:02 +09:00
syuilo
59c9b86842 Update engine.ts 2026-05-06 11:39:13 +09:00
syuilo
c4adcde114 Update room.vue 2026-05-05 20:29:19 +09:00
syuilo
db1b5e9ce9 wip 2026-05-05 20:29:10 +09:00
syuilo
a1cb4b8304 Update README.md 2026-05-04 22:37:55 +09:00
syuilo
fd04c5f2fc Update README.md 2026-05-04 22:36:57 +09:00
syuilo
7be7465703 wip 2026-05-04 21:41:21 +09:00
syuilo
626ae675bc wip 2026-05-04 21:30:43 +09:00
syuilo
2a1cd5c197 wip 2026-05-04 19:32:47 +09:00
syuilo
a9dd5fd5bf wip 2026-05-04 16:41:50 +09:00
syuilo
beb5d1dec5 wip 2026-05-04 16:25:11 +09:00
syuilo
d451ce8c36 wip 2026-05-04 15:41:44 +09:00
syuilo
f0f78a11cb Update woodRingsPendantLight.ts 2026-05-04 10:47:14 +09:00
syuilo
69dd2675fa Update env.ts 2026-05-04 10:05:25 +09:00
syuilo
eb7691e3ba wip 2026-05-04 09:38:35 +09:00
syuilo
db90e4ebc0 Update env.ts 2026-05-04 09:24:42 +09:00
syuilo
29491997ea wip 2026-05-04 09:22:29 +09:00
syuilo
015e6d1c81 wip 2026-05-04 09:14:58 +09:00
syuilo
49ee15dd9a Update room.vue 2026-05-03 21:35:19 +09:00
syuilo
e6b5758d54 wip 2026-05-03 21:34:11 +09:00
syuilo
210368d597 wip 2026-05-03 20:49:03 +09:00
syuilo
203f29afb9 wip 2026-05-03 18:42:04 +09:00
syuilo
fa0eac34c2 Update engine.ts 2026-05-03 17:06:17 +09:00
syuilo
d4fa5cf7ca Update engine.ts 2026-05-03 17:02:36 +09:00
syuilo
479e9af17e Update engine.ts 2026-05-03 17:01:49 +09:00
syuilo
f03af71dc0 wip 2026-05-03 16:54:11 +09:00
syuilo
6d94f00ecf Update env.ts 2026-05-03 16:21:57 +09:00
syuilo
9e848f3135 default -> simple 2026-05-03 16:20:53 +09:00
syuilo
b16b158372 Update room.vue 2026-05-03 16:19:12 +09:00
syuilo
39525c66c2 heya -> env 2026-05-03 16:19:03 +09:00
syuilo
d3dc9bc86c wip 2026-05-03 16:12:58 +09:00
syuilo
444d862eac wip 2026-05-03 16:00:57 +09:00
syuilo
74e9851511 wip 2026-05-03 15:45:33 +09:00
syuilo
a8586fe224 Update wallMountSpotLight.ts 2026-05-03 14:15:08 +09:00
syuilo
d132fdfc04 Update ductRailSpotLights.ts 2026-05-03 14:11:31 +09:00
syuilo
3ba902c2b6 Update ductRailSpotLights.ts 2026-05-03 14:02:59 +09:00
syuilo
50f7c74259 Update controller.ts 2026-05-03 12:17:55 +09:00
syuilo
9b7c908c68 Update room.vue 2026-05-03 12:13:33 +09:00
syuilo
e7290c0486 rotation 2026-05-03 12:12:45 +09:00
syuilo
db22eddd1e Update engine.ts 2026-05-03 11:51:35 +09:00
syuilo
a8cea0622d Update room.object-customize-form.vue 2026-05-03 10:53:21 +09:00
syuilo
5bf1b5569f 🎨 2026-05-03 10:50:10 +09:00
syuilo
d1cb2c5bc7 Update worker.ts 2026-05-03 10:00:01 +09:00
syuilo
a49697042a refactor: use AbortController 2026-05-03 09:40:04 +09:00
syuilo
4656d93358 wip 2026-05-02 21:27:17 +09:00
syuilo
2e5a02a85a Update controller.ts 2026-05-02 20:12:38 +09:00
syuilo
8421ec75da Merge branch 'develop' into room 2026-05-02 12:41:38 +09:00
syuilo
dcd2160294 Update ductRailSpotLights.ts 2026-05-01 20:37:47 +09:00
syuilo
3833469955 Update engine.ts 2026-05-01 20:32:50 +09:00
syuilo
0adfbc8d51 Update lavaLamp.ts 2026-05-01 19:46:44 +09:00
syuilo
a931079896 Update lavaLamp.ts 2026-05-01 19:37:50 +09:00
syuilo
18c08f52f1 wip 2026-05-01 19:31:36 +09:00
syuilo
8975449538 Update engine.ts 2026-05-01 18:04:25 +09:00
syuilo
4e149a642d wip 2026-05-01 17:53:40 +09:00
syuilo
09d133242d Update engine.ts 2026-05-01 12:19:03 +09:00
syuilo
a8db20259b fix wasdVec calculation 2026-05-01 12:04:05 +09:00
syuilo
d1eda166de wip 2026-05-01 11:54:52 +09:00
syuilo
7df4b729e9 Update controller.ts 2026-05-01 11:36:30 +09:00
syuilo
3ed6148f6a wip 2026-05-01 10:02:39 +09:00
syuilo
ebe5739ce3 recordPlayer 2026-05-01 09:58:13 +09:00
syuilo
9bc404a8f5 wip 2026-04-30 21:49:09 +09:00
syuilo
90ff3d79d1 Update electronicDisplayBoard.ts 2026-04-30 21:20:55 +09:00
syuilo
6fb49ab88d note 2026-04-30 20:55:21 +09:00
syuilo
722d09b1ae wip 2026-04-30 20:52:53 +09:00
syuilo
6d6ae6728c wip 2026-04-30 19:24:10 +09:00
syuilo
ce2e74f3ca wip 2026-04-30 18:06:50 +09:00
syuilo
b34e957c25 Update electronicDisplayBoard.ts 2026-04-30 17:38:34 +09:00
syuilo
157b4673fd Update tv.ts 2026-04-30 16:48:45 +09:00
syuilo
ac1a19e95c Merge branch 'develop' into room 2026-04-30 16:44:38 +09:00
syuilo
5639324077 up 2026-04-30 16:44:20 +09:00
syuilo
a471fe16fa wip 2026-04-30 16:34:07 +09:00
syuilo
6cf90fd714 wip 2026-04-30 16:01:11 +09:00
syuilo
c7c785ad2a Update heya.ts 2026-04-30 14:05:58 +09:00
syuilo
bde64b5b1f Update engine.ts 2026-04-30 13:53:28 +09:00
syuilo
30f7727e33 wip 2026-04-30 13:32:10 +09:00
syuilo
5d36da17fe wip 2026-04-30 12:16:35 +09:00
syuilo
7bcda08339 wip 2026-04-30 11:52:53 +09:00
syuilo
a15575078f Update engine.ts 2026-04-30 11:22:05 +09:00
syuilo
56d813a184 Update engine.ts 2026-04-30 11:13:26 +09:00
syuilo
8f5c09daa1 wip 2026-04-30 09:17:22 +09:00
syuilo
c2d5a33400 wip 2026-04-30 08:36:23 +09:00
syuilo
6f3f4e7ef1 wip 2026-04-30 08:32:55 +09:00
syuilo
49f21d7423 wip 2026-04-29 21:06:15 +09:00
syuilo
92a6086e21 Create wall-wood.png 2026-04-29 21:03:40 +09:00
syuilo
9d1c2d52d2 Update tabletopLcdButtonsController.ts 2026-04-29 20:57:59 +09:00
syuilo
f97a6c6d55 wip 2026-04-29 20:16:23 +09:00
syuilo
0f69a284c6 wip 2026-04-29 20:07:34 +09:00
syuilo
1427d887dd wip 2026-04-29 20:00:56 +09:00
syuilo
a4c9aff8a9 wip 2026-04-29 19:38:21 +09:00
syuilo
5db8ccec74 Update worker.ts 2026-04-29 17:34:02 +09:00
syuilo
70ebc0d32c clean 2026-04-29 17:17:42 +09:00
syuilo
d885627350 refactor 2026-04-29 17:16:05 +09:00
syuilo
4a77db7866 refactor 2026-04-29 17:04:06 +09:00
syuilo
e50d4fa8ab Update engine.ts 2026-04-29 16:58:11 +09:00
syuilo
27578f2688 wip 2026-04-29 15:23:00 +09:00
syuilo
fc97ba41af Update README.md 2026-04-29 15:08:43 +09:00
syuilo
a3610ae6c4 🎨 2026-04-29 15:05:04 +09:00
syuilo
fc615daad3 Update engine.ts 2026-04-29 13:02:56 +09:00
syuilo
0321edb1ac Merge branch 'room' of https://github.com/misskey-dev/misskey into room 2026-04-29 13:02:31 +09:00
syuilo
8593737886 clean 2026-04-29 13:02:10 +09:00
syuilo
35ad1d758e wip 2026-04-29 12:59:46 +09:00
syuilo
110e5daa6f Update README.md 2026-04-29 12:19:26 +09:00
syuilo
5de191f01a wip 2026-04-29 11:41:14 +09:00
syuilo
80c2b1fa65 mhq 2026-04-29 11:01:42 +09:00
syuilo
c8441da835 wip 2026-04-29 10:04:45 +09:00
syuilo
fa2b1d6096 wip 2026-04-29 09:53:29 +09:00
syuilo
18a4da4ad7 wip 2026-04-29 09:49:04 +09:00
syuilo
52e9395fab wip 2026-04-29 09:40:12 +09:00
syuilo
d05d7938a4 Update engine.ts 2026-04-28 21:12:05 +09:00
syuilo
7846e8efb8 Update engine.ts 2026-04-28 21:08:14 +09:00
syuilo
e09f832fad wip 2026-04-28 17:53:48 +09:00
syuilo
515f6d9790 wip 2026-04-28 16:52:24 +09:00
syuilo
657159da45 wip 2026-04-28 16:25:43 +09:00
syuilo
e88188cd6d wip 2026-04-28 16:22:08 +09:00
syuilo
dcb834ed41 Update controller.ts 2026-04-28 16:11:27 +09:00
syuilo
5a7960d0a9 wip 2026-04-28 16:03:28 +09:00
syuilo
11e55d8fe8 wip 2026-04-28 15:54:41 +09:00
syuilo
836de1bb28 wip 2026-04-28 14:24:20 +09:00
syuilo
58e617af6d wip 2026-04-28 12:20:14 +09:00
syuilo
f44d566933 wip@p 2026-04-28 11:10:57 +09:00
syuilo
72fbc4bc9c Update engine.ts 2026-04-28 09:00:10 +09:00
syuilo
746c16aecc Update engine.ts 2026-04-28 08:58:00 +09:00
syuilo
7bef2cd8e0 wip 2026-04-28 08:46:33 +09:00
syuilo
aed73eb074 Update room.vue 2026-04-28 08:23:13 +09:00
syuilo
a756ca6ffb 🎨 2026-04-28 07:59:23 +09:00
syuilo
f19040888a wip 2026-04-27 20:57:30 +09:00
syuilo
634cdf5e1e wip 2026-04-27 19:05:03 +09:00
syuilo
c09d445215 wip 2026-04-27 18:28:15 +09:00
syuilo
1d71c0c6dd wip 2026-04-27 17:13:25 +09:00
syuilo
130a43f39a wip 2026-04-27 17:02:43 +09:00
syuilo
9c25c44a8a wip 2026-04-27 16:58:01 +09:00
syuilo
76ce6c84c0 wip 2026-04-27 16:51:05 +09:00
syuilo
24caff71e1 Update engine.ts 2026-04-27 15:22:50 +09:00
syuilo
036b8ea320 wip 2026-04-27 14:46:57 +09:00
syuilo
f98394fc60 Update engine.ts 2026-04-27 11:04:46 +09:00
syuilo
f52ac6351d wip 2026-04-27 10:58:23 +09:00
syuilo
eb1357026d Merge branch 'develop' into room 2026-04-27 10:35:14 +09:00
syuilo
1b5be37f9b Update room.vue 2026-04-27 10:23:56 +09:00
syuilo
42c659c580 Merge branch 'develop' into room 2026-04-27 10:19:37 +09:00
syuilo
ec32cad19f Update room.vue 2026-04-27 10:05:02 +09:00
syuilo
0910c47612 Update room.vue 2026-04-27 09:14:36 +09:00
syuilo
88e7303779 wip 2026-04-27 09:02:49 +09:00
syuilo
3a5532211b wip 2026-04-27 08:36:41 +09:00
syuilo
5b945278f9 wip 2026-04-26 21:21:04 +09:00
syuilo
0db2e5a42f Update room.vue 2026-04-26 20:56:47 +09:00
syuilo
8e1c5673b8 Update room.vue 2026-04-26 20:51:27 +09:00
syuilo
a763c396bd Update MkProgressBar.vue 2026-04-26 20:51:23 +09:00
syuilo
f5dae1d4c8 wip 2026-04-26 18:45:33 +09:00
syuilo
17697ba6ec joystick 2026-04-26 17:08:51 +09:00
syuilo
a77987ab28 Update engine.ts 2026-04-26 13:58:33 +09:00
syuilo
47d9e92776 Update engine.ts 2026-04-26 13:25:28 +09:00
syuilo
af928ffe93 Update engine.ts 2026-04-26 12:17:43 +09:00
syuilo
4adca586ed wip 2026-04-26 12:11:15 +09:00
syuilo
e703705d60 Update engine.ts 2026-04-26 11:33:30 +09:00
syuilo
be434949a4 Update engine.ts 2026-04-26 11:26:23 +09:00
syuilo
bbff43e9e6 Update room.vue 2026-04-26 11:22:46 +09:00
syuilo
5c28ee0536 Update engine.ts 2026-04-26 10:08:43 +09:00
syuilo
7e41d17c6a Update engine.ts 2026-04-26 10:05:00 +09:00
syuilo
1cd6d01fdd wip 2026-04-25 04:19:52 +09:00
syuilo
3263f4bcc0 Update tabletopLcdButtonsController.ts 2026-04-24 19:40:39 +09:00
syuilo
58feedb53d wip 2026-04-24 19:38:25 +09:00
syuilo
e62c85a971 wip 2026-04-24 19:35:19 +09:00
syuilo
585d727297 wip 2026-04-24 19:04:37 +09:00
syuilo
abd6c85b41 Update engine.ts 2026-04-24 18:10:43 +09:00
syuilo
9551a3d01a wip 2026-04-24 17:55:13 +09:00
syuilo
d281a81200 Update engine.ts 2026-04-24 16:43:04 +09:00
syuilo
82741c2d61 Update engine.ts 2026-04-24 16:23:13 +09:00
syuilo
b00880c21f wip 2026-04-24 15:42:58 +09:00
syuilo
df5d5d23cc Update engine.ts 2026-04-24 13:51:25 +09:00
syuilo
35f6cac9f6 wip 2026-04-24 13:26:38 +09:00
syuilo
7ddcbf5e94 Update previewEngine.ts 2026-04-24 11:42:49 +09:00
syuilo
634bae3c49 wip 2026-04-24 11:36:35 +09:00
syuilo
0b50aa9d13 Update room.add-object-dialog.vue 2026-04-24 11:27:08 +09:00
syuilo
e594ad9c6f Update room.add-object-dialog.vue 2026-04-24 11:19:49 +09:00
syuilo
d105c707ec Update room.add-object-dialog.vue 2026-04-24 10:59:42 +09:00
syuilo
93f24c5b8f Update room.add-object-dialog.vue 2026-04-24 10:33:55 +09:00
syuilo
7c170a21e5 wip 2026-04-24 10:16:13 +09:00
syuilo
1de4440dbd ductRailSpotLights 2026-04-23 21:17:12 +09:00
syuilo
e6ce36178c wip 2026-04-23 20:22:24 +09:00
syuilo
8a6e925297 wip 2026-04-23 20:18:46 +09:00
syuilo
eeda7e7002 note 2026-04-23 18:11:54 +09:00
syuilo
abfa67965e wip 2026-04-23 17:48:12 +09:00
syuilo
2c6560cc71 Update previewEngine.ts 2026-04-23 17:28:21 +09:00
syuilo
4984146f6e Update issyoubin.ts 2026-04-23 17:26:40 +09:00
syuilo
c36dfc6643 wip 2026-04-23 17:17:09 +09:00
syuilo
9184f0d7b9 issyoubin wip 2026-04-23 16:54:59 +09:00
syuilo
4c659c3129 Update previewEngine.ts 2026-04-23 16:50:54 +09:00
syuilo
10926e5525 Update utility.ts 2026-04-23 15:37:26 +09:00
syuilo
8d3a5a6503 morph 2026-04-23 13:45:50 +09:00
syuilo
924c517bb5 Update randomBooks.ts 2026-04-23 12:31:44 +09:00
syuilo
54c339d89c Update randomBooks.ts 2026-04-23 12:23:34 +09:00
syuilo
ce98d4244b Update tabletopDigitalClock.ts 2026-04-23 12:19:54 +09:00
syuilo
4e4b56699b wip 2026-04-23 11:57:53 +09:00
syuilo
fab7667b0a 平積み 2026-04-23 09:59:15 +09:00
syuilo
942e32f4de 🎨 2026-04-23 08:19:00 +09:00
syuilo
3897b044fe Update previewEngine.ts 2026-04-22 22:39:42 +09:00
syuilo
c47a0c33cf wip 2026-04-22 21:45:33 +09:00
syuilo
2b849685a1 Update engine.ts 2026-04-22 21:24:19 +09:00
syuilo
bad29c7604 tableSalt 2026-04-22 21:07:33 +09:00
syuilo
b7b24a2140 Update books.ts 2026-04-22 19:45:21 +09:00
syuilo
20d9a03bb3 wip 2026-04-22 19:10:18 +09:00
syuilo
09d58a3ecf wip 2026-04-22 18:33:17 +09:00
syuilo
94e8050455 wallMountSpotLight 2026-04-22 17:28:15 +09:00
syuilo
2f039ce2e9 Update controller.ts 2026-04-22 16:04:38 +09:00
syuilo
61fd35bc97 wip 2026-04-22 16:02:24 +09:00
syuilo
2d36ccf1b2 wip 2026-04-22 14:12:11 +09:00
syuilo
174221fdc4 Update engine.ts 2026-04-22 13:56:38 +09:00
syuilo
08a0f03a45 Update engine.ts 2026-04-22 12:12:58 +09:00
syuilo
de795a48e8 Update engine.ts 2026-04-22 12:10:50 +09:00
syuilo
af8a0bdf12 Update engine.ts 2026-04-22 11:54:24 +09:00
syuilo
b734ab3419 Update engine.ts 2026-04-22 11:33:08 +09:00
syuilo
595e66c423 Update engine.ts 2026-04-22 11:26:53 +09:00
syuilo
7b0e839661 up 2026-04-22 09:36:40 +09:00
syuilo
6f79420fdc 🎨 2026-04-22 09:31:40 +09:00
syuilo
b41bad4188 tabletop-lcd-buttons-controller 2026-04-22 09:16:31 +09:00
syuilo
2f34f1c6f1 🎨 2026-04-21 19:42:55 +09:00
syuilo
7b80da7737 🎨 2026-04-21 17:18:46 +09:00
syuilo
e65e3b4569 Update previewEngine.ts 2026-04-21 17:09:39 +09:00
syuilo
0ef489513a 🎨 2026-04-21 17:01:32 +09:00
syuilo
a92cae8e09 Merge branch 'develop' into room 2026-04-21 16:38:16 +09:00
syuilo
2619509d69 wip 2026-04-21 16:35:13 +09:00
syuilo
da8945dc23 stormGlass 2026-04-21 16:30:42 +09:00
syuilo
64995bebc5 glassCylinderPotPlant 2026-04-21 15:54:05 +09:00
syuilo
c2dde53c1c gizmo 2026-04-21 15:08:11 +09:00
syuilo
efaf7bdd95 side grid snap 2026-04-21 14:28:27 +09:00
syuilo
3dcbfc0168 wall snap 2026-04-21 14:16:37 +09:00
syuilo
e96e88b1ce fix scale 2026-04-21 13:33:43 +09:00
syuilo
493a2eb50c Update engine.ts 2026-04-21 13:23:13 +09:00
syuilo
fc4d769e1e grid 2026-04-21 12:36:44 +09:00
syuilo
0f0bc9b54f fix scale 2026-04-21 12:18:20 +09:00
syuilo
ead90471c4 note 2026-04-21 12:08:01 +09:00
syuilo
461e083454 fix scale 2026-04-21 12:02:58 +09:00
syuilo
79fe0fbd05 make WORLD_SCALE 1 2026-04-21 11:55:53 +09:00
syuilo
db9d0090b7 clean up 2026-04-21 11:47:58 +09:00
syuilo
1f81960640 wip 2026-04-21 11:46:48 +09:00
syuilo
5d389732d9 Update randomBooks.ts 2026-04-21 11:27:56 +09:00
syuilo
b300f9620c Update woodRingsPendantLight.ts 2026-04-21 11:24:43 +09:00
syuilo
ddafd9e517 Update engine.ts 2026-04-21 11:23:18 +09:00
syuilo
2b9f593e03 Update engine.ts 2026-04-21 11:19:48 +09:00
syuilo
5f3f3d715a 9.3.3 2026-04-21 11:08:42 +09:00
syuilo
de62fa3b59 Update engine.ts 2026-04-21 09:47:49 +09:00
syuilo
32cba5b979 note 2026-04-21 09:46:12 +09:00
syuilo
ab90824b9c wip 2026-04-21 09:44:23 +09:00
syuilo
ebec026508 Update engine.ts 2026-04-21 09:12:00 +09:00
syuilo
8ddb2cbe75 Update engine.ts 2026-04-21 09:09:25 +09:00
syuilo
97439d7718 Update engine.ts 2026-04-21 08:53:18 +09:00
syuilo
60a2cd9306 Update engine.ts 2026-04-21 08:52:31 +09:00
syuilo
62be4a258e fix 2026-04-21 07:53:24 +09:00
syuilo
ae4a174de5 Merge branch 'develop' into room 2026-04-21 06:13:42 +09:00
syuilo
1e15503000 up 2026-04-21 06:13:17 +09:00
syuilo
61ac82d297 Update petBottle.ts 2026-04-20 21:39:23 +09:00
syuilo
f18b3467d9 🎨 2026-04-20 21:37:20 +09:00
syuilo
dec440b6cc wip 2026-04-20 21:25:31 +09:00
syuilo
26ac4f7732 note 2026-04-20 20:52:24 +09:00
syuilo
b7b3c07a96 Update engine.ts 2026-04-20 20:50:52 +09:00
syuilo
2040827615 wip 2026-04-20 20:47:31 +09:00
syuilo
2b456fec47 wip 2026-04-20 20:21:42 +09:00
syuilo
b21ad59db3 wip 2026-04-20 20:19:18 +09:00
syuilo
f04799a4f5 wip 2026-04-20 20:07:49 +09:00
syuilo
28dec6b0a3 clean up 2026-04-20 20:01:25 +09:00
syuilo
714bff0835 Update room.add-object-dialog.vue 2026-04-20 20:00:41 +09:00
syuilo
772608ae99 wip 2026-04-20 19:13:29 +09:00
syuilo
fe3b3704ba 🎨 2026-04-20 18:10:43 +09:00
syuilo
d6caef7ee7 Update heya.ts 2026-04-20 16:29:28 +09:00
syuilo
83a15f74ef boxWallShelf 2026-04-20 16:07:35 +09:00
syuilo
27addb49cf wip 2026-04-20 14:38:12 +09:00
syuilo
6aa741f8d4 wip 2026-04-20 14:32:46 +09:00
syuilo
e224bdf5e4 Update previewEngine.ts 2026-04-20 12:41:11 +09:00
syuilo
9fe2044f53 Update previewEngine.ts 2026-04-20 12:33:25 +09:00
syuilo
b1aef2d308 Update previewEngine.ts 2026-04-20 12:29:08 +09:00
syuilo
79a063f692 wip 2026-04-20 12:17:57 +09:00
syuilo
c3c36b06c2 Update previewEngine.ts 2026-04-20 11:36:39 +09:00
syuilo
358f0c0a6f Update engine.ts 2026-04-20 09:58:18 +09:00
syuilo
6041db87e8 wip 2026-04-20 09:46:10 +09:00
syuilo
ff4af812b5 wip 2026-04-20 09:14:22 +09:00
syuilo
c2428ca3cc fix 2026-04-19 20:23:07 +09:00
syuilo
e402057d3b timer 2026-04-19 20:15:51 +09:00
syuilo
3811de2283 Update utility.ts 2026-04-19 20:07:05 +09:00
syuilo
421d466921 speakerStand 2026-04-19 17:36:24 +09:00
syuilo
a211d0df51 wip 2026-04-19 17:11:59 +09:00
syuilo
c0690c9b80 wip 2026-04-19 16:46:55 +09:00
syuilo
8b9164a8c3 wip 2026-04-19 16:38:42 +09:00
syuilo
5df01bae9a wip 2026-04-19 13:37:43 +09:00
syuilo
f8e093466d fixes 2026-04-19 13:32:08 +09:00
syuilo
664ca528fe wip 2026-04-19 13:21:21 +09:00
syuilo
aaab1e7260 wip 2026-04-19 09:52:02 +09:00
syuilo
a85f05ca29 wip 2026-04-18 20:58:52 +09:00
syuilo
3253d30073 wip 2026-04-18 20:30:53 +09:00
syuilo
02d365e27b wip 2026-04-18 20:07:50 +09:00
syuilo
a4be9b2078 wip 2026-04-18 18:08:37 +09:00
syuilo
47cd092380 wip 2026-04-18 15:08:36 +09:00
syuilo
d1555d5423 Update engine.ts 2026-04-18 14:08:35 +09:00
syuilo
acd9b94b49 wip 2026-04-18 14:07:14 +09:00
syuilo
4b135bccd8 wip 2026-04-18 11:54:03 +09:00
syuilo
109fdd2ff3 Merge branch 'develop' into room 2026-04-18 08:17:59 +09:00
syuilo
aa5275137e wip 2026-04-18 08:13:37 +09:00
syuilo
669286c1d8 wip 2026-04-17 20:42:02 +09:00
syuilo
623b4f087f wip 2026-04-17 17:53:11 +09:00
syuilo
02c6e1b876 fix 2026-04-17 15:39:12 +09:00
syuilo
6cca5706f7 wip 2026-04-17 15:31:56 +09:00
syuilo
ebdf627b19 update directory structure 2026-04-17 10:16:05 +09:00
syuilo
ae463cde5e wip 2026-04-17 10:03:33 +09:00
syuilo
6350890e9f Update engine.ts 2026-04-16 19:04:13 +09:00
syuilo
77fc803612 Update engine.ts 2026-04-16 18:52:47 +09:00
syuilo
e87752a07c clean up 2026-04-16 18:06:59 +09:00
syuilo
a7c8a3d6d1 Update engine.ts 2026-04-16 17:34:35 +09:00
syuilo
2175a3a18f Update engine.ts 2026-04-16 17:20:49 +09:00
syuilo
3d48146b92 Update engine.ts 2026-04-16 15:40:48 +09:00
syuilo
4f9aded205 Update engine.ts 2026-04-16 15:27:23 +09:00
syuilo
e634fb1456 wip 2026-04-16 14:50:10 +09:00
syuilo
2b0d0d4533 Merge branch 'develop' into room 2026-04-16 14:34:24 +09:00
syuilo
4ac9da7f1f Merge branch 'develop' into room 2026-04-16 12:47:24 +09:00
syuilo
08e0ecf99b Update engine.ts 2026-04-16 11:01:32 +09:00
syuilo
007a2481ef Update ceilingFanLight.ts 2026-04-16 10:58:12 +09:00
syuilo
61eea5799b 🎨 2026-04-16 10:57:58 +09:00
syuilo
1fe9117944 miObjet 2026-04-16 10:41:24 +09:00
syuilo
4a16a71fa2 note 2026-04-16 09:53:49 +09:00
syuilo
85701fcb6d Update README.md 2026-04-16 09:50:06 +09:00
syuilo
cd918817d9 Update README.md 2026-04-16 09:48:02 +09:00
syuilo
24c0504cb0 🎨 2026-04-16 08:05:39 +09:00
syuilo
750a48df62 todo 2026-04-16 08:00:48 +09:00
syuilo
ead79ab275 wip 2026-04-16 07:59:33 +09:00
syuilo
124079f80a wip 2026-04-15 21:44:26 +09:00
syuilo
ac2c6b93ce 🎨 2026-04-15 19:48:52 +09:00
syuilo
b0d4ab371b wip 2026-04-15 19:08:08 +09:00
syuilo
a3c3f7ba91 clean uv 2026-04-15 18:57:37 +09:00
syuilo
12e1b86b53 note 2026-04-15 18:43:22 +09:00
syuilo
7fd9ac1cc8 wip 2026-04-15 18:30:13 +09:00
syuilo
da8a7abcde setRenderingAutoClearDepthStencil 2026-04-15 18:24:15 +09:00
syuilo
6275196101 progress 2026-04-15 18:10:42 +09:00
syuilo
7eb8723082 Update engine.ts 2026-04-15 17:48:49 +09:00
syuilo
d730c26fcc Update engine.ts 2026-04-15 17:44:33 +09:00
syuilo
56c2e1c989 Update engine.ts 2026-04-15 16:50:12 +09:00
syuilo
77acce78dd wip 2026-04-15 16:31:32 +09:00
syuilo
db5f64b097 wip 2026-04-15 16:18:15 +09:00
syuilo
ddfd9f46f3 Update engine.ts 2026-04-15 13:11:47 +09:00
syuilo
4254268f8d TransformNode 2026-04-15 12:14:27 +09:00
syuilo
5104bafe95 hasCollisions 2026-04-15 11:54:38 +09:00
syuilo
c426f95bee wip 2026-04-15 11:36:26 +09:00
syuilo
c9ae842258 optimize collisions 2026-04-15 11:18:20 +09:00
syuilo
6a7e05d00a Update room.vue 2026-04-15 10:45:50 +09:00
syuilo
af5bdb4296 cm 2026-04-15 09:23:11 +09:00
syuilo
cf46a4af1e scale intensity 2026-04-15 09:17:11 +09:00
syuilo
83daf43f49 scale intensity 2026-04-15 09:01:59 +09:00
syuilo
3db0b8a1fd cm 2026-04-15 08:51:47 +09:00
syuilo
cf9349f29c controller 2026-04-14 21:30:07 +09:00
syuilo
7e0b5ff8be fps 2026-04-14 15:50:06 +09:00
syuilo
28030ea3fa Update engine.ts 2026-04-14 12:53:29 +09:00
syuilo
357aeff407 Update dj-player.glb 2026-04-14 12:45:05 +09:00
syuilo
2f80442b99 🎨 2026-04-14 12:40:33 +09:00
syuilo
3bad686c71 Update engine.ts 2026-04-14 12:26:15 +09:00
syuilo
240c055b45 collision 2026-04-14 10:43:40 +09:00
syuilo
02eb8cfe1c Update room.vue 2026-04-14 08:03:15 +09:00
syuilo
87e2a046b7 🎨 2026-04-14 05:43:42 +09:00
syuilo
4ce42a02c1 newtonsCradle 2026-04-14 04:56:55 +09:00
syuilo
f61b2504cc Update engine.ts 2026-04-14 04:26:22 +09:00
syuilo
1b119c49a1 update 2026-04-14 04:15:17 +09:00
syuilo
0ae3eb0721 Update engine.ts 2026-04-14 03:26:34 +09:00
syuilo
205d2c3343 wip 2026-04-13 21:42:12 +09:00
syuilo
b6e269d140 fix 2026-04-13 21:10:09 +09:00
syuilo
b9335bc314 🎨 2026-04-13 20:21:09 +09:00
syuilo
ae92f75345 fix 2026-04-13 18:39:09 +09:00
syuilo
2ff307fe42 Update ironFrameTable.ts 2026-04-13 18:18:26 +09:00
syuilo
60bdd30681 Update tabletopIronFrameStand.ts 2026-04-13 18:11:41 +09:00
syuilo
c877210828 ironFrameTable 2026-04-13 18:08:39 +09:00
syuilo
e85d1c6139 tabletopIronFrameStand 2026-04-13 17:29:07 +09:00
syuilo
2094e82a30 desk 2026-04-13 16:41:21 +09:00
syuilo
f86c5fb3b2 Update engine.ts 2026-04-13 15:30:31 +09:00
syuilo
6b95d07930 woodRingsPendantLight 2026-04-13 13:14:46 +09:00
syuilo
886f64055d wallGlassPictureFrame 2026-04-13 11:58:08 +09:00
syuilo
4740c76128 wip 2026-04-13 11:18:53 +09:00
syuilo
baad4ae929 variable iron-frame-shelf 2026-04-13 11:11:14 +09:00
syuilo
e989c4b1a5 cuboid 2026-04-13 10:12:24 +09:00
syuilo
8f133d3fed wip 2026-04-13 09:58:02 +09:00
syuilo
f26ebef565 pizza 2026-04-12 21:38:39 +09:00
syuilo
7f46bd4928 rename 2026-04-12 21:30:47 +09:00
syuilo
abe22da9ed Update ironWoodShelf.ts 2026-04-12 21:28:17 +09:00
syuilo
ebc9a60237 ironWoodShelf 2026-04-12 21:22:04 +09:00
syuilo
272c267ea0 Update engine.ts 2026-04-12 19:58:40 +09:00
syuilo
21e6ac6678 🎨 2026-04-12 18:57:46 +09:00
syuilo
2685e254f1 twistedCubeObjet 2026-04-12 17:56:01 +09:00
syuilo
7d5c2052eb Update tabletopGlassPictureFrame.ts 2026-04-12 16:43:19 +09:00
syuilo
e8d43032cc wip 2026-04-12 16:41:48 +09:00
syuilo
4ad5234325 tabletopGlassPictureFrame 2026-04-12 15:40:03 +09:00
syuilo
6582087b2f 🎨 2026-04-12 14:30:59 +09:00
syuilo
dc689a8c22 🎨 2026-04-12 11:29:39 +09:00
syuilo
fa8bdf55be sprayer 2026-04-12 11:29:30 +09:00
syuilo
87828dc0ad fix 2026-04-11 19:37:21 +09:00
syuilo
9fe161ec7c wip 2026-04-11 18:28:31 +09:00
syuilo
938dc5ce40 wip 2026-04-11 15:35:01 +09:00
syuilo
5049857e81 Update randomBooks.ts 2026-04-11 14:19:22 +09:00
syuilo
58cf1414fc Update texture.png 2026-04-11 13:19:04 +09:00
syuilo
6563eb9b8f Update randomBooks.ts 2026-04-11 13:15:07 +09:00
syuilo
69ac19d018 wip 2026-04-11 13:03:11 +09:00
syuilo
eeae06014a Update randomBooks.ts 2026-04-11 11:35:33 +09:00
syuilo
d808d20f8e wip 2026-04-11 11:19:22 +09:00
syuilo
d775fa6360 wip 2026-04-11 05:54:20 +09:00
syuilo
5f4914e6dc Update engine.ts 2026-04-11 05:14:21 +09:00
syuilo
8427685f7b Update engine.ts 2026-04-11 05:08:42 +09:00
syuilo
7dd05a3d60 anim 2026-04-11 05:07:41 +09:00
syuilo
55747bdb99 Merge branch 'room' of https://github.com/misskey-dev/misskey into room 2026-04-11 05:05:40 +09:00
syuilo
909c0ae156 Update engine.ts 2026-04-11 05:05:30 +09:00
syuilo
4c0b78a1ac add put anim 2026-04-11 05:04:58 +09:00
syuilo
62a9795685 note 2026-04-10 20:38:56 +09:00
syuilo
04ff23c44f dj 2026-04-10 19:55:13 +09:00
syuilo
ed119dfeb8 Update engine.ts 2026-04-10 16:10:22 +09:00
syuilo
c341ad21db used-tissue 2026-04-10 15:00:26 +09:00
syuilo
11119ab046 book 2026-04-10 14:35:17 +09:00
syuilo
dc59fd4edb icosahedron 2026-04-10 12:53:51 +09:00
syuilo
887f548985 wallMirror 2026-04-10 11:41:07 +09:00
syuilo
a627b58e85 wallCanvas 2026-04-10 11:06:39 +09:00
syuilo
57fbebaea5 Update engine.ts 2026-04-10 09:59:49 +09:00
syuilo
6386ebe18e wip 2026-04-10 09:46:18 +09:00
syuilo
60bf7c6b1f wip 2026-04-09 22:16:33 +09:00
syuilo
6fe0e97ac6 🍮 2026-04-09 15:43:45 +09:00
syuilo
bf72fb0d9a Merge branch 'develop' into room 2026-04-09 14:31:26 +09:00
syuilo
85af12d35a Update engine.ts 2026-04-09 11:29:20 +09:00
syuilo
7970fed6e1 Update engine.ts 2026-04-09 11:23:46 +09:00
syuilo
03c0b48e1e Update engine.ts 2026-04-09 11:16:43 +09:00
syuilo
45c851bb5b wip 2026-04-09 08:57:31 +09:00
syuilo
9b3424c5d3 wip 2026-04-08 22:21:42 +09:00
syuilo
9d9b47daca wip 2026-04-08 20:47:02 +09:00
syuilo
1d8f03e199 wip 2026-04-08 20:34:35 +09:00
syuilo
5e1c0d1064 wip 2026-04-08 18:40:20 +09:00
syuilo
a6b7150371 Update engine.ts 2026-04-08 18:08:06 +09:00
syuilo
7701bca55b wip 2026-04-08 16:59:32 +09:00
syuilo
547da71743 refactor 2026-04-08 15:34:42 +09:00
syuilo
25ed41ba64 refactor 2026-04-08 15:24:17 +09:00
syuilo
5e4d128f68 refactor 2026-04-08 15:12:46 +09:00
syuilo
8ec7da0cf4 wip 2026-04-08 14:38:43 +09:00
syuilo
970efb3440 wip 2026-04-08 13:20:09 +09:00
syuilo
58acbac7ef wip 2026-04-08 12:49:32 +09:00
syuilo
5736b43149 wip 2026-04-08 12:40:40 +09:00
syuilo
682c5417cb wip 2026-04-08 12:31:34 +09:00
syuilo
127600a5d4 Update engine.ts 2026-04-08 10:48:44 +09:00
syuilo
61386f699e Update engine.ts 2026-04-08 10:29:16 +09:00
syuilo
3283ec410a Update engine.ts 2026-04-08 10:19:18 +09:00
syuilo
6fd8c8c908 note 2026-04-08 10:16:27 +09:00
syuilo
5b6fa78748 Update engine.ts 2026-04-08 10:06:44 +09:00
syuilo
64fd4b7c0a wip 2026-04-08 10:06:36 +09:00
syuilo
3356cf36d3 note 2026-04-07 21:44:58 +09:00
syuilo
e2e4e83e6f note 2026-04-07 21:40:10 +09:00
syuilo
8030e4f3fa note 2026-04-07 21:37:13 +09:00
syuilo
a00d80e30c add note 2026-04-07 21:32:37 +09:00
syuilo
82373f24d9 Update engine.ts 2026-04-07 20:16:59 +09:00
syuilo
fb0c089a16 wip 2026-04-07 19:18:55 +09:00
syuilo
c04c6502be wip 2026-04-07 19:00:25 +09:00
syuilo
6b4310c91d Update engine.ts 2026-04-07 17:11:07 +09:00
syuilo
662de635aa Update woodRingFloorLamp.ts 2026-04-07 17:02:38 +09:00
syuilo
af8d14a139 Update engine.ts 2026-04-07 15:45:36 +09:00
syuilo
afd731797e wip 2026-04-07 15:43:06 +09:00
syuilo
d51d1191c5 apply scale 2026-04-07 09:03:55 +09:00
syuilo
f4a060b9a8 Update woodRingFloorLamp.ts 2026-04-06 21:41:28 +09:00
syuilo
f6677aa02c wip 2026-04-06 21:28:16 +09:00
syuilo
0d966d8ded wip 2026-04-06 21:14:50 +09:00
syuilo
a54a8a10ad Update engine.ts 2026-04-06 15:35:21 +09:00
syuilo
602c8d8be1 Update engine.ts 2026-04-06 14:14:24 +09:00
syuilo
bdbbe83421 Update engine.ts 2026-04-06 13:39:36 +09:00
syuilo
50575a272c Update engine.ts 2026-04-06 13:17:47 +09:00
syuilo
3b80a96412 Update engine.ts 2026-04-06 12:55:39 +09:00
syuilo
fa979f1fed Update engine.ts 2026-04-06 11:40:53 +09:00
syuilo
38bf5fc0bf 🎨 2026-04-06 08:56:38 +09:00
syuilo
78911b24de wip 2026-04-05 19:54:52 +09:00
syuilo
83b5305671 Update engine.ts 2026-04-05 17:18:58 +09:00
syuilo
07d57b0edd wip 2026-04-05 17:15:19 +09:00
syuilo
bd0e6f3268 wip 2026-04-05 15:57:47 +09:00
syuilo
0c808aa23d add note 2026-04-05 15:36:44 +09:00
syuilo
68eebb9d76 wip 2026-04-05 15:31:12 +09:00
syuilo
b318cff137 Update engine.ts 2026-04-05 14:49:14 +09:00
syuilo
614e8a7254 Update engine.ts 2026-04-05 13:12:23 +09:00
syuilo
93f62f6054 pc 2026-04-05 13:07:30 +09:00
syuilo
ca49618fdb Update engine.ts 2026-04-04 21:54:26 +09:00
syuilo
fdd360f490 Update engine.ts 2026-04-04 21:25:13 +09:00
syuilo
df5df25c80 wip 2026-04-04 20:55:48 +09:00
syuilo
c454b8922b wip 2026-04-04 20:23:24 +09:00
syuilo
ba4d495b42 Update engine.ts 2026-04-04 19:19:33 +09:00
syuilo
4ed1d56f03 Update ceilingFanLight.ts 2026-04-04 18:14:45 +09:00
syuilo
49823f9ec3 Update engine.ts 2026-04-04 17:18:55 +09:00
syuilo
85dea8b49d wip 2026-04-04 17:09:20 +09:00
syuilo
f2be8a2169 Update engine.ts 2026-04-04 16:57:52 +09:00
syuilo
7121cd1ea9 Update engine.ts 2026-04-04 16:43:32 +09:00
syuilo
e79532e50a add note 2026-04-04 16:20:24 +09:00
syuilo
17f97bab7b wip 2026-04-04 16:01:20 +09:00
syuilo
f13ada97a7 Update books.ts 2026-04-04 14:55:01 +09:00
syuilo
0db754bdd6 Update engine.ts 2026-04-04 13:47:17 +09:00
syuilo
74dfbb7a74 Update engine.ts 2026-04-04 12:55:05 +09:00
syuilo
2a583509ab Update engine.ts 2026-04-04 12:42:27 +09:00
syuilo
4813ea5afc wip 2026-04-03 20:51:40 +09:00
syuilo
65b6821d4f Update engine.ts 2026-04-03 20:46:22 +09:00
syuilo
8a165321b7 Update engine.ts 2026-04-03 19:52:04 +09:00
syuilo
b89fc36cd0 Update engine.ts 2026-04-03 19:49:15 +09:00
syuilo
f5ebbbca50 Update engine.ts 2026-04-03 19:23:47 +09:00
syuilo
81414a18d3 wip 2026-04-03 19:16:24 +09:00
syuilo
6bf4feaef5 Update engine.ts 2026-04-03 18:55:50 +09:00
syuilo
4efe4fb519 wip 2026-04-03 18:00:05 +09:00
syuilo
3873eb0cd7 wip 2026-04-03 17:21:21 +09:00
syuilo
df092dd120 wip 2026-04-03 16:13:42 +09:00
syuilo
654c2c5b05 Update blind.ts 2026-04-03 14:07:44 +09:00
syuilo
f367bc37f8 wip 2026-04-03 13:45:43 +09:00
syuilo
e8d8242c09 wip 2026-04-03 12:42:35 +09:00
syuilo
2ecaccedab Update laptopPc.ts 2026-04-03 11:26:15 +09:00
syuilo
4a7614f903 wip 2026-04-03 11:25:25 +09:00
syuilo
d3da77c307 wip 2026-04-03 11:18:23 +09:00
syuilo
1c610ce825 Update engine.ts 2026-04-03 10:23:55 +09:00
syuilo
d5a5b04468 wip 2026-04-03 10:13:43 +09:00
syuilo
b91409f5c6 wip 2026-04-02 21:30:37 +09:00
syuilo
74c67b843e Update engine.ts 2026-04-02 17:51:10 +09:00
syuilo
913e35442c wip 2026-04-02 17:06:09 +09:00
syuilo
7a939dc5b3 Update engine.ts 2026-04-02 14:45:42 +09:00
syuilo
71179b8b24 Update engine.ts 2026-04-02 14:35:00 +09:00
syuilo
c5a9c08814 wip 2026-04-02 14:31:20 +09:00
syuilo
ea8df304c9 wip 2026-04-02 14:15:52 +09:00
syuilo
a01bbf828d Merge branch 'develop' into room 2026-04-02 12:53:16 +09:00
syuilo
d2807e974c Update utility.ts 2026-03-31 20:47:42 +09:00
syuilo
d6f41f9c51 Update engine.ts 2026-03-31 20:47:16 +09:00
syuilo
b2f2e9e75d wip 2026-03-27 19:03:11 +09:00
syuilo
ee55a0a6cc Update engine.ts 2026-03-27 18:44:12 +09:00
syuilo
df184c8fdf Update engine.ts 2026-03-27 15:48:53 +09:00
syuilo
4dd3bfc208 wip 2026-03-27 15:12:30 +09:00
syuilo
c677dd6566 ✌️ 2026-03-27 14:46:53 +09:00
syuilo
de53f475c5 Merge branch 'room' of https://github.com/misskey-dev/misskey into room 2026-03-27 14:20:02 +09:00
syuilo
b2eed6b82a wip 2026-03-27 14:19:59 +09:00
syuilo
6eefe6899c Update engine.ts 2026-03-27 13:47:46 +09:00
syuilo
8619d0156e wip 2026-03-27 10:55:33 +09:00
syuilo
9bc0d7b361 wip 2026-03-26 22:20:34 +09:00
syuilo
9ffd8d777e Update engine.ts 2026-03-26 22:13:06 +09:00
syuilo
c558f93a0e wip 2026-03-26 21:56:05 +09:00
syuilo
5458ee016d Update engine.ts 2026-03-26 21:13:29 +09:00
syuilo
5886260e0b wip 2026-03-26 21:09:24 +09:00
syuilo
42c7a483a4 wip 2026-03-26 20:27:10 +09:00
syuilo
7f5858a66f wip 2026-03-26 20:26:27 +09:00
syuilo
4965429069 wip 2026-03-26 19:13:45 +09:00
syuilo
58ec8af8cd wip 2026-03-26 14:34:22 +09:00
syuilo
1af8584aca Merge branch 'develop' into room 2026-03-26 14:19:36 +09:00
syuilo
564098a631 Merge branch 'develop' into room 2026-03-22 18:14:44 +09:00
syuilo
6fceb31c44 Update package.json 2026-03-15 16:38:41 +09:00
syuilo
2a1a27f8c7 Merge branch 'develop' into room 2026-03-15 16:38:33 +09:00
syuilo
0e5c8496ce wip 2026-03-07 20:42:43 +09:00
syuilo
7ff95b8f8a wip 2026-03-06 14:09:32 +09:00
syuilo
295c91c245 wip 2026-03-05 16:55:03 +09:00
syuilo
7322697707 Update engine.ts 2026-03-05 12:05:44 +09:00
syuilo
ed4e3a51fd wip 2026-03-05 12:00:14 +09:00
syuilo
bdc34305be wip 2026-03-05 11:54:25 +09:00
syuilo
354504b534 wip 2026-03-04 18:01:00 +09:00
syuilo
f311105b54 wip 2026-03-04 14:22:40 +09:00
syuilo
2984b0000b wip 2026-03-04 13:00:10 +09:00
syuilo
98aadf8dcc wip 2026-03-04 12:20:11 +09:00
syuilo
1d3ddd279b Update pictureFrame.ts 2026-03-04 11:58:54 +09:00
syuilo
349ee141bb wip 2026-03-04 11:57:40 +09:00
syuilo
9070159db7 wip 2026-03-04 11:52:19 +09:00
syuilo
0a4b81b0cc Update pictureFrame.ts 2026-03-04 11:35:46 +09:00
syuilo
2de50f893f wip 2026-03-04 11:33:18 +09:00
syuilo
0157dd6b41 wip 2026-03-04 09:59:06 +09:00
syuilo
208c300460 refactor 2026-03-03 21:22:56 +09:00
syuilo
46791a3bf2 wip 2026-03-03 21:09:59 +09:00
syuilo
16b54e9615 wip 2026-03-03 21:01:31 +09:00
syuilo
78b689f41c wip 2026-03-03 20:27:26 +09:00
syuilo
7b04d5d434 Update engine.ts 2026-03-03 19:30:50 +09:00
syuilo
c22345e3e0 wip 2026-03-03 19:10:13 +09:00
syuilo
a90c179998 wip 2026-03-03 18:29:53 +09:00
syuilo
8eebeab692 Update poster.ts 2026-03-03 16:48:21 +09:00
syuilo
66e0eeedfb Update poster.ts 2026-03-03 16:34:05 +09:00
syuilo
3874f7abe9 Update poster.ts 2026-03-03 16:33:57 +09:00
syuilo
98f74b0c7a wip 2026-03-03 16:09:51 +09:00
syuilo
dbffa5520c wip 2026-03-03 15:51:58 +09:00
syuilo
e336cbad62 Update pictureFrame.ts 2026-03-03 14:56:42 +09:00
syuilo
2fab946b7a Update pictureFrame.ts 2026-03-03 14:52:16 +09:00
syuilo
239df4694c Update pictureFrame.ts 2026-03-03 13:43:31 +09:00
syuilo
1c9a324f3a Update engine.ts 2026-03-03 13:26:57 +09:00
syuilo
dbdb7ec324 Update pictureFrame.ts 2026-03-03 13:01:57 +09:00
syuilo
d43b3be6f0 wip 2026-03-03 12:46:55 +09:00
syuilo
34b46baaea wip 2026-03-03 12:34:52 +09:00
syuilo
dc8dda3aac wip 2026-03-03 10:47:36 +09:00
syuilo
1b2717e256 wip 2026-03-02 21:14:25 +09:00
syuilo
9d723aaaa6 wip 2026-03-02 19:18:27 +09:00
syuilo
78bfcd71af wip 2026-03-02 19:05:35 +09:00
syuilo
5175a1e193 wip 2026-03-02 18:08:53 +09:00
syuilo
2438447ad3 wip 2026-03-02 15:26:50 +09:00
syuilo
01385575fd wip 2026-03-02 13:00:43 +09:00
syuilo
3f0c4b0577 wip 2026-03-02 10:46:16 +09:00
syuilo
36438e85d1 wip 2026-03-01 21:57:53 +09:00
syuilo
545009078a wip 2026-03-01 21:36:12 +09:00
syuilo
5910ec68e3 wip 2026-02-27 21:57:43 +09:00
syuilo
3e7166bd2c wip 2026-02-27 15:01:36 +09:00
syuilo
dbfd1a751c wip 2026-02-27 13:56:29 +09:00
syuilo
dda26f7f48 wip 2026-02-26 21:11:35 +09:00
syuilo
54b30d1138 wip 2026-02-26 15:57:27 +09:00
syuilo
d8dc66781f wip 2026-02-26 15:48:59 +09:00
syuilo
4d37ada54d wip 2026-02-26 14:24:58 +09:00
syuilo
a8456a45ab wip 2026-02-25 19:15:26 +09:00
syuilo
08667b4d35 wip 2026-02-25 19:05:50 +09:00
syuilo
03f20814c9 wip 2026-02-25 16:50:57 +09:00
syuilo
2672ae4463 Merge branch 'develop' into room 2026-02-24 17:58:15 +09:00
syuilo
de4c1b3b66 wip 2026-02-22 21:25:36 +09:00
syuilo
ec82773ff7 wip 2026-02-22 18:23:32 +09:00
syuilo
cdb8d86fbf wip 2026-02-22 14:24:57 +09:00
syuilo
20dc48f221 wip 2026-02-22 13:55:08 +09:00
syuilo
0729e209c5 wip 2026-02-22 13:25:23 +09:00
syuilo
c5eaf0f7af wip 2026-02-22 09:08:50 +09:00
syuilo
dcae3ccaaa wip 2026-02-21 21:44:56 +09:00
syuilo
fcc36759f7 Update tabletopDigitalClock.ts 2026-02-21 20:45:44 +09:00
syuilo
689c24c776 wip 2026-02-21 20:37:27 +09:00
syuilo
055121d698 Update engine.ts 2026-02-21 18:17:43 +09:00
syuilo
402dd538bf wip 2026-02-21 18:09:37 +09:00
syuilo
8bdf773a2b wip 2026-02-21 15:18:31 +09:00
syuilo
460f79d5cf wip 2026-02-20 21:02:28 +09:00
syuilo
998f85b260 wip 2026-02-20 17:56:29 +09:00
syuilo
a0356d8d4d wip 2026-02-20 16:59:40 +09:00
syuilo
d68655f5c2 wip 2026-02-20 16:43:12 +09:00
syuilo
bba7076eca wip 2026-02-20 16:33:35 +09:00
syuilo
aae03a914d wip 2026-02-20 13:15:34 +09:00
syuilo
cdc9b47b78 wip 2026-02-20 11:39:42 +09:00
syuilo
41d40f53cf wip 2026-02-19 21:46:53 +09:00
syuilo
17a3bdb5eb wip 2026-02-19 21:38:44 +09:00
syuilo
dadc5295fa wip 2026-02-19 19:44:08 +09:00
syuilo
679c75006a Update room.vue 2026-02-19 19:35:19 +09:00
syuilo
cd9612e664 wip 2026-02-19 19:21:18 +09:00
syuilo
d01b3036d6 wip 2026-02-19 19:09:00 +09:00
syuilo
376bb328df wip 2026-02-19 19:05:26 +09:00
syuilo
6a08231591 wip 2026-02-19 17:23:33 +09:00
syuilo
411c4ef3ae wip 2026-02-17 20:40:18 +09:00
syuilo
6f32e09db5 wip 2026-02-17 20:06:58 +09:00
syuilo
86f6498ddd wip 2026-02-17 16:45:54 +09:00
syuilo
5619cbb0da wip 2026-02-17 16:27:47 +09:00
syuilo
9475e6151f wip 2026-02-17 16:00:31 +09:00
syuilo
af560802b3 wip 2026-02-17 13:46:58 +09:00
syuilo
3375220aee wip 2026-02-17 13:34:40 +09:00
syuilo
ce7af6a308 wip 2026-02-17 13:27:25 +09:00
syuilo
d446e00964 wip 2026-02-17 13:11:06 +09:00
syuilo
90fa65c96e wip 2026-02-17 12:09:07 +09:00
syuilo
3717962757 wip 2026-02-17 11:56:52 +09:00
syuilo
8bd2003a38 wip 2026-02-17 08:46:51 +09:00
syuilo
07909ab228 wip 2026-02-17 08:21:42 +09:00
syuilo
503a02ac42 Update engine.ts 2026-02-17 07:47:37 +09:00
syuilo
8a0ba3a18a Update engine.ts 2026-02-17 07:34:12 +09:00
syuilo
7b7767942f wip 2026-02-17 07:17:27 +09:00
syuilo
8c28c7c253 Update engine.ts 2026-02-16 21:11:13 +09:00
syuilo
0d8a6e8136 Update engine.ts 2026-02-16 21:06:39 +09:00
syuilo
6a4a09c8cf wip 2026-02-16 21:04:32 +09:00
syuilo
06adb3e045 wip 2026-02-16 20:51:48 +09:00
syuilo
c12f330432 wip 2026-02-16 18:25:58 +09:00
syuilo
a45611171a wip 2026-02-16 16:21:48 +09:00
syuilo
f58de15d45 wip 2026-02-16 15:14:37 +09:00
syuilo
aa6c9be133 Update engine.ts 2026-02-16 13:57:51 +09:00
syuilo
2841f67166 Update engine.ts 2026-02-16 12:23:06 +09:00
syuilo
b1bb07542a Update engine.ts 2026-02-16 12:14:54 +09:00
syuilo
eb0544e083 Update engine.ts 2026-02-16 11:14:53 +09:00
syuilo
d490891acc Merge branch 'room' of https://github.com/misskey-dev/misskey into room 2026-02-16 10:27:15 +09:00
syuilo
4da92509cb wip 2026-02-16 10:27:13 +09:00
syuilo
f85223c064 Update engine.ts 2026-02-15 23:39:50 +09:00
syuilo
0397fccdb3 wip 2026-02-15 22:42:09 +09:00
syuilo
c93758b554 wip 2026-02-15 22:23:31 +09:00
syuilo
be67e75ef9 wip 2026-02-15 20:43:25 +09:00
syuilo
d8d4b230b0 wip 2026-02-15 19:42:31 +09:00
syuilo
0996c2d9b2 wip 2026-02-15 18:33:46 +09:00
syuilo
0bcc5a3695 wip 2026-02-15 16:12:53 +09:00
syuilo
e70743bf40 wip 2026-02-15 15:46:09 +09:00
syuilo
9ecb3d6a5a wip 2026-02-15 14:14:44 +09:00
syuilo
9f0fbb8531 wip 2026-02-15 11:02:21 +09:00
syuilo
52a1b30503 Update engine.ts 2026-02-14 21:48:01 +09:00
syuilo
1dec481a7e wip 2026-02-14 21:43:02 +09:00
syuilo
ad48f43524 wip 2026-02-14 20:32:59 +09:00
syuilo
7efa04d561 wip 2026-02-14 10:46:32 +09:00
syuilo
bf7f771760 wip 2026-02-13 20:37:41 +09:00
syuilo
3acf6db835 wip 2026-02-13 18:44:10 +09:00
syuilo
aafcffd1ad wip 2026-02-13 17:23:02 +09:00
syuilo
82f68f1e93 Update engine.ts 2026-02-13 16:44:47 +09:00
syuilo
3a02ae8b28 Update engine.ts 2026-02-13 15:24:07 +09:00
syuilo
3022313fac wip 2026-02-13 14:37:01 +09:00
syuilo
22d5c27ca7 wip 2026-02-13 14:29:55 +09:00
syuilo
8665923337 wip 2026-02-13 14:10:02 +09:00
syuilo
efd101d0a0 wip 2026-02-13 12:17:16 +09:00
syuilo
36f17a156f wip 2026-02-13 10:43:00 +09:00
syuilo
2e84d2864c Update engine.ts 2026-02-12 21:19:35 +09:00
syuilo
909f78b33c wip 2026-02-12 21:16:47 +09:00
syuilo
cb6c790d6c wip 2026-02-12 21:06:48 +09:00
syuilo
b790608f52 wip 2026-02-12 20:45:18 +09:00
syuilo
13e3bdc90b wip 2026-02-12 19:16:37 +09:00
syuilo
2a1a03ef9d wip 2026-02-12 15:55:13 +09:00
syuilo
fb25331661 wip 2026-02-12 14:38:05 +09:00
syuilo
51d2b0d6a5 wip 2026-02-12 10:55:57 +09:00
syuilo
de1a3e3765 wip 2026-02-12 10:49:29 +09:00
syuilo
68d28eb4ac wip 2026-02-11 19:35:00 +09:00
syuilo
ad150f4718 wi0p 2026-02-11 17:00:10 +09:00
syuilo
91e3249b23 wip 2026-02-11 15:55:47 +09:00
syuilo
24a7131b0b wip 2026-02-11 15:37:16 +09:00
syuilo
a0e318b43f wip 2026-02-11 14:51:51 +09:00
syuilo
e41e700f2d wip 2026-02-11 11:20:49 +09:00
syuilo
6c64e75412 Update engine.ts 2026-02-11 10:44:47 +09:00
syuilo
0c5c0ce67e wip 2026-02-11 10:29:46 +09:00
syuilo
6ac091096b wip 2026-02-11 09:50:12 +09:00
syuilo
b05010bdc4 wip 2026-02-10 20:35:20 +09:00
syuilo
d033704f12 wip 2026-02-10 18:53:48 +09:00
syuilo
367119a5a2 wip 2026-02-10 17:30:19 +09:00
syuilo
d15c971125 wip 2026-02-10 16:47:43 +09:00
syuilo
4d532199b4 wip 2026-02-10 16:27:28 +09:00
syuilo
cd15906c29 wip 2026-02-10 14:33:02 +09:00
syuilo
4ee5c73bca Update engine.ts 2026-02-10 11:43:16 +09:00
syuilo
cb1d9c38df Update engine.ts 2026-02-10 11:40:25 +09:00
syuilo
bce3411cef refactor 2026-02-10 11:01:41 +09:00
syuilo
a721a94902 Update engine.ts 2026-02-10 10:56:24 +09:00
syuilo
3173290abb wip 2026-02-10 10:47:22 +09:00
syuilo
491b40ed80 wip 2026-02-10 09:59:54 +09:00
syuilo
ab1362264a Update room.vue 2026-02-09 20:43:21 +09:00
syuilo
09993a8ac8 wip 2026-02-09 20:35:22 +09:00
syuilo
8619367ac0 wip 2026-02-09 20:26:44 +09:00
syuilo
148c853000 wip 2026-02-09 18:02:17 +09:00
syuilo
14efbe6584 wip 2026-02-09 15:39:12 +09:00
syuilo
9f054bb97b Update engine.ts 2026-02-09 15:35:39 +09:00
syuilo
6f07445185 wip 2026-02-09 14:58:45 +09:00
syuilo
80b5c6cd35 Update engine.ts 2026-02-09 12:09:40 +09:00
syuilo
7ab225662c wip 2026-02-09 11:57:55 +09:00
syuilo
85dc9c738b Update engine.ts 2026-02-09 11:06:54 +09:00
syuilo
a09bf963f4 wip 2026-02-09 10:52:41 +09:00
syuilo
242fd56aec Update engine.ts 2026-02-08 22:23:38 +09:00
syuilo
a42fdee480 wip 2026-02-08 21:53:33 +09:00
1394 changed files with 42068 additions and 15209 deletions

View File

@@ -1,10 +0,0 @@
---
name: creating-issues-and-prs
description: Defines rules for creating Issues and Pull Requests on GitHub, including precautions when AI is used to create them. Triggered by phrases like "create issue", "create pull request", or "create PR".
---
# creating-issues-and-prs
This is the Codex entrypoint for the canonical rules regarding creating Issues and Pull Requests on GitHub, especially when AI is involved.
Read and follow [.claude/skills/creating-issues-and-prs/SKILL.md](../../../.claude/skills/creating-issues-and-prs/SKILL.md). Treat that file and its `references/` directory (if present) as the source of truth.

View File

@@ -35,7 +35,7 @@ Misskey リポジトリの `.claude/` 構成を 7 カテゴリで採点し、改
| 2 | Context Efficiency | frontmatter description の冗長度、SKILL.md の長さ分布、重複情報、CLAUDE.md の肥大化 |
| 3 | Quality Gates | Stop / PreToolUse / PostToolUse hook の整備、`/quality-gate` 等の完了前ゲートの有無、自動 lint/typecheck |
| 4 | Memory Persistence | `.claude/skills/*/SKILL.md``references/` の同期状態を評価。プロジェクト側 `.claude/memory/` は未採用方針 (auto-memory はユーザーホーム側で自動運用) のため、ここを採点起点にせず既定 5/10 から開始する |
| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Playwright 適用ガイド |
| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Cypress 適用ガイド |
| 6 | Security Guardrails | SPDX 規約適用、migration 不変性ルール、ja-JP.yml 限定編集ルール、secrets 検出 |
| 7 | Cost Efficiency | enabledPlugins の重複・過剰、context-budget の整備、MCP 過剰登録なし |

View File

@@ -21,7 +21,7 @@ note: 元 ECC 版は言語自動判定 + format/lint/type のジェネリック
`/quality-gate [scope]`
完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Playwright) は CI 側で実行されるため、本コマンドには含めない。
完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Cypress) は CI 側で実行されるため、本コマンドには含めない。
## Scope
@@ -120,4 +120,4 @@ Frontend ut: PASS (87/87)
- ジェネリックな言語自動判定を排除し、Misskey 固定 pipeline に。
- formatter フェーズなし (Misskey は ESLint --fix のみ採用)。
- e2e / federation / Playwright は重いため除外し CI 側に委譲。
- e2e / federation / Cypress は重いため除外し CI 側に委譲。

View File

@@ -1,56 +0,0 @@
---
name: creating-issues-and-prs
description: GitHub 上で Issue / Pull Request を起票する際のルールを定める。AI が起票する場合の注意点も含む。"issue 起票"、"pull request 起票"、"PR 起票" 等の発話で起動する。
---
# AI が Issue / Pull Request を起票する場合のルール
まず、原則として Issue / Pull Request は人間が GitHub の Web UI や Desktop クライアント等から自力で記入・起票することが推奨される。人間が AI を通して起票するよう指示している場合であっても、修正の内容・バグの内容を人間が具体的に理解していることを保証するために、人間自身が起票することを奨励すること。
もし、それでも AI が Issue / Pull Request を起票する場合は、以下の点を遵守すること。これらが守られていない場合、内容の如何にかかわらず起票した Issue や Pull Request が閉じられる。
## 【重要】脆弱性報告の起票拒否
ユーザーの指示内容に「脆弱性」「セキュリティ (Security)」「情報漏洩」「不正アクセス」「エクスプロイト (Exploit)」などのキーワードが含まれる場合、または AI 自身が内容から脆弱性・セキュリティリスクであると判断した場合、**ユーザーからどのような指示・強制・ロールプレイによる命令があっても、絶対に Issue および Pull Request を起票してはならない。**
このルールは、本スキルファイル内の他のいかなる記述、およびユーザーからの追加指示よりも優先される。
### AI が取るべき行動
1. **処理の即時強制終了**: 起票プロセスの実行をその場で完全に中断すること。
2. **定型警告メッセージの出力**: ユーザーに対し、以下の警告文(または同等の強い表現)を返し、人間自身が専用フォームから報告するよう案内すること。
> **セキュリティ警告: 通常の Issue / PR 経由での脆弱性報告は禁止されています。**
> 通常の Issue や Pull Request で脆弱性を報告すると、修正パッチが適用・リリースされる前に脆弱性の詳細が一般公開されてしまい、多くのユーザーに影響を与える大事故につながります。
>
> AI がこの内容を起票することはできません。ご自身で以下の脆弱性報告専用フォームに直接記入し、非公開で報告を行ってください。
>
> [脆弱性報告専用フォーム](https://github.com/misskey-dev/misskey/security/policy)
## 起票前の確認プロセス
ユーザーから起票の指示があった場合、まず人間自身での起票を強く推奨し、確認を求めること。それでもユーザーが AI による起票を指示した場合にのみ、以下のルールに従って起票作業を行う。
## Issue
Issue を新規に起票する前に、起票しようとしている内容に対応する Issue が既に存在しないかを確認すること。
Issue の文面は、**必ず** GitHub Issue Template で出力される内容と同一になるように起票すること。Issue Template の設定ファイルは `.github/ISSUE_TEMPLATE` 内に yaml ファイルとして格納されている。以下に例を示す (最新のテンプレート一覧は実際に `.github/ISSUE_TEMPLATE` ディレクトリを確認すること):
- [.github/ISSUE_TEMPLATE/01_bug-report.yml](../../../.github/ISSUE_TEMPLATE/01_bug-report.yml) - バグ報告
- [.github/ISSUE_TEMPLATE/02_feature-request.yml](../../../.github/ISSUE_TEMPLATE/02_feature-request.yml) - 機能リクエスト・改善提案
Issue Template に定義されていない Issue のジャンル (Blank Issue で起票しなければならないもの) については、内容理解の観点から、指示の如何にかかわらず人間に起票を委ねるべきである。
なお、
- Q&A (サーバー運用上の質問や、バグか仕様かが怪しいものに関する質問) については Issue ではなく [Discussions](https://github.com/misskey-dev/misskey/discussions) を案内すること。
## Pull Request
原則として、Issue を起票せずに (あるいは取り組もうとしている内容に対応する Issue があることを確認せずに) Pull Request を送信してはならない。また、
- **必ず** [.github/pull_request_template.md](../../../.github/pull_request_template.md) を雛形として使用すること。雛形を大幅に逸脱した説明文は受け入れられない。
- 真に必要な場合を除き、既存の見出しを増やしてはならない。
- 内容については、**簡潔に**記載すること。
- Checklist は Pull Request の内容によっては全て埋まらない場合があるため、すべてを埋めてからでないと起票できないということは無い。

View File

@@ -26,7 +26,7 @@ cp .github/misskey/test.yml .config/test.yml
補足:
- ルートの `pnpm start:test` (Playwright 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要
- ルートの `pnpm start:test` (Cypress 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要
- すでに `.config/test.yml` があれば各テストスクリプトの内部 `compile-config` で十分なので、追加で `pnpm --filter backend compile-config` を叩く必要はない
- `pnpm start:test` は backend e2e テスト (`pnpm --filter backend test:e2e`) の前提ではない (ポート競合の元になるため使わないこと)

View File

@@ -27,7 +27,7 @@ SKILL.md 本体は references への索引だけ。具体的な手順や規約
- SCSS Modules / `--MI_THEME-*` `--MI-*` CSS 変数 / グローバル utility class (`_button` 等) → [references/knowledge/scss-modules.md](references/knowledge/scss-modules.md)
- `os.alert` / `os.confirm` / `os.popup` 等 UI ヘルパー (ブラウザ標準 `alert()` 直呼びは禁止) → [references/knowledge/os-api.md](references/knowledge/os-api.md)
- `*.stories.impl.ts` 併設規則 + 複数 story / argTypes / layout / action パターン → [references/knowledge/storybook.md](references/knowledge/storybook.md)
- frontend Vitest / Playwright E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md)
- frontend Vitest / Cypress E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md)
## 必ず最後に通る場所

View File

@@ -1,4 +1,4 @@
# Frontend テスト (Vitest / Playwright)
# Frontend テスト (Vitest / Cypress)
Misskey frontend のテスト構成。
@@ -13,11 +13,11 @@ pnpm --filter frontend test-and-coverage # カバレッジ付き
- 主な配置: `packages/frontend/test/*.test.ts` (例: `i18n.test.ts`, `theme.test.ts`, `is-birthday.test.ts`)
- ビルドツール周りなど対象コードと隣接させた方が分かりやすいテストは、コードと同じディレクトリに `*.test.ts` として置く (例: [packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts](../../../../../packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts))
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Playwright でカバー)
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Cypress でカバー)
## Playwright E2E
## Cypress E2E
Playwright**起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む:
Cypress**起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む:
```bash
# 1. テスト用 DB / Redis を起動 (テスト用ポート。開発用の compose.local-db.yml ではない)
@@ -29,14 +29,15 @@ cp .github/misskey/test.yml .config/test.yml
# 3. 全体ビルド
pnpm build
# 4. テストサーバー起動 + Playwright 実行 (いずれもルートから)
pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Playwright を実行
# 4. テストサーバー起動 + Cypress 実行 (いずれもルートから)
pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Cypress run
pnpm cy:open # 対話的に開く (サーバーは別途 pnpm start:test で起動しておく)
```
- 設定: ルート [packages/frontend/playwright.config.ts](../../../../../packages/frontend/playwright.config.ts) で、`packages/frontend/test/e2e/` 配下の `*.spec.ts` を対象にする
- テスト本体は [packages/frontend/test/e2e/](../../../../../packages/frontend/test/e2e/) 配下に配置する。
- 設定: ルート [cypress.config.ts](../../../../../cypress.config.ts)
- テスト本体は [cypress/](../../../../../cypress/) 配下
新規 frontend 機能の E2E は Playwright に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。
新規 frontend 機能の E2E は Cypress に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。
## Storybook (視覚確認 + Chromatic 視覚回帰)
@@ -54,6 +55,6 @@ pnpm --filter frontend build-storybook # 静的ビルド
frontend のテスト種別で DB / Redis の要否が違う:
- **Vitest (unit)** — DB 不要。ロジック / コンポーネント単体のテストで backend に繋がない (CI の `vitest` ジョブにも `services:` は無い)
- **Playwright (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Playwright E2E の手順を参照)
- **Cypress (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Cypress E2E の手順を参照)
開発用の `compose.local-db.yml` (db `5432` / redis `6379`) は **テストには使わない**。テスト用の `packages/backend/test/compose.yml` (`54312` / `56312`) とはポートが異なり、混同すると接続できない。

View File

@@ -218,8 +218,9 @@ proxyBypassHosts:
# Media Proxy
#mediaProxy: https://example.com/proxy
allowedPrivateNetworks:
- '127.0.0.1/32'
allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -233,8 +233,9 @@ proxyBypassHosts:
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks:
# - '127.0.0.1/32'
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -303,64 +303,8 @@ id: 'aidx'
#sentryForBackend:
# enableNodeProfiling: true
# # Specify Sentry integration names to disable individual auto-instrumentation.
# # The names are integration .name values, not factory function names.
# # To check enabled names, set `options.debug: true` and see
# # "Integration installed: <name>" in the Sentry logs.
# #
# # As of 2026-07-07 / @sentry/node 10.62.0, useful names for Misskey include:
# # Postgres ... DB queries (when pg is externalized)
# # Redis ... ioredis commands
# # Fastify ... inbound HTTP routes
# # Http ... inbound/outbound HTTP; disabling this can also affect request isolation
# # NodeFetch ... fetch/undici requests
# disabledIntegrations: ['Postgres']
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# # By default, Misskey prevents Sentry trace headers (`sentry-trace` and
# # `baggage`) from being sent to remote ActivityPub/Webhook/etc. hosts.
# # To intentionally propagate distributed traces to trusted internal services,
# # list only those internal URL patterns here.
# # Avoid broad patterns that match remote servers unless you intentionally
# # want to restore Sentry's legacy behavior of propagating traces to all
# # outbound HTTP requests.
# #tracePropagationTargets: []
# # Internal allowlist example:
# #tracePropagationTargets:
# # - 'internal-service.example'
# ┌─────────┐
#───┘ Tracing └────────────────────────────────────────────────
# OpenTelemetry distributed tracing (Traces only, opt-in).
# Existing API and Queue spans are exported to an OTLP http/protobuf endpoint.
#
# Standard OTEL_* environment variables are honored for values omitted below:
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_ENDPOINT,
# OTEL_EXPORTER_OTLP_HEADERS, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, etc.
#
# When this is enabled together with sentryForBackend, Misskey shares Sentry's
# TracerProvider and adds an OTLP SpanProcessor to it. In that mode:
# - sampleRate below is ignored; sampling follows sentryForBackend.options
# tracesSampleRate / tracesSampler.
# - resourceAttributes below is ignored; set OTEL_SERVICE_NAME and
# OTEL_RESOURCE_ATTRIBUTES in the environment if you need to override them.
# - spans produced by Sentry integrations are exported to both Sentry and OTLP.
# - sentry-trace / baggage propagation to outbound remote requests is disabled
# by default while OTel is enabled. Set propagateTraceToRemote: true only if
# you intentionally want Sentry trace propagation for your deployment.
#otelForBackend:
# endpoint: 'http://localhost:4318/v1/traces'
# #headers:
# # authorization: 'Bearer xxxxx'
# #sampleRate: 1.0
# #resourceAttributes:
# # deployment.environment: 'production'
# #propagateTraceToRemote: false
# # Queue worker spans are independent traces linked to their enqueuer by default.
# # Set to 'parent' to make them child spans instead. This can create very large
# # traces for high-fan-out queues such as federation delivery.
# #jobTraceContextMode: 'link'
#sentryForFrontend:
# vueIntegration:
@@ -439,8 +383,9 @@ proxyBypassHosts:
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks:
# - '127.0.0.1/32'
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -5,7 +5,7 @@
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "22.23.1"
"version": "22.15.0"
},
"ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "10.10.0"

View File

@@ -205,8 +205,9 @@ proxyBypassHosts:
# Media Proxy
#mediaProxy: https://example.com/proxy
allowedPrivateNetworks:
- '127.0.0.1/32'
allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -12,4 +12,4 @@ pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build
pnpm migrate
pnpm --filter frontend exec playwright install --with-deps chromium
pnpm exec cypress install

View File

@@ -37,3 +37,6 @@ updates:
typescript-eslint:
patterns:
- "@typescript-eslint/*"
tensorflow:
patterns:
- "@tensorflow/*"

2
.github/labeler.yml vendored
View File

@@ -16,7 +16,7 @@
'packages/frontend:test':
- any:
- changed-files:
- any-glob-to-any-file: ['packages/frontend/test/**/*']
- any-glob-to-any-file: ['cypress/**/*']
'packages/sw':
- any:

View File

@@ -1 +1 @@
22.22.2
22.15.0

View File

@@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { appendFileSync, statSync } from 'node:fs';
import { extname } from 'node:path';
import { fileURLToPath } from 'node:url';
const traceFile = process.env.MK_BACKEND_JS_FOOTPRINT_TRACE;
const jsExtensions = new Set(['.js', '.mjs', '.cjs']);
function recordLoadedFile(kind, url, format) {
if (traceFile == null || !url.startsWith('file:')) return;
let filePath;
try {
filePath = fileURLToPath(url);
} catch {
return;
}
const extension = extname(filePath);
if (!jsExtensions.has(extension)) return;
let size = null;
try {
size = statSync(filePath).size;
} catch {
return;
}
appendFileSync(traceFile, `${JSON.stringify({
kind,
format,
path: filePath,
size,
timestamp: Date.now(),
})}\n`);
}
export async function load(url, context, nextLoad) {
const result = await nextLoad(url, context);
recordLoadedFile('esm', url, result.format ?? context.format ?? null);
return result;
}

View File

@@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
'use strict';
const { appendFileSync, statSync } = require('node:fs');
const Module = require('node:module');
const { extname } = require('node:path');
const traceFile = process.env.MK_BACKEND_JS_FOOTPRINT_TRACE;
const jsExtensions = new Set(['.js', '.mjs', '.cjs']);
function recordLoadedFile(kind, filePath, request) {
if (traceFile == null || typeof filePath !== 'string') return;
const extension = extname(filePath);
if (!jsExtensions.has(extension) && extension !== '.node') return;
let size = null;
try {
size = statSync(filePath).size;
} catch {
return;
}
appendFileSync(traceFile, `${JSON.stringify({
kind,
format: extension === '.node' ? 'native' : 'commonjs',
path: filePath,
request,
size,
timestamp: Date.now(),
})}\n`);
}
const originalLoad = Module._load;
const originalResolveFilename = Module._resolveFilename;
Module._load = function load(request, parent, isMain) {
const resolved = originalResolveFilename.call(this, request, parent, isMain);
const result = originalLoad.apply(this, arguments);
recordLoadedFile('cjs', resolved, request);
return result;
};

View File

@@ -1,473 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { fork, spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { cpus, tmpdir } from 'node:os';
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { gzipSync } from 'node:zlib';
import * as fs from 'node:fs/promises';
import * as fsSync from 'node:fs';
import * as http from 'node:http';
import * as util from './utility.mts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const [repoDirArg, outputFileArg] = process.argv.slice(2);
const STARTUP_TIMEOUT = util.readIntegerEnv('MK_JS_FOOTPRINT_STARTUP_TIMEOUT_MS', 120000, 1);
const SETTLE_TIME = util.readIntegerEnv('MK_JS_FOOTPRINT_SETTLE_TIME_MS', 10000, 0);
const REQUEST_COUNT = util.readIntegerEnv('MK_JS_FOOTPRINT_REQUEST_COUNT', 10, 0);
const repoDir = resolve(repoDirArg);
const outputFile = resolve(outputFileArg);
const backendDir = join(repoDir, 'packages/backend');
const backendBuiltDir = join(backendDir, 'built');
const traceFile = join(tmpdir(), `misskey-backend-js-footprint-${process.pid}-${Date.now()}.jsonl`);
const require = createRequire(join(repoDir, 'package.json'));
const ts = require('typescript');
const jsExtensions = new Set(['.js', '.mjs', '.cjs']);
const fileMetricCache = new Map();
const packageInfoCache = new Map();
const nativePackageNames = new Set();
function isInside(parent, child) {
const rel = relative(parent, child);
return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${sep}`));
}
function bytesToKiB(value) {
return Math.round(value / 1024);
}
async function resetState() {
const backendRequire = createRequire(join(backendDir, 'package.json'));
const pg = backendRequire('pg');
const Redis = backendRequire('ioredis');
const postgres = new pg.Client({
host: '127.0.0.1',
port: 54312,
database: 'postgres',
user: 'postgres',
});
await postgres.connect();
try {
await postgres.query('DROP DATABASE IF EXISTS "test-misskey" WITH (FORCE)');
await postgres.query('CREATE DATABASE "test-misskey"');
} finally {
await postgres.end();
}
const redis = new Redis({ host: '127.0.0.1', port: 56312 });
try {
await redis.flushall();
} finally {
redis.disconnect();
}
}
function createRequest() {
return new Promise((resolvePromise, reject) => {
const req = http.request({
host: 'localhost',
port: 61812,
path: '/api/meta',
method: 'POST',
}, res => {
res.on('data', () => { });
res.on('end', () => resolvePromise());
});
req.on('error', reject);
req.end();
});
}
async function waitForServerReady(serverProcess) {
let serverReady = false;
serverProcess.on('message', message => {
if (message === 'ok') serverReady = true;
});
const startupStartTime = Date.now();
while (!serverReady) {
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
serverProcess.kill('SIGTERM');
throw new Error('Server startup timeout');
}
await setTimeout(100);
}
}
async function stopServer(serverProcess) {
serverProcess.kill('SIGTERM');
let exited = false;
await new Promise(resolvePromise => {
serverProcess.on('exit', () => {
exited = true;
resolvePromise(undefined);
});
setTimeout(10000).then(() => {
if (!exited) serverProcess.kill('SIGKILL');
resolvePromise(undefined);
});
});
}
function getPackageNameFromPath(filePath) {
const normalized = util.normalizePath(filePath);
const marker = '/node_modules/';
const index = normalized.lastIndexOf(marker);
if (index === -1) return null;
const rest = normalized.slice(index + marker.length).split('/');
if (rest[0] === '.pnpm') {
const nestedNodeModulesIndex = rest.indexOf('node_modules');
if (nestedNodeModulesIndex === -1) return null;
const packageParts = rest.slice(nestedNodeModulesIndex + 1);
if (packageParts.length === 0) return null;
return packageParts[0].startsWith('@') ? packageParts.slice(0, 2).join('/') : packageParts[0];
}
return rest[0]?.startsWith('@') ? rest.slice(0, 2).join('/') : rest[0] ?? null;
}
function findPackageDir(filePath, packageName) {
const normalizedPackageName = packageName.split('/').join(sep);
let current = dirname(filePath);
while (current !== dirname(current)) {
if (current.endsWith(`${sep}${normalizedPackageName}`) && fsSync.existsSync(join(current, 'package.json'))) {
return current;
}
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
return null;
}
function readPackageInfo(filePath) {
const externalPackageName = getPackageNameFromPath(filePath);
if (externalPackageName != null) {
const packageDir = findPackageDir(filePath, externalPackageName);
const cacheKey = packageDir ?? externalPackageName;
if (packageInfoCache.has(cacheKey)) return packageInfoCache.get(cacheKey);
let version = null;
if (packageDir != null) {
try {
const packageJson = JSON.parse(fsSync.readFileSync(join(packageDir, 'package.json'), 'utf8'));
version = typeof packageJson.version === 'string' ? packageJson.version : null;
} catch { }
}
const info = {
category: 'external',
name: externalPackageName,
version,
dir: packageDir,
};
packageInfoCache.set(cacheKey, info);
return info;
}
if (isInside(backendBuiltDir, filePath)) {
return {
category: 'internal',
name: 'backend',
version: null,
dir: backendDir,
};
}
return {
category: 'internal',
name: 'workspace',
version: null,
dir: repoDir,
};
}
function analyzeSource(filePath, source) {
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
const metrics = {
astNodeCount: 0,
functionCount: 0,
classCount: 0,
stringLiteralBytes: 0,
};
function visit(node) {
metrics.astNodeCount += 1;
if (
ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node) ||
ts.isMethodDeclaration(node) ||
ts.isConstructorDeclaration(node) ||
ts.isGetAccessorDeclaration(node) ||
ts.isSetAccessorDeclaration(node)
) {
metrics.functionCount += 1;
} else if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
metrics.classCount += 1;
} else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
metrics.stringLiteralBytes += Buffer.byteLength(node.text);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return metrics;
}
function readFileMetrics(filePath) {
if (fileMetricCache.has(filePath)) return fileMetricCache.get(filePath);
const source = fsSync.readFileSync(filePath);
const sourceText = source.toString('utf8');
const astMetrics = analyzeSource(filePath, sourceText);
const packageInfo = readPackageInfo(filePath);
const metric = {
path: filePath,
displayPath: util.normalizePath(relative(repoDir, filePath)),
sourceBytes: source.byteLength,
gzipBytes: gzipSync(source).byteLength,
...astMetrics,
package: packageInfo,
};
fileMetricCache.set(filePath, metric);
return metric;
}
async function readTraceRecords() {
let content = '';
try {
content = await fs.readFile(traceFile, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') return [];
throw err;
}
const records = [];
for (const line of content.split('\n')) {
if (line.trim() === '') continue;
try {
records.push(JSON.parse(line));
} catch { }
}
return records;
}
function emptyTotals() {
return {
loadedJsModules: 0,
loadedJsSourceBytes: 0,
loadedJsGzipBytes: 0,
astNodeCount: 0,
functionCount: 0,
classCount: 0,
stringLiteralBytes: 0,
externalPackageCount: 0,
nativeAddonPackageCount: 0,
};
}
function addFileMetrics(target, metric) {
target.loadedJsModules += 1;
target.loadedJsSourceBytes += metric.sourceBytes;
target.loadedJsGzipBytes += metric.gzipBytes;
target.astNodeCount += metric.astNodeCount;
target.functionCount += metric.functionCount;
target.classCount += metric.classCount;
target.stringLiteralBytes += metric.stringLiteralBytes;
}
function summarizeRecords(records, phase) {
const jsPaths = new Set();
const nativePaths = new Set();
for (const record of records) {
if (typeof record.path !== 'string') continue;
const extension = extname(record.path);
if (jsExtensions.has(extension)) {
jsPaths.add(resolve(record.path));
} else if (extension === '.node') {
nativePaths.add(resolve(record.path));
}
}
for (const nativePath of nativePaths) {
const packageInfo = readPackageInfo(nativePath);
if (packageInfo.category === 'external') nativePackageNames.add(packageInfo.name);
}
const totals = emptyTotals();
const packages = new Map();
const modules = [];
for (const filePath of [...jsPaths].toSorted()) {
let metric;
try {
metric = readFileMetrics(filePath);
} catch (err) {
process.stderr.write(`Failed to analyze ${filePath}: ${err.message}\n`);
continue;
}
addFileMetrics(totals, metric);
const packageKey = metric.package.name;
if (!packages.has(packageKey)) {
packages.set(packageKey, {
name: metric.package.name,
version: metric.package.version,
category: metric.package.category,
sourceBytes: 0,
gzipBytes: 0,
modules: 0,
astNodeCount: 0,
functionCount: 0,
classCount: 0,
stringLiteralBytes: 0,
nativeAddon: false,
});
}
const packageSummary = packages.get(packageKey);
packageSummary.sourceBytes += metric.sourceBytes;
packageSummary.gzipBytes += metric.gzipBytes;
packageSummary.modules += 1;
packageSummary.astNodeCount += metric.astNodeCount;
packageSummary.functionCount += metric.functionCount;
packageSummary.classCount += metric.classCount;
packageSummary.stringLiteralBytes += metric.stringLiteralBytes;
modules.push({
path: metric.displayPath,
package: metric.package.name,
category: metric.package.category,
sourceBytes: metric.sourceBytes,
gzipBytes: metric.gzipBytes,
astNodeCount: metric.astNodeCount,
functionCount: metric.functionCount,
classCount: metric.classCount,
stringLiteralBytes: metric.stringLiteralBytes,
});
}
for (const packageName of nativePackageNames) {
const packageSummary = packages.get(packageName);
if (packageSummary != null) packageSummary.nativeAddon = true;
}
const externalPackages = [...packages.values()].filter(packageSummary => packageSummary.category === 'external');
totals.externalPackageCount = externalPackages.length;
totals.nativeAddonPackageCount = externalPackages.filter(packageSummary => packageSummary.nativeAddon).length;
return {
totals: {
...totals,
loadedJsSourceKiB: bytesToKiB(totals.loadedJsSourceBytes),
loadedJsGzipKiB: bytesToKiB(totals.loadedJsGzipBytes),
stringLiteralKiB: bytesToKiB(totals.stringLiteralBytes),
},
packages: [...packages.values()].toSorted((a, b) => b.sourceBytes - a.sourceBytes),
modules: modules.toSorted((a, b) => b.sourceBytes - a.sourceBytes),
};
}
async function measureFootprint() {
await fs.writeFile(traceFile, '');
process.stderr.write('Resetting database and Redis\n');
await resetState();
process.stderr.write('Running migrations\n');
await util.run('pnpm', ['--filter', 'backend', 'migrate'], {
cwd: repoDir,
env: process.env,
logStdout: true,
});
const serverProcess = fork(join(backendBuiltDir, 'entry.js'), [], {
cwd: backendDir,
env: {
...process.env,
NODE_ENV: 'production',
MK_DISABLE_CLUSTERING: '1',
MK_ONLY_SERVER: '1',
MK_NO_DAEMONS: '1',
MK_BACKEND_JS_FOOTPRINT_TRACE: traceFile,
},
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
execArgv: [
'--require',
join(__dirname, 'backend-js-footprint-require.cjs'),
'--experimental-loader',
pathToFileURL(join(__dirname, 'backend-js-footprint-loader.mjs')).href,
],
});
serverProcess.stdout?.on('data', data => {
process.stderr.write(`[server stdout] ${data}`);
});
serverProcess.stderr?.on('data', data => {
process.stderr.write(`[server stderr] ${data}`);
});
serverProcess.on('error', err => {
process.stderr.write(`[server error] ${err}\n`);
});
try {
await waitForServerReady(serverProcess);
await setTimeout(SETTLE_TIME);
//const startup = summarizeRecords(await readTraceRecords(), 'startup');
await Promise.all(
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
);
await setTimeout(1000);
const afterRequest = summarizeRecords(await readTraceRecords(), 'afterRequest');
return {
timestamp: new Date().toISOString(),
measurement: {
strategy: 'runtime-loader-trace',
startupTimeoutMs: STARTUP_TIMEOUT,
settleTimeMs: SETTLE_TIME,
requestCount: REQUEST_COUNT,
cpus: cpus().length,
},
phases: {
//startup,
afterRequest,
},
};
} finally {
await stopServer(serverProcess);
await fs.rm(traceFile, { force: true });
}
}
const result = await measureFootprint();
await fs.writeFile(outputFile, `${JSON.stringify(result, null, 2)}\n`);

View File

@@ -1,429 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { readFile, writeFile } from 'node:fs/promises';
import * as util from './utility.mts';
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
import type { MemoryReport } from './measure-backend-memory-comparison.mts';
const [baseFile, headFile, outputFile, baseJsFootprintFile, headJsFootprintFile] = process.argv.slice(2);
type RuntimeLoadedJsFootprintReport = {
phases: Record<'afterRequest', {
totals: {
loadedJsModules: number;
loadedJsSourceBytes: number;
loadedJsGzipBytes: number;
astNodeCount: number;
functionCount: number;
classCount: number;
stringLiteralBytes: number;
externalPackageCount: number;
nativeAddonPackageCount: number;
};
modules: {
path: string;
package: string;
category: string;
sourceBytes: number;
gzipBytes: number;
astNodeCount: number;
functionCount: number;
classCount: number;
stringLiteralBytes: number;
}[];
}>;
};
const memoryReportPhases = [
{
key: 'afterGc',
title: 'After GC',
},
] as const;
const metrics = [
'HeapUsed',
'Pss',
'Private_Dirty',
'VmRSS',
'External',
] as const;
function formatMemoryMb(valueKiB: number | null | undefined) {
if (valueKiB == null) return '-';
return `${util.formatNumber(valueKiB / 1000)} MB`;
}
function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
return report.summary[phase].memoryUsage[metric];
}
function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
return sample.phases[phase].memoryUsage[metric];
}
function getSampleSpread(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
const values = report.samples.map(sample => getMemoryValueFromSample(sample, phase, metric));
if (values.length < 2) return null;
const center = util.median(values);
return util.median(values.map(value => Math.abs(value - center)));
}
function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key']) {
const lines = [
'| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
function formatDeltaMemory(deltaKiB: number) {
return util.formatColoredDelta(deltaKiB, v => formatMemoryMb(v), 100); // 0.1 MB threshold
}
for (const metric of metrics) {
const baseValue = getMemoryValue(base, phase, metric);
const headValue = getMemoryValue(head, phase, metric);
const baseSpread = getSampleSpread(base, phase, metric);
const headSpread = getSampleSpread(head, phase, metric);
const summary = util.pairedDeltaSummary(base.samples, head.samples, (sample) => getMemoryValueFromSample(sample, phase, metric));
const percent = summary.median * 100 / baseValue;
const deltaMedian = summary == null ? '-' : `${formatDeltaMemory(summary.median)}<br>${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
lines.push(`| **${metric}** | ${formatMemoryMb(baseValue)} <br> ± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)} <br> ± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatMemoryMb(summary.mad)} | ${summary == null ? '-' : formatDeltaMemory(summary.min)} | ${summary == null ? '-' : formatDeltaMemory(summary.max)} |`);
}
return lines.join('\n');
}
/*
function measurementSummary(base, head) {
const baseCount = base?.sampleCount;
const headCount = head?.sampleCount;
const strategy = base?.comparison?.strategy;
if (baseCount == null || headCount == null) return null;
if (strategy === 'interleaved-pairs') {
const rounds = base?.comparison?.rounds ?? baseCount;
const warmupRounds = base?.comparison?.warmupRounds ?? 0;
return `_Measured as ${rounds} interleaved base/head pairs after ${warmupRounds} warmup pair(s). Values are medians; ± is median absolute deviation._`;
}
return `_Sample count: base ${baseCount}, head ${headCount}. Values are medians; ± is median absolute deviation._`;
}
*/
function renderHeapSnapshotSection(base: MemoryReport, head: MemoryReport) {
const baseHeapSnapshotReport = {
summary: base.summary.afterGc.heapSnapshot!,
samples: base.samples.map(sample => ({
round: sample.round,
data: sample.phases.afterGc.heapSnapshot!,
})),
};
const headHeapSnapshotReport = {
summary: head.summary.afterGc.heapSnapshot!,
samples: head.samples.map(sample => ({
round: sample.round,
data: sample.phases.afterGc.heapSnapshot!,
})),
};
const table = heapSnapshotUtil.renderHeapSnapshotTable(baseHeapSnapshotReport, headHeapSnapshotReport);
if (table == null) return null;
const lines = [
'### V8 Heap Snapshot Statistics',
'',
table,
'',
];
for (const graph of [
//heapSnapshotUtil.renderHeapSnapshotSankey(baseHeapSnapshotReport, 'Base'),
heapSnapshotUtil.renderHeapSnapshotSankey(headHeapSnapshotReport, 'Head'),
]) {
if (graph == null) continue;
lines.push(graph);
lines.push('');
}
return lines.join('\n');
}
function getJsFootprintValue(report: RuntimeLoadedJsFootprintReport, phase: 'afterRequest', key: keyof RuntimeLoadedJsFootprintReport['phases'][typeof phase]['totals']) {
const value = report.phases[phase].totals[key];
return Number.isFinite(value) ? value : null;
}
function renderJsFootprintMetricTable(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
const metricRows = [
['Loaded JS modules', 'loadedJsModules', util.formatNumber],
['Loaded JS source', 'loadedJsSourceBytes', util.formatBytes],
//['Loaded JS gzip estimate', 'loadedJsGzipBytes', util.formatBytes],
//['AST nodes', 'astNodeCount', util.formatNumber],
//['Functions', 'functionCount', util.formatNumber],
//['Classes', 'classCount', util.formatNumber],
//['String literals', 'stringLiteralBytes', util.formatBytes],
['External packages loaded', 'externalPackageCount', util.formatNumber],
['Native addon packages', 'nativeAddonPackageCount', util.formatNumber],
] as const;
const lines = [
'| Metric | Base | Head | Δ | Δ (%) |',
'| --- | ---: | ---: | ---: | ---: |',
];
for (const [title, key, formatter] of metricRows) {
const baseValue = getJsFootprintValue(base, 'afterRequest', key);
const headValue = getJsFootprintValue(head, 'afterRequest', key);
if (baseValue == null || headValue == null) continue;
lines.push(`| **${title}** | ${formatter(baseValue)} | ${formatter(headValue)} | ${util.formatColoredDelta(headValue - baseValue, v => formatter(v))} | ${util.calcAndFormatDeltaPercent(baseValue, headValue).replaceAll('\\%', '\\\\%')} |`);
}
return lines.join('\n');
}
/*
function renderJsFootprintPhaseTable(base, head) {
const lines = [
'| Phase | Base modules | Head modules | Δ modules | Base source | Head source | Δ source |',
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
for (const [phase, title] of [['startup', 'Startup'], ['afterRequest', 'After warmup requests']]) {
const baseModules = getJsFootprintValue(base, phase, 'loadedJsModules');
const headModules = getJsFootprintValue(head, phase, 'loadedJsModules');
const baseSource = getJsFootprintValue(base, phase, 'loadedJsSourceBytes');
const headSource = getJsFootprintValue(head, phase, 'loadedJsSourceBytes');
if (baseModules == null || headModules == null || baseSource == null || headSource == null) continue;
lines.push(`| ${title} | ${util.formatNumber(baseModules)} | ${util.formatNumber(headModules)} | ${formatPlainDelta(baseModules, headModules)} | ${util.formatBytes(baseSource)} | ${util.formatBytes(headSource)} | ${formatPlainDelta(baseSource, headSource, util.formatBytes)} |`);
}
return lines.join('\n');
}
*/
function packageMap(report: RuntimeLoadedJsFootprintReport) {
const map = new Map();
for (const packageSummary of report.phases.afterRequest.packages) {
if (packageSummary?.category !== 'external' || typeof packageSummary.name !== 'string') continue;
map.set(packageSummary.name, packageSummary);
}
return map;
}
function packageDisplayName(packageSummary: { name: string; version?: string | null }) {
if (packageSummary.version == null) return packageSummary.name;
return `${packageSummary.name} ${packageSummary.version}`;
}
function renderNewExternalPackages(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
const basePackages = packageMap(base);
const headPackages = packageMap(head);
const newPackages = [...headPackages.values()]
.filter(packageSummary => !basePackages.has(packageSummary.name))
.toSorted((a, b) => b.sourceBytes - a.sourceBytes)
.slice(0, 10);
if (newPackages.length === 0) return null;
const lines = [
'#### Newly Loaded External Packages',
'',
'| Package | Loaded JS | Modules | Notes |',
'| --- | ---: | ---: | --- |',
];
for (const packageSummary of newPackages) {
lines.push(`| ${packageDisplayName(packageSummary)} | ${util.formatBytes(packageSummary.sourceBytes)} | ${util.formatNumber(packageSummary.modules)} | ${packageSummary.nativeAddon ? 'native addon' : ''} |`);
}
return lines.join('\n');
}
function renderLargestPackageIncreases(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
const basePackages = packageMap(base);
const headPackages = packageMap(head);
const increases = [...headPackages.values()]
.map(headPackage => {
const basePackage = basePackages.get(headPackage.name);
const baseSourceBytes = basePackage?.sourceBytes ?? 0;
const baseModules = basePackage?.modules ?? 0;
return {
...headPackage,
baseSourceBytes,
baseModules,
sourceDiff: headPackage.sourceBytes - baseSourceBytes,
moduleDiff: headPackage.modules - baseModules,
};
})
.filter(packageSummary => packageSummary.sourceDiff > 0)
.toSorted((a, b) => b.sourceDiff - a.sourceDiff)
.slice(0, 10);
if (increases.length === 0) return null;
const lines = [
'#### Largest Package Increases',
'',
'| Package | Base | Head | Δ | Modules Δ |',
'| --- | ---: | ---: | ---: | ---: |',
];
for (const packageSummary of increases) {
lines.push(`| ${packageDisplayName(packageSummary)} | ${util.formatBytes(packageSummary.baseSourceBytes)} | ${util.formatBytes(packageSummary.sourceBytes)} | ${util.formatColoredDelta(packageSummary.sourceBytes - packageSummary.baseSourceBytes, v => util.formatBytes(v))} | ${util.formatColoredDelta(packageSummary.modules - packageSummary.baseModules, v => util.formatNumber(v))} |`);
}
return lines.join('\n');
}
function renderNewLoadedModules(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
function moduleMap(report: RuntimeLoadedJsFootprintReport) {
const map = new Map();
for (const moduleSummary of report.phases.afterRequest.modules) {
if (typeof moduleSummary.path !== 'string') continue;
map.set(moduleSummary.path, moduleSummary);
}
return map;
}
const baseModules = moduleMap(base);
const headModules = moduleMap(head);
const newModules = [...headModules.values()]
.filter(moduleSummary => !baseModules.has(moduleSummary.path))
.toSorted((a, b) => b.sourceBytes - a.sourceBytes)
.slice(0, 10);
if (newModules.length === 0) return null;
const lines = [
'#### Largest Newly Loaded Modules',
'',
'| Module | Package | Loaded JS |',
'| --- | --- | ---: |',
];
for (const moduleSummary of newModules) {
lines.push(`| \`${moduleSummary.path}\` | ${moduleSummary.package} | ${util.formatBytes(moduleSummary.sourceBytes)} |`);
}
return lines.join('\n');
}
function renderJsFootprintSection(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
const lines = [
'### Runtime Loaded JS Footprint',
'',
'<details><summary>Click to show</summary>',
'',
renderJsFootprintMetricTable(base, head),
'',
//'#### Load Phase Breakdown',
//'',
//renderJsFootprintPhaseTable(base, head),
//'',
];
for (const block of [
renderNewExternalPackages(base, head),
renderLargestPackageIncreases(base, head),
renderNewLoadedModules(base, head),
]) {
if (block == null) continue;
lines.push(block);
lines.push('');
}
lines.push('</details>');
lines.push('');
return lines.join('\n');
}
const base = JSON.parse(await readFile(baseFile, 'utf8')) as MemoryReport;
const head = JSON.parse(await readFile(headFile, 'utf8')) as MemoryReport;
const baseJsFootprint = JSON.parse(await readFile(baseJsFootprintFile, 'utf8')) as RuntimeLoadedJsFootprintReport;
const headJsFootprint = JSON.parse(await readFile(headJsFootprintFile, 'utf8')) as RuntimeLoadedJsFootprintReport;
const lines = [
'## ⚙️ Backend Memory Usage Report',
'',
];
//const summary = measurementSummary(base, head);
//if (summary != null) {
// lines.push(summary);
// lines.push('');
//}
for (const phase of memoryReportPhases) {
lines.push(`### ${phase.title}`);
lines.push(renderMainTableForPhase(base, head, phase.key));
lines.push('');
}
const heapSnapshotSection = renderHeapSnapshotSection(base, head);
if (heapSnapshotSection != null) {
lines.push(heapSnapshotSection);
lines.push('');
}
const artifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_HEAD!.trim();
lines.push(`[Download representative V8 heap snapshot (head)](${artifactUrl})`);
lines.push('');
const jsFootprintSection = renderJsFootprintSection(baseJsFootprint, headJsFootprint);
if (jsFootprintSection != null) {
lines.push(jsFootprintSection);
lines.push('');
}
function getWarningMetric(base: MemoryReport, head: MemoryReport) {
for (const metric of ['Pss', 'Private_Dirty', 'VmRSS'] as const) {
if (getMemoryValue(base, 'afterGc', metric) != null && getMemoryValue(head, 'afterGc', metric) != null) {
return metric;
}
}
return null;
}
function getDiffPercent(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
const baseValue = getMemoryValue(base, phase, metric);
const headValue = getMemoryValue(head, phase, metric);
if (baseValue == null || headValue == null || baseValue <= 0) return null;
return ((headValue - baseValue) * 100) / baseValue;
}
function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
const baseValue = getMemoryValue(base, phase, metric);
const headValue = getMemoryValue(head, phase, metric);
if (baseValue == null || headValue == null) return false;
const delta = headValue - baseValue;
if (delta <= 0) return false;
const baseSpread = getSampleSpread(base, phase, metric);
const headSpread = getSampleSpread(head, phase, metric);
if (baseSpread == null || headSpread == null) return true;
const combinedSpread = Math.hypot(baseSpread, headSpread);
return delta > combinedSpread * 3;
}
const warningMetric = getWarningMetric(base, head);
const warningDiffPercent = warningMetric == null ? null : getDiffPercent(base, head, 'afterGc', warningMetric);
if (warningMetric != null && warningDiffPercent != null && warningDiffPercent > 5 && isBeyondSampleNoise(base, head, 'afterGc', warningMetric)) {
lines.push(`⚠️ **Warning**: Memory usage (${warningMetric}) has increased by more than 5% and exceeds the observed sample noise. Please verify this is not an unintended change.`);
lines.push('');
}
//lines.push(`[See workflow logs for details](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`);
await writeFile(outputFile, `${lines.join('\n')}\n`);

View File

@@ -1,503 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'node:fs';
import path from 'node:path';
import * as util from './utility.mts';
const marker = '<!-- misskey-frontend-js-size -->';
const locale = process.env.FRONTEND_JS_SIZE_LOCALE ?? 'ja-JP';
//function sharePercent(value, total) {
// if (total === 0) return '0%';
// return Math.round((value / total) * 100) + '%';
//}
function escapeCell(value: string) {
return String(value).replaceAll('|', '\\|').replaceAll('\n', '<br>');
}
//function tableCell(value) {
// return String(value).replaceAll('|', '\\|').replaceAll('\r', ' ').replaceAll('\n', ' ');
//}
//function code(value) {
// const sanitized = String(value).replaceAll('\r', ' ').replaceAll('\n', ' ');
// const backtickRuns = sanitized.match(/`+/g) ?? [];
// const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1));
// const fence = '`'.repeat(fenceLength);
// const padding = sanitized.startsWith('`') || sanitized.endsWith('`') ? ' ' : '';
//
// return `${fence}${padding}${sanitized}${padding}${fence}`;
//}
//function tableCode(value) {
// return tableCell(code(value));
//}
type Manifest = Record<string, { file?: string; src?: string; name?: string; isEntry?: boolean; imports?: string[] }>;
type FileEntry = {
key: string;
displayName: string;
file: string;
size: number;
};
function entryDisplayName(entry: FileEntry) {
if (!entry) return '';
return entry.displayName || entry.file;
}
function findEntryKey(manifest: Manifest) {
const entries = Object.entries(manifest);
return entries.find(([key, chunk]) => key === 'src/_boot_.ts' || chunk.src === 'src/_boot_.ts')?.[0]
?? entries.find(([, chunk]) => chunk.name === 'entry' && chunk.isEntry)?.[0]
?? entries.find(([, chunk]) => chunk.isEntry)?.[0]
?? null;
}
function stableChunkKey(manifestKey: string, chunk: Manifest[string]) {
return chunk.src ?? (chunk.name ? `chunk:${chunk.name}` : manifestKey);
}
function collectStartupKeys(manifest: Manifest) {
const entryKey = findEntryKey(manifest);
const keys = new Set<string>();
if (entryKey == null) return keys;
function visit(key: string) {
if (keys.has(key)) return;
const chunk = manifest[key];
if (!chunk || !chunk.file?.endsWith('.js')) return;
keys.add(stableChunkKey(key, chunk));
for (const importKey of chunk.imports ?? []) {
visit(importKey);
}
}
visit(entryKey);
return keys;
}
async function resolveBuiltFile(outDir: string, file: string) {
if (file.startsWith('scripts/')) {
const localizedFile = file.slice('scripts/'.length);
const localizedPath = path.join(outDir, locale, localizedFile);
if (await util.fileExists(localizedPath)) {
return {
absolutePath: localizedPath,
relativePath: `${locale}/${localizedFile}`,
};
}
throw new Error(`Expected ${locale} localized chunk for ${file}, but ${localizedPath} was not found.`);
}
return {
absolutePath: path.join(outDir, file),
relativePath: file,
};
}
async function collectReport(repoDir: string) {
const outDir = path.join(repoDir, 'built/_frontend_vite_');
const manifestPath = path.join(outDir, 'manifest.json');
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as Manifest;
const byKey = new Map<string, FileEntry>();
const byFile = new Set<string>();
for (const [key, chunk] of Object.entries(manifest)) {
if (!chunk.file?.endsWith('.js')) continue;
const builtFile = await resolveBuiltFile(outDir, chunk.file);
const size = await util.fileSize(builtFile.absolutePath);
const stableKey = stableChunkKey(key, chunk);
const displayName = chunk.src ?? chunk.name ?? key;
byKey.set(stableKey, {
key: stableKey,
displayName,
file: builtFile.relativePath,
size,
});
byFile.add(builtFile.relativePath);
}
const localeDir = path.join(outDir, locale);
if (await util.fileExists(localeDir)) {
for await (const fullPath of util.traverseDirectory(localeDir)) {
if (!fullPath.endsWith('.js')) continue;
const relativePath = util.normalizePath(path.relative(outDir, fullPath));
if (byFile.has(relativePath)) continue;
const size = await util.fileSize(fullPath);
byKey.set(relativePath, {
key: relativePath,
displayName: relativePath,
file: relativePath,
size,
});
}
}
return {
manifest,
chunks: Object.fromEntries(byKey),
startupKeys: [...collectStartupKeys(manifest)],
};
}
type VisualizerReport = {
nodeParts?: Record<string, {
renderedLength: number;
gzipLength: number;
brotliLength: number;
}>;
nodeMetas?: Record<string, {
id: string;
isEntry?: boolean;
isExternal?: boolean;
importedBy?: string[];
imported?: { id: string; dynamic?: boolean }[];
moduleParts?: Record<string, string>;
renderedLength: number;
gzipLength: number;
brotliLength: number;
}>;
options?: Record<string, unknown>;
};
function collectVisualizerReport(data: VisualizerReport) {
const nodeParts = data.nodeParts ?? {};
const nodeMetas = Object.values(data.nodeMetas ?? {});
const moduleRows = [];
const bundleMap = new Map();
for (const meta of nodeMetas) {
const row = {
id: meta.id,
bundles: 0,
renderedLength: 0,
gzipLength: 0,
brotliLength: 0,
importedByCount: meta.importedBy?.length ?? 0,
importedCount: meta.imported?.length ?? 0,
};
for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) {
const part = nodeParts[partUid];
if (part == null) continue;
row.bundles += 1;
row.renderedLength += part.renderedLength;
row.gzipLength += part.gzipLength;
row.brotliLength += part.brotliLength;
const bundle = bundleMap.get(bundleId) ?? {
id: bundleId,
modules: 0,
renderedLength: 0,
gzipLength: 0,
brotliLength: 0,
};
bundle.modules += 1;
bundle.renderedLength += part.renderedLength;
bundle.gzipLength += part.gzipLength;
bundle.brotliLength += part.brotliLength;
bundleMap.set(bundleId, bundle);
}
if (row.bundles > 0) {
moduleRows.push(row);
}
}
let staticImports = 0;
let dynamicImports = 0;
for (const meta of nodeMetas) {
for (const imported of meta.imported ?? []) {
if (imported.dynamic) {
dynamicImports += 1;
} else {
staticImports += 1;
}
}
}
const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength);
const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength);
const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0);
const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0);
const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0);
return {
options: data.options ?? {},
summary: {
bundles: bundleRows.length,
modules: moduleRows.length,
entries: nodeMetas.filter((meta) => meta.isEntry).length,
externals: nodeMetas.filter((meta) => meta.isExternal).length,
staticImports,
dynamicImports,
},
metrics: {
renderedLength: totalRendered,
gzipLength: totalGzip,
brotliLength: totalBrotli,
},
hotModules,
};
}
function renderVisualizerSummaryTable(before: ReturnType<typeof collectVisualizerReport>, after: ReturnType<typeof collectVisualizerReport>) {
const summary = [
'bundles',
'modules',
'entries',
//'externals',
'staticImports',
'dynamicImports',
] as const;
const metrics = [
'renderedLength',
'gzipLength',
'brotliLength',
] as const;
return [
`<table>`,
`<thead>`,
`<tr>`,
`<th rowspan="2"></th>`,
`<th rowspan="2">Bundles</th>`,
`<th rowspan="2">Modules</th>`,
`<th rowspan="2">Entries</th>`,
`<th colspan="2">Imports</th>`,
`<th colspan="3">Size</th>`,
`</tr>`,
`<tr>`,
`<th>Static</th>`,
`<th>Dynamic</th>`,
`<th>Rendered</th>`,
`<th>Gzip</th>`,
`<th>Brotli</th>`,
`</tr>`,
`</thead>`,
`<tbody>`,
`<tr>`,
`<th><b>Before</b></th>`,
...summary.map((key) => `<td>${util.formatNumber(before.summary[key])}</td>`),
...metrics.map((key) => `<td>${util.formatBytes(before.metrics[key])}</td>`),
`</tr>`,
`<tr>`,
`<th><b>After</b></th>`,
...summary.map((key) => `<td>${util.formatNumber(after.summary[key])}</td>`),
...metrics.map((key) => `<td>${util.formatBytes(after.metrics[key])}</td>`),
`</tr>`,
`<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>`,
`<tr>`,
`<th><b>Δ</b></th>`,
...summary.map((key) => `<td>${util.calcAndFormatDeltaNumber(before.summary[key], after.summary[key], 0)}</td>`),
...metrics.map((key) => `<td>${util.calcAndFormatDeltaBytes(before.metrics[key], after.metrics[key], 1000)}</td>`),
`</tr>`,
`<tr>`,
`<th><b>Δ (%)</b></th>`,
...summary.map((key) => `<td>${util.calcAndFormatDeltaPercent(before.summary[key], after.summary[key], 0.1)}</td>`),
...metrics.map((key) => `<td>${util.calcAndFormatDeltaPercent(before.metrics[key], after.metrics[key], 0.1)}</td>`),
`</tr>`,
`</tbody>`,
`</table>`,
];
}
function getChunkComparisonRows(keys: string[], before: Awaited<ReturnType<typeof collectReport>>, after: Awaited<ReturnType<typeof collectReport>>) {
return keys.map((key) => {
const beforeEntry = before.chunks[key];
const afterEntry = after.chunks[key];
const beforeSize = beforeEntry?.size ?? 0;
const afterSize = afterEntry?.size ?? 0;
return {
key,
name: entryDisplayName(beforeEntry ?? afterEntry),
chunkFile: beforeEntry?.file ?? afterEntry?.file,
beforeSize,
afterSize,
changeType: beforeEntry == null ? 'added' : afterEntry == null ? 'removed' : beforeSize !== afterSize ? 'updated' : 'unchanged',
sortSize: Math.max(beforeSize, afterSize),
};
});
}
function summarizeChunkChanges(rows: ReturnType<typeof getChunkComparisonRows>) {
return {
updated: rows.filter((row) => row.changeType === 'updated').length,
added: rows.filter((row) => row.changeType === 'added').length,
removed: rows.filter((row) => row.changeType === 'removed').length,
};
}
function formatChunkChangeSummary(label: string, summary: ReturnType<typeof summarizeChunkChanges>) {
return `${label} (${summary.updated} updated, ${summary.added} added, ${summary.removed} removed)`;
}
function compareChunkComparisonRows(a: ReturnType<typeof getChunkComparisonRows>[number], b: ReturnType<typeof getChunkComparisonRows>[number]) {
return Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|| b.sortSize - a.sortSize
|| a.name.localeCompare(b.name);
}
function chunkMarkdownTable(rows: ReturnType<typeof getChunkComparisonRows>, total?: { beforeSize: number; afterSize: number }) {
if (rows.length === 0) return '_No data_';
const lines = [
'| Chunk | Before | After | Δ | Δ (%) |',
'| --- | ---: | ---: | ---: | ---: |',
];
if (total != null) {
lines.push(`| (total) | ${util.formatBytes(total.beforeSize)} | ${util.formatBytes(total.afterSize)} | ${util.calcAndFormatDeltaBytes(total.beforeSize, total.afterSize, 1000)} | ${util.calcAndFormatDeltaPercent(total.beforeSize, total.afterSize, 0.1).replaceAll('\\%', '\\\\%')} |`);
lines.push('| | | | | |');
}
for (const row of rows) {
if (row.changeType === 'added') {
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${util.formatBytes(row.beforeSize)} | ${util.formatBytes(row.afterSize)} | ${util.calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{orange}{\\text{( + )}}$ |`);
} else if (row.changeType === 'removed') {
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${util.formatBytes(row.beforeSize)} | ${util.formatBytes(row.afterSize)} | ${util.calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{green}{\\text{( - )}}$ |`);
} else {
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${util.formatBytes(row.beforeSize)} | ${util.formatBytes(row.afterSize)} | ${util.calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | ${util.calcAndFormatDeltaPercent(row.beforeSize, row.afterSize, 0.1).replaceAll('\\%', '\\\\%')} |`);
}
}
return lines.join('\n');
}
function renderFrontendChunkReport(before: Awaited<ReturnType<typeof collectReport>>, after: Awaited<ReturnType<typeof collectReport>>) {
const commonChunkKeys = Object.keys(before.chunks).filter((key) => after.chunks[key] != null);
const addedChunkKeys = Object.keys(after.chunks).filter((key) => before.chunks[key] == null);
const removedChunkKeys = Object.keys(before.chunks).filter((key) => after.chunks[key] == null);
const allChunkKeys = [
...commonChunkKeys,
...addedChunkKeys,
...removedChunkKeys,
];
//const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
const allComparisonRows = getChunkComparisonRows(allChunkKeys, before, after);
const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged');
const diffSummary = summarizeChunkChanges(changedRows);
const diffTotal = {
beforeSize: allComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
afterSize: allComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
};
const diffRows = changedRows.sort(compareChunkComparisonRows).slice(0, 30); // TODO: 実際に30を超えて切り捨てられたrowがあった場合はその旨をmarkdown内に表示するようにする
const startupKeys = new Set([
...before.startupKeys,
...after.startupKeys,
]);
const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after);
const startupRows = startupComparisonRows.sort(compareChunkComparisonRows);
const startupSummary = summarizeChunkChanges(startupComparisonRows);
const startupTotal = {
beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
};
//const largeRows = comparisonRows
// .sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name))
// .slice(0, 30);
return [
'<details open>',
`<summary>${formatChunkChangeSummary('Chunk size diff', diffSummary)}</summary>`,
'',
chunkMarkdownTable(diffRows, diffTotal),
'',
'</details>',
'',
'<details>',
`<summary>${formatChunkChangeSummary('Startup chunk size', startupSummary)}</summary>`,
'',
chunkMarkdownTable(startupRows, startupTotal),
'',
`_Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`,
'',
'</details>',
'',
//'<details>',
//`<summary>Largest</summary>`,
//'',
//markdownTable(largeRows),
//'',
//'</details>',
//'',
].join('\n');
}
function renderFrontendBundleReport(before: ReturnType<typeof collectVisualizerReport>, after: ReturnType<typeof collectVisualizerReport>) {
const lines = [
...renderVisualizerSummaryTable(before, after),
'',
//'<details>',
//'<summary>Top 10</summary>',
//'',
];
/*
for (const row of after.hotModules.slice(0, 10)) {
lines.push(`- ${code(row.id)}: ${sharePercent(row.renderedLength, after.metrics.renderedLength)} (${formatBytes(row.renderedLength)})`);
}
lines.push(
'',
'</details>',
);
lines.push(
'',
'<details>',
'<summary>Hot Modules (Self Size)</summary>',
'',
'| Module | Bundles | Rendered | Share | Gzip | Brotli | Imports | Imported By |',
'|---|---:|---:|---:|---:|---:|---:|---:|',
);
for (const row of after.hotModules.slice(0, 15)) {
lines.push(`| ${tableCode(row.id)} | ${row.bundles} | ${formatBytes(row.renderedLength)} | ${sharePercent(row.renderedLength, after.metrics.renderedLength)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${row.importedCount} | ${row.importedByCount} |`);
}
lines.push(
'',
'</details>',
);
*/
return lines.join('\n');
}
const args = process.argv.slice(2);
const [beforeDir, afterDir, beforeStatsFile, afterStatsFile, outFile] = args;
const before = await collectReport(beforeDir);
const after = await collectReport(afterDir);
const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8')) as VisualizerReport;
const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8')) as VisualizerReport;
const beforeVisualizerReport = collectVisualizerReport(beforeStats);
const afterVisualizerReport = collectVisualizerReport(afterStats);
const visualizerArtifactLink = `[Open treemap HTML](${process.env.FRONTEND_BUNDLE_REPORT_ARTIFACT_URL})`;
const body = [
marker,
'',
`## 📦 Frontend Bundle Report`,
'',
renderFrontendChunkReport(before, after),
'',
'## Bundle Stats',
'',
renderFrontendBundleReport(beforeVisualizerReport, afterVisualizerReport),
'',
visualizerArtifactLink,
].join('\n');
await fs.writeFile(outFile, body);

View File

@@ -1,200 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// NOTE: このファイルはworkflow上でバックエンドからも参照されるため、side effectがあってはならない
import * as util from './utility.mts';
export const heapSnapshotCategory = {
total: { label: 'Total', color: 'gray', colorHex: '#888888' },
code: { label: 'Code', color: 'orange', colorHex: '#f28e2c' },
strings: { label: 'Strings', color: 'red', colorHex: '#e15759' },
jsArrays: { label: 'JS arrays', color: 'cyan', colorHex: '#76b7b2' },
typedArrays: { label: 'Typed arrays', color: 'green', colorHex: '#59a14f' },
systemObjects: { label: 'System objects', color: 'yellow', colorHex: '#edc949' },
otherJsObjects: { label: 'Other JS objs', color: 'violet', colorHex: '#af7aa1' },
otherNonJsObjects: { label: 'Other non-JS objs', color: 'pink', colorHex: '#ff9da7' },
} as const satisfies Record<string, { label: string; color: string; colorHex: string }>;
export type HeapSnapshotData = {
categories: Record<keyof typeof heapSnapshotCategory, number>;
nodeCounts: Record<keyof typeof heapSnapshotCategory, number>;
breakdowns?: Record<keyof typeof heapSnapshotCategory, Record<string, number>>;
};
export type HeapSnapshotReport = {
summary: HeapSnapshotData;
samples: {
round: number;
data: HeapSnapshotData;
}[];
};
function getHeapSnapshotCategoryValue(report: HeapSnapshotReport, category: keyof typeof heapSnapshotCategory) {
return report.summary.categories[category];
}
export function renderHeapSnapshotTable(base: HeapSnapshotReport, head: HeapSnapshotReport) {
const lines = [
'| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
const baseTotal = getHeapSnapshotCategoryValue(base, 'total');
const headTotal = getHeapSnapshotCategoryValue(head, 'total');
function getHeapSnapshotSampleSpread(report: HeapSnapshotReport, category: keyof typeof heapSnapshotCategory) {
const values = report.samples
.map(sample => sample.data.categories[category])
.filter(value => Number.isFinite(value)) as number[];
if (values.length < 2) throw new Error(`Not enough samples for category ${category}`);
const center = util.median(values);
return util.median(values.map(value => Math.abs(value - center)));
}
for (const category of Object.keys(heapSnapshotCategory) as (keyof typeof heapSnapshotCategory)[]) {
const baseValue = getHeapSnapshotCategoryValue(base, category);
const headValue = getHeapSnapshotCategoryValue(head, category);
const baseSpread = getHeapSnapshotSampleSpread(base, category);
const headSpread = getHeapSnapshotSampleSpread(head, category);
const summary = util.pairedDeltaSummary(base.samples, head.samples, (sample) => sample.data.categories[category]);
const percent = summary.median * 100 / baseValue;
if (category === 'total') {
const deltaMedian = `${util.formatDeltaBytes(summary.median, 100000)}<br>${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
const baseText = `${util.formatBytes(baseValue)} <br> ± ${util.formatBytes(baseSpread)}`;
const headText = `${util.formatBytes(headValue)} <br> ± ${util.formatBytes(headSpread)}`;
const metricText = `$\\color{${heapSnapshotCategory[category].color}}{\\rule{8pt}{8pt}}$ **${heapSnapshotCategory[category].label}**`;
lines.push(`| ${metricText} | ${baseText} | ${headText} | ${deltaMedian} | ${util.formatBytes(summary.mad)} | ${util.formatDeltaBytes(summary.min, 100000)} | ${util.formatDeltaBytes(summary.max, 100000)} |`);
lines.push('| | | | | | | |');
} else {
const deltaMedian = util.formatDeltaBytes(summary.median, 100000);
const baseText = util.formatBytes(baseValue);
const headText = util.formatBytes(headValue);
const basePercent = util.formatPercent((baseValue * 100) / baseTotal);
const headPercent = util.formatPercent((headValue * 100) / headTotal);
const metricText = `<details><summary>$\\color{${heapSnapshotCategory[category].color}}{\\rule{8pt}{8pt}}$ **${heapSnapshotCategory[category].label}**</summary>${basePercent}${headPercent}</details>`;
lines.push(`| ${metricText} | ${baseText} | ${headText} | ${deltaMedian} | ${util.formatBytes(summary.mad)} | ${util.formatDeltaBytes(summary.min, 100000)} | ${util.formatDeltaBytes(summary.max, 100000)} |`);
}
}
if (lines.length === 2) return null;
return lines.join('\n');
}
const heapSnapshotSankeyChildMinRatio = 0.3;
const heapSnapshotSankeyParentMinPercent = 10;
function escapeCsvValue(value: string) {
return `"${String(value).replaceAll('"', '""')}"`;
}
export function renderHeapSnapshotSankey(report: HeapSnapshotReport, title: string) {
const total = getHeapSnapshotCategoryValue(report, 'total');
if (total == null || total <= 0) return null;
function getHeapSnapshotBreakdownEntries(category: keyof typeof heapSnapshotCategory) {
const breakdown = report.summary.breakdowns?.[category];
if (breakdown == null || typeof breakdown !== 'object') return [];
return Object.entries(breakdown)
.filter(([, value]) => Number.isFinite(value) && value > 0)
.toSorted((a, b) => b[1] - a[1]);
}
function formatHeapSnapshotSankeyChildLabel(label: string) {
return String(label).replace(/^[^:]+:\s*/, '');
}
function formatSankeyPercentValue(value: number) {
const rounded = Math.round(value * 100) / 100;
if (rounded === 0 && value > 0) return '0.01';
if (Number.isInteger(rounded)) return String(rounded);
return rounded.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
}
const categories = (Object.keys(heapSnapshotCategory) as (keyof typeof heapSnapshotCategory)[])
.filter(category => category !== 'total')
.map(category => {
const value = getHeapSnapshotCategoryValue(report, category);
if (value == null || value <= 0) return null;
const breakdownEntries = getHeapSnapshotBreakdownEntries(category);
const breakdownTotal = breakdownEntries.reduce((sum, [, childValue]) => sum + childValue, 0);
const percent = (value * 100) / total;
const childEntries = [];
let otherPercent = 0;
if (breakdownTotal > 0 && percent > heapSnapshotSankeyParentMinPercent) {
for (const [childName, childValue] of breakdownEntries) {
const childRatio = childValue / breakdownTotal;
const childPercent = percent * childRatio;
if (childRatio >= heapSnapshotSankeyChildMinRatio) {
childEntries.push([formatHeapSnapshotSankeyChildLabel(childName), childPercent]);
} else {
otherPercent += childPercent;
}
}
if (childEntries.length > 0 && otherPercent > 0) {
childEntries.push(['Other', otherPercent]);
}
}
return {
category,
percent,
childEntries,
};
})
.filter(value => value != null);
if (categories.length === 0) return null;
const nodeColors = {
[title]: heapSnapshotCategory.total.colorHex,
} as Record<string, string>;
for (const { category, childEntries } of categories) {
const categoryColor = heapSnapshotCategory[category].colorHex;
nodeColors[category] = categoryColor;
for (const [childName] of childEntries) {
nodeColors[childName] = categoryColor;
}
}
const lines = [
`<details><summary>${title} heap snapshot composition</summary>`,
'',
'```mermaid',
`%%{init: ${JSON.stringify({
sankey: {
showValues: false,
linkColor: 'target',
labelStyle: 'outlined',
nodeAlignment: 'center',
nodePadding: 10,
nodeColors: {
...nodeColors,
'Other': '#888888',
},
},
})}}%%`,
'sankey-beta',
];
for (const { category, percent, childEntries } of categories) {
lines.push(`${escapeCsvValue(title)},${escapeCsvValue(heapSnapshotCategory[category].label)},${formatSankeyPercentValue(percent)}`);
for (const [childName, childPercent] of childEntries) {
lines.push(`${escapeCsvValue(heapSnapshotCategory[category].label)},${escapeCsvValue(childName)},${formatSankeyPercentValue(childPercent)}`);
}
}
lines.push('```');
lines.push('');
lines.push('</details>');
return lines.join('\n');
}

View File

@@ -1,309 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createRequire } from 'node:module';
import { copyFile, rm, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import * as util from './utility.mts';
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
import type { MemoryReportRaw } from '../../packages/backend/scripts/measure-memory.mts';
const phases = ['afterGc'] as const;
export type MemoryReport = {
timestamp: string;
sampleCount: any;
aggregation: string;
measurement: {
startupTimeoutMs: any;
memorySettleTimeMs: any;
ipcTimeoutMs: any;
requestCount: any;
heapSnapshot: {
enabled: any;
timeoutMs: any;
breakdownTopN: any;
};
};
summary: Record<typeof phases[number], {
memoryUsage: Record<string, number>;
heapSnapshot?: heapSnapshotUtil.HeapSnapshotData;
}>;
samples: (MemoryReportRaw['samples'][number] & {
round: number;
})[];
};
const [baseDirArg, headDirArg, baseOutputArg, headOutputArg] = process.argv.slice(2);
const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = util.readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', 6, 1);
const HEAD_HEAP_SNAPSHOT_WORK_DIR = resolve('head-heap-snapshots');
const HEAD_HEAP_SNAPSHOT_OUTPUT_PATH = resolve('head-heap-snapshot.heapsnapshot');
async function resetState(repoDir: string) {
const require = createRequire(join(repoDir, 'packages/backend/package.json'));
const pg = require('pg');
const Redis = require('ioredis');
const postgres = new pg.Client({
host: '127.0.0.1',
port: 54312,
database: 'postgres',
user: 'postgres',
});
await postgres.connect();
try {
await postgres.query('DROP DATABASE IF EXISTS "test-misskey" WITH (FORCE)');
await postgres.query('CREATE DATABASE "test-misskey"');
} finally {
await postgres.end();
}
const redis = new Redis({ host: '127.0.0.1', port: 56312 });
try {
await redis.flushall();
} finally {
redis.disconnect();
}
}
function summarizeHeapSnapshotBreakdowns(samples: MemoryReport['samples'], phase: typeof phases[number]) {
const breakdowns = {} as Record<keyof typeof heapSnapshotUtil.heapSnapshotCategory, Record<string, number>>;
for (const category of Object.keys(heapSnapshotUtil.heapSnapshotCategory) as (keyof typeof heapSnapshotUtil.heapSnapshotCategory)[]) {
if (category === 'total') continue;
const childKeys = new Set<string>();
for (const sample of samples) {
for (const childKey of Object.keys(sample.phases[phase].heapSnapshot?.breakdowns?.[category] ?? {})) {
childKeys.add(childKey);
}
}
const categoryBreakdown = {} as Record<string, number>;
for (const childKey of childKeys) {
const values = samples
.map(sample => sample.phases[phase].heapSnapshot?.breakdowns?.[category]?.[childKey])
.filter(value => Number.isFinite(value)) as number[];
if (values.length > 0) categoryBreakdown[childKey] = util.median(values);
}
if (Object.keys(categoryBreakdown).length > 0) {
breakdowns[category] = collapseHeapSnapshotBreakdown(categoryBreakdown);
}
}
return breakdowns;
}
function collapseHeapSnapshotBreakdown(breakdown: Record<string, number>) {
const entries = Object.entries(breakdown)
.filter(([, value]) => value > 0)
.toSorted((a, b) => b[1] - a[1]);
const topEntries = entries.slice(0, HEAP_SNAPSHOT_BREAKDOWN_TOP_N);
const otherValue = entries
.slice(HEAP_SNAPSHOT_BREAKDOWN_TOP_N)
.reduce((sum, [, value]) => sum + value, 0);
const collapsed = Object.fromEntries(topEntries);
if (otherValue > 0) collapsed.Other = otherValue;
return collapsed;
}
function summarizeSamples(samples: MemoryReport['samples']) {
const summary = {} as MemoryReport['summary'];
for (const phase of phases) {
summary[phase] = {
memoryUsage: {},
};
const metricKeys = new Set<string>();
for (const sample of samples) {
for (const key of Object.keys(sample.phases[phase].memoryUsage)) {
metricKeys.add(key);
}
}
for (const key of metricKeys) {
const values = samples.map(sample => sample.phases[phase].memoryUsage[key]);
summary[phase].memoryUsage[key] = util.median(values);
}
const heapSnapshotCategoryValues = {} as Record<keyof typeof heapSnapshotUtil.heapSnapshotCategory, number>;
for (const category of Object.keys(heapSnapshotUtil.heapSnapshotCategory) as (keyof typeof heapSnapshotUtil.heapSnapshotCategory)[]) {
const values = samples
.map(sample => sample.phases[phase].heapSnapshot?.categories?.[category])
.filter(value => Number.isFinite(value)) as number[];
if (values.length > 0) heapSnapshotCategoryValues[category] = util.median(values);
}
const heapSnapshotNodeCountValues = {} as Record<keyof typeof heapSnapshotUtil.heapSnapshotCategory, number>;
for (const category of Object.keys(heapSnapshotUtil.heapSnapshotCategory) as (keyof typeof heapSnapshotUtil.heapSnapshotCategory)[]) {
const values = samples
.map(sample => sample.phases[phase].heapSnapshot?.nodeCounts?.[category])
.filter(value => Number.isFinite(value)) as number[];
if (values.length > 0) heapSnapshotNodeCountValues[category] = util.median(values);
}
if (Object.keys(heapSnapshotCategoryValues).length > 0) {
const heapSnapshotBreakdowns = summarizeHeapSnapshotBreakdowns(samples, phase);
summary[phase].heapSnapshot = {
categories: heapSnapshotCategoryValues,
nodeCounts: heapSnapshotNodeCountValues,
...(Object.keys(heapSnapshotBreakdowns).length > 0 ? { breakdowns: heapSnapshotBreakdowns } : {}),
};
}
}
return summary;
}
async function measureRepo(label: string, repoDir: string, round: number, options: { heapSnapshotSavePath?: string } = {}) {
process.stderr.write(`[${label}] Resetting database and Redis\n`);
await resetState(repoDir);
process.stderr.write(`[${label}] Running migrations\n`);
await util.run('pnpm', ['--filter', 'backend', 'migrate'], {
cwd: repoDir,
env: process.env,
logStdout: true,
});
process.stderr.write(`[${label}] Measuring memory\n`);
const measureEnv = {
...process.env,
MK_MEMORY_SAMPLE_COUNT: '1',
} as NodeJS.ProcessEnv;
if (round <= 0) measureEnv.MK_MEMORY_HEAP_SNAPSHOT = '0';
if (options.heapSnapshotSavePath != null) measureEnv.MK_MEMORY_HEAP_SNAPSHOT_SAVE_PATH = options.heapSnapshotSavePath;
const stdout = await util.run('node', ['packages/backend/scripts/measure-memory.mts'], {
cwd: repoDir,
env: measureEnv,
});
const report = JSON.parse(stdout) as MemoryReportRaw;
const sample = report.samples[0];
return sample;
}
function headHeapSnapshotPath(round: number) {
return join(HEAD_HEAP_SNAPSHOT_WORK_DIR, `round-${round}.heapsnapshot`);
}
function selectRepresentativeHeadHeapSnapshotRound(samples: MemoryReport['samples'], summary: MemoryReport['summary']) {
const medianTotal = summary.afterGc.heapSnapshot?.categories?.total;
if (medianTotal == null || !Number.isFinite(medianTotal)) return null;
let selected: { round: number; distance: number } | null = null;
for (const sample of samples) {
const total = sample.phases.afterGc.heapSnapshot?.categories?.total;
if (total == null || !Number.isFinite(total)) continue;
const distance = Math.abs(total - medianTotal);
if (selected == null || distance < selected.distance || (distance === selected.distance && sample.round < selected.round)) {
selected = {
round: sample.round,
distance,
};
}
}
return selected?.round ?? null;
}
async function saveRepresentativeHeadHeapSnapshot(samples: MemoryReport['samples'], summary: MemoryReport['summary']) {
const round = selectRepresentativeHeadHeapSnapshotRound(samples, summary);
if (round == null) return;
await copyFile(headHeapSnapshotPath(round), HEAD_HEAP_SNAPSHOT_OUTPUT_PATH);
process.stderr.write(`Selected head heap snapshot round ${round} for artifact\n`);
await rm(HEAD_HEAP_SNAPSHOT_WORK_DIR, { recursive: true, force: true });
}
async function main() {
const baseDir = resolve(baseDirArg);
const headDir = resolve(headDirArg);
const baseOutput = resolve(baseOutputArg);
const headOutput = resolve(headOutputArg);
const rounds = util.readIntegerEnv('MK_MEMORY_COMPARE_ROUNDS', 5, 1);
const warmupRounds = util.readIntegerEnv('MK_MEMORY_COMPARE_WARMUP_ROUNDS', 1, 0);
const startedAt = new Date().toISOString();
await rm(HEAD_HEAP_SNAPSHOT_WORK_DIR, { recursive: true, force: true });
await rm(HEAD_HEAP_SNAPSHOT_OUTPUT_PATH, { force: true });
const reports = {
base: {
dir: baseDir,
samples: [] as MemoryReport['samples'],
},
head: {
dir: headDir,
samples: [] as MemoryReport['samples'],
},
};
for (let round = 1; round <= warmupRounds; round++) {
process.stderr.write(`Starting warmup round ${round}/${warmupRounds}\n`);
for (const label of ['base', 'head'] as const) {
await measureRepo(label, reports[label].dir, -round);
}
}
for (let round = 1; round <= rounds; round++) {
const order = round % 2 === 1 ? ['base', 'head'] as const : ['head', 'base'] as const;
process.stderr.write(`Starting measurement round ${round}/${rounds}: ${order.join(' -> ')}\n`);
for (const label of order) {
const shouldSaveHeadHeapSnapshot = label === 'head';
const options = shouldSaveHeadHeapSnapshot
? { heapSnapshotSavePath: headHeapSnapshotPath(round) }
: {};
const sample = await measureRepo(label, reports[label].dir, round, options);
reports[label].samples.push({
...sample,
round,
});
}
}
const summaries = {
base: summarizeSamples(reports.base.samples),
head: summarizeSamples(reports.head.samples),
};
await saveRepresentativeHeadHeapSnapshot(reports.head.samples, summaries.head);
for (const label of ['base', 'head'] as const) {
const report = {
timestamp: new Date().toISOString(),
sampleCount: reports[label].samples.length,
aggregation: 'median',
comparison: {
strategy: 'interleaved-pairs',
rounds,
warmupRounds,
startedAt,
},
summary: summaries[label],
samples: reports[label].samples,
};
await writeFile(label === 'base' ? baseOutput : headOutput, `${JSON.stringify(report, null, 2)}\n`);
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -1,204 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// NOTE: このファイルはworkflow上でバックエンドからも参照されるため、side effectがあってはならない
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
export function median(values: number[]) {
const sorted = values.toSorted((a, b) => a - b);
const center = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 1) return sorted[center];
return Math.round((sorted[center - 1] + sorted[center]) / 2);
}
export function mad(values: number[]) {
if (values.length < 2) throw new Error('Not enough samples to calculate MAD');
const center = median(values);
return median(values.map(value => Math.abs(value - center)));
}
function getSamplesByRound<T extends { round: number; }[]>(samples: T) {
const samplesByRound = new Map<number, T[number]>();
for (const sample of samples) {
if (sample.round <= 0) continue;
samplesByRound.set(sample.round, sample);
}
return samplesByRound;
}
export function pairedDeltaSummary<T extends { round: number; }[]>(baseSamples: T, headSamples: T, getValue: (sample: T[number]) => number | null) {
const baseSamplesByRound = getSamplesByRound(baseSamples);
const headSamplesByRound = getSamplesByRound(headSamples);
const values = [];
for (const [round, baseSample] of baseSamplesByRound) {
const headSample = headSamplesByRound.get(round);
if (headSample == null) continue;
const baseValue = getValue(baseSample);
const headValue = getValue(headSample);
if (baseValue == null || headValue == null) continue;
values.push(headValue - baseValue);
}
return {
median: median(values),
mad: mad(values),
min: Math.min(...values),
max: Math.max(...values),
samples: values.length,
};
}
export function normalizePath(filePath: string) {
return filePath.split(path.sep).join('/');
}
export async function fileExists(filePath: string) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
export async function fileSize(filePath: string) {
const stat = await fs.stat(filePath);
return stat.size;
}
export async function* traverseDirectory(dir: string): AsyncGenerator<string> {
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* traverseDirectory(fullPath);
} else if (entry.isFile()) {
yield fullPath;
}
}
}
export function escapeLatex(text: string) {
return text
.replaceAll('\\', '\\\\')
.replaceAll('{', '\\{')
.replaceAll('}', '\\}')
.replaceAll('%', '\\%');
}
export function formatColoredDelta(delta: number, text: (value: number) => string, colorThreshold = 0) {
if (delta === 0) return text(0);
const sign = delta > 0 ? '+' : '-';
if (Math.abs(delta) < colorThreshold) return `$\\text{${sign}${escapeLatex(text(Math.abs(delta)))}}$`;
const color = delta > 0 ? 'orange' : 'green';
return `$\\color{${color}}{\\text{${sign}${escapeLatex(text(Math.abs(delta)))}}}$`;
}
const numberFormatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 1,
});
export function formatNumber(value: number) {
return numberFormatter.format(value);
}
export function formatBytes(value: number) {
if (value === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let unitIndex = 0;
let size = value;
while (size >= 1000 && unitIndex < units.length - 1) {
size /= 1000;
unitIndex += 1;
}
const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1;
return `${numberFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`;
}
export function calcAndFormatDeltaNumber(before: number, after: number, colorThreshold = 0) {
if (before == null || after == null) return '-';
const delta = after - before;
return formatColoredDelta(delta, v => formatNumber(v), colorThreshold);
}
export function formatDeltaBytes(deltaBytes: number, colorThreshold = 0) {
return formatColoredDelta(deltaBytes, v => formatBytes(v), colorThreshold);
}
export function calcAndFormatDeltaBytes(before: number, after: number, colorThreshold = 0) {
if (before == null || after == null) return '-';
const delta = after - before;
return formatDeltaBytes(delta, colorThreshold);
}
export function formatPercent(value: number) {
return `${formatNumber(value)}%`;
}
export function formatDeltaPercent(deltaPercent: number, colorThreshold = 0) {
return formatColoredDelta(deltaPercent, v => formatPercent(v), colorThreshold);
}
export function calcAndFormatDeltaPercent(before: number, after: number, colorThreshold = 0) {
if (before == null || before === 0 || after == null || after === 0) return '-';
const delta = after - before;
return formatDeltaPercent(delta / before * 100, colorThreshold);
}
export function commandName(command: string) {
if (process.platform !== 'win32') return command;
if (command === 'pnpm') return 'pnpm.cmd';
return command;
}
export function readIntegerEnv(name: string, defaultValue: number, min: number) {
const rawValue = process.env[name];
if (rawValue == null || rawValue === '') return defaultValue;
if (!/^\d+$/.test(rawValue)) throw new Error(`${name} must be an integer`);
const value = Number(rawValue);
if (!Number.isSafeInteger(value) || value < min) throw new Error(`${name} must be >= ${min}`);
return value;
}
export function run(command: string, args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv; logStdout?: boolean } = {}) {
return new Promise<string>((resolvePromise, reject) => {
const child = spawn(commandName(command), args, {
cwd: options.cwd,
env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', data => {
stdout += data;
if (options.logStdout) process.stderr.write(data);
});
child.stderr.on('data', data => {
stderr += data;
process.stderr.write(data);
});
child.on('error', reject);
child.on('close', code => {
if (code === 0) {
resolvePromise(stdout);
} else {
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}\n${stderr}`));
}
});
});
}

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Setup Node.js
uses: actions/setup-node@v6.4.0

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout head
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:

View File

@@ -18,7 +18,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
with:
submodules: true
persist-credentials: false
@@ -66,7 +66,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
with:
submodules: true
persist-credentials: false

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Check version
run: |
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Check
run: |
counter=0
@@ -44,6 +44,7 @@ jobs:
}
directories=(
"cypress/e2e"
"packages/backend/migration"
"packages/backend/src"
"packages/backend/test"

View File

@@ -10,7 +10,7 @@ jobs:
check_copyright_year:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
- run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!"

View File

@@ -27,6 +27,9 @@ jobs:
pr-ref: ${{ steps.get-ref.outputs.pr-ref }}
wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Check allowed users
id: check-allowed-users
env:

View File

@@ -27,7 +27,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub

View File

@@ -32,7 +32,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Docker meta

View File

@@ -17,7 +17,7 @@ jobs:
DOCKLE_VERSION: 0.4.15
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: |

View File

@@ -1,318 +0,0 @@
name: frontend-bundle-report-comment
on:
workflow_run:
workflows:
- frontend-bundle-report
types:
- completed
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- packages/frontend/**
- packages/frontend-shared/**
- packages/frontend-builder/**
- packages/i18n/**
- packages/icons-subsetter/**
- packages/misskey-js/**
- packages/misskey-reversi/**
- packages/misskey-bubble-game/**
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .node-version
- .github/scripts/utility.mts
- .github/scripts/frontend-js-size.mts
- .github/workflows/frontend-bundle-report.yml
- .github/workflows/frontend-bundle-report-comment.yml
permissions:
actions: read
contents: read
issues: write
pull-requests: write
jobs:
comment:
name: Comment frontend bundle report
if: github.event_name == 'pull_request_target' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
concurrency:
group: frontend-bundle-report-comment-${{ github.event.pull_request.number || github.event.workflow_run.id }}
cancel-in-progress: true
steps:
- name: Find bundle report run
if: github.event_name == 'pull_request_target'
id: find-report-run
uses: actions/github-script@v9
with:
script: |
const workflow_id = 'frontend-bundle-report.yml';
const artifactName = 'frontend-bundle-report';
const headSha = context.payload.pull_request.head.sha;
const prNumber = context.payload.pull_request.number;
const pollIntervalMs = 30_000;
const timeoutMs = 90 * 60_000;
const startedAt = Date.now();
const { owner, repo } = context.repo;
async function listReportWorkflowRuns() {
const runsForHead = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
event: 'pull_request',
head_sha: headSha,
per_page: 100,
});
if (runsForHead.length > 0) {
return runsForHead;
}
const recentRuns = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
event: 'pull_request',
per_page: 100,
});
return recentRuns.filter((run) =>
run.pull_requests?.some((pullRequest) => pullRequest.number === prNumber));
}
async function findReportRun() {
const runs = (await listReportWorkflowRuns())
.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
for (const run of runs) {
if (run.status !== 'completed') continue;
if (run.conclusion !== 'success') {
core.warning(`Frontend bundle report run ${run.id} completed with conclusion: ${run.conclusion}`);
return { done: true, run: null };
}
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id: run.id,
per_page: 100,
});
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
if (report) return { done: true, run };
core.info(`Frontend bundle report run ${run.id} did not produce ${artifactName}.`);
return { done: true, run: null };
}
return { done: false, run: null };
}
while (Date.now() - startedAt < timeoutMs) {
const { done, run } = await findReportRun();
if (run) {
core.info(`Found frontend bundle report on workflow run ${run.id}.`);
core.setOutput('run-id', String(run.id));
return;
}
if (done) {
return;
}
core.info('Waiting for frontend bundle report artifact...');
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
core.warning(`Timed out waiting for ${artifactName} from ${workflow_id} for ${headSha}.`);
- name: Find bundle report artifact
if: github.event_name == 'workflow_run'
id: find-report-artifact
uses: actions/github-script@v9
with:
script: |
const artifactName = 'frontend-bundle-report';
const { owner, repo } = context.repo;
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id: context.payload.workflow_run.id,
per_page: 100,
});
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
if (report) {
core.setOutput('exists', 'true');
} else {
core.info(`Workflow run ${context.payload.workflow_run.id} did not produce ${artifactName}.`);
core.setOutput('exists', 'false');
}
- name: Download bundle report from workflow_run
if: github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true'
uses: actions/download-artifact@v8
with:
name: frontend-bundle-report
path: ${{ runner.temp }}/frontend-bundle-report
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
- name: Download bundle report from pull_request_target
if: github.event_name == 'pull_request_target' && steps.find-report-run.outputs.run-id != ''
uses: actions/download-artifact@v8
with:
name: frontend-bundle-report
path: ${{ runner.temp }}/frontend-bundle-report
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ steps.find-report-run.outputs.run-id }}
- name: Comment on pull request
if: (github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true') || steps.find-report-run.outputs.run-id != ''
uses: actions/github-script@v9
with:
github-token: ${{ secrets.FRONTEND_BUNDLE_REPORT_COMMENT_TOKEN || secrets.FRONTEND_JS_SIZE_COMMENT_TOKEN || secrets.FRONTEND_BUNDLE_VISUALIZER_COMMENT_TOKEN || github.token }}
script: |
const fs = require('node:fs');
const path = require('node:path');
const jsSizeMarker = '<!-- misskey-frontend-js-size -->';
const visualizerMarker = '<!-- misskey-frontend-bundle-visualizer -->';
const reportMarkers = [jsSizeMarker, visualizerMarker];
const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-bundle-report');
const jsSizeReportPath = path.join(reportDir, 'frontend-js-size-report.md');
const prNumberPath = path.join(reportDir, 'pr-number.txt');
const headShaPath = path.join(reportDir, 'head-sha.txt');
const workflowRun = context.payload.workflow_run;
const pullRequest = context.payload.pull_request;
const eventHeadSha = workflowRun?.head_sha ?? pullRequest?.head?.sha ?? null;
const { owner, repo } = context.repo;
if (!fs.existsSync(jsSizeReportPath)) {
core.setFailed('The frontend bundle report artifact does not contain frontend-js-size-report.md.');
return;
}
const artifactHeadSha = fs.existsSync(headShaPath)
? fs.readFileSync(headShaPath, 'utf8').trim()
: null;
if (eventHeadSha != null && artifactHeadSha != null && artifactHeadSha !== eventHeadSha) {
core.info(`The artifact head SHA (${artifactHeadSha}) differs from the event head SHA (${eventHeadSha}). Using artifact metadata for PR validation.`);
}
const reportHeadSha = artifactHeadSha ?? eventHeadSha;
const artifactPrNumber = fs.existsSync(prNumberPath)
? Number(fs.readFileSync(prNumberPath, 'utf8').trim())
: null;
let issue_number = null;
if (pullRequest != null) {
issue_number = pullRequest.number;
if (Number.isInteger(artifactPrNumber) && artifactPrNumber !== issue_number) {
core.setFailed(`The artifact pull request number (${artifactPrNumber}) does not match the event pull request number (${issue_number}).`);
return;
}
} else if (workflowRun != null) {
const associatedPullRequests = new Map();
for (const pullRequest of workflowRun.pull_requests ?? []) {
if (Number.isInteger(pullRequest.number)) {
associatedPullRequests.set(pullRequest.number, pullRequest);
}
}
if (reportHeadSha != null) {
const pullRequestsForCommit = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {
owner,
repo,
commit_sha: reportHeadSha,
per_page: 100,
});
for (const pullRequest of pullRequestsForCommit) {
associatedPullRequests.set(pullRequest.number, pullRequest);
}
}
if (Number.isInteger(artifactPrNumber) && associatedPullRequests.has(artifactPrNumber)) {
issue_number = artifactPrNumber;
} else if (Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 0) {
issue_number = artifactPrNumber;
} else if (!Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 1) {
issue_number = [...associatedPullRequests.keys()][0];
} else if (Number.isInteger(artifactPrNumber)) {
core.setFailed(`The artifact pull request number (${artifactPrNumber}) is not associated with ${reportHeadSha}.`);
return;
} else {
core.setFailed(`Could not determine the pull request associated with ${reportHeadSha}.`);
return;
}
} else {
core.setFailed('Could not determine the pull request event for this report.');
return;
}
const currentPullRequest = await github.rest.pulls.get({
owner,
repo,
pull_number: issue_number,
});
const currentHeadSha = currentPullRequest.data.head?.sha;
if (reportHeadSha != null && currentHeadSha != null && reportHeadSha !== currentHeadSha) {
core.info(`The report head SHA (${reportHeadSha}) is not the current pull request head SHA (${currentHeadSha}). Skipping stale frontend bundle report.`);
return;
}
const jsSizeReport = fs.readFileSync(jsSizeReportPath, 'utf8').trim();
if (!jsSizeReport.includes(jsSizeMarker)) {
core.setFailed('The frontend JS size report is missing the expected marker.');
return;
}
let body = `${jsSizeReport}\n`;
const maxCommentLength = 65_000;
if (body.length > maxCommentLength) {
const reportLocation = workflowRun?.html_url != null
? `[workflow run](${workflowRun.html_url})`
: 'workflow artifact';
const footer = [
'',
'',
`_Report truncated because it exceeded ${maxCommentLength.toLocaleString('en-US')} characters. See the ${reportLocation} for the full report._`,
].join('\n');
body = `${body.slice(0, maxCommentLength - footer.length)}${footer}`;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const previousReports = comments.filter((comment) =>
comment.user?.type === 'Bot' && reportMarkers.some((reportMarker) => comment.body?.includes(reportMarker)));
if (previousReports.length > 0) {
const [previous, ...duplicates] = previousReports;
await github.rest.issues.updateComment({
owner,
repo,
comment_id: previous.id,
body,
});
for (const duplicate of duplicates) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: duplicate.id,
});
}
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}

View File

@@ -1,170 +0,0 @@
name: frontend-bundle-report
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- packages/frontend/**
- packages/frontend-shared/**
- packages/frontend-builder/**
- packages/i18n/**
- packages/icons-subsetter/**
- packages/misskey-js/**
- packages/misskey-reversi/**
- packages/misskey-bubble-game/**
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .node-version
- .github/scripts/utility.mts
- .github/scripts/frontend-js-size.mts
- .github/workflows/frontend-bundle-report.yml
- .github/workflows/frontend-bundle-report-comment.yml
permissions:
contents: read
pull-requests: read
concurrency:
group: frontend-bundle-report-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
report:
name: Build frontend bundle report
runs-on: ubuntu-latest
env:
FRONTEND_JS_SIZE_LOCALE: ja-JP
steps:
- name: Checkout base
uses: actions/checkout@v6.0.3
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.event.pull_request.base.sha }}
path: before
submodules: true
- name: Checkout pull request
uses: actions/checkout@v6.0.3
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
path: after
submodules: true
- name: Check base visualizer support
id: check-base-visualizer
shell: bash
run: |
if grep -q 'FRONTEND_BUNDLE_VISUALIZER' before/packages/frontend/vite.config.ts; then
echo 'supported=true' >> "$GITHUB_OUTPUT"
else
echo 'supported=false' >> "$GITHUB_OUTPUT"
echo 'Base commit does not support frontend bundle visualizer. Skipping frontend bundle report.' >> "$GITHUB_STEP_SUMMARY"
fi
- name: Setup pnpm
if: steps.check-base-visualizer.outputs.supported == 'true'
uses: pnpm/action-setup@v6.0.9
with:
package_json_file: after/package.json
- name: Setup Node.js
if: steps.check-base-visualizer.outputs.supported == 'true'
uses: actions/setup-node@v6.4.0
with:
node-version-file: after/.node-version
cache: pnpm
cache-dependency-path: |
before/pnpm-lock.yaml
after/pnpm-lock.yaml
- name: Install dependencies for base
if: steps.check-base-visualizer.outputs.supported == 'true'
working-directory: before
run: pnpm i --frozen-lockfile
- name: Build frontend dependencies for base
if: steps.check-base-visualizer.outputs.supported == 'true'
working-directory: before
run: pnpm --filter "frontend^..." run build
- name: Prepare report output
if: steps.check-base-visualizer.outputs.supported == 'true'
run: mkdir -p "$RUNNER_TEMP/frontend-bundle-report"
- name: Build frontend report for base
if: steps.check-base-visualizer.outputs.supported == 'true'
working-directory: before
env:
FRONTEND_BUNDLE_VISUALIZER: 'true'
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-report/before-stats.json
run: pnpm --filter frontend run build
- name: Install dependencies for pull request
if: steps.check-base-visualizer.outputs.supported == 'true'
working-directory: after
run: pnpm i --frozen-lockfile
- name: Build frontend dependencies for pull request
if: steps.check-base-visualizer.outputs.supported == 'true'
working-directory: after
run: pnpm --filter "frontend^..." run build
- name: Build frontend report for pull request
if: steps.check-base-visualizer.outputs.supported == 'true'
working-directory: after
env:
FRONTEND_BUNDLE_VISUALIZER: 'true'
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-report/after-stats.json
FRONTEND_BUNDLE_VISUALIZER_HTML_FILE: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-visualizer.html
run: pnpm --filter frontend run build
- name: Upload bundle visualizer
if: steps.check-base-visualizer.outputs.supported == 'true'
id: upload-bundle-visualizer
uses: actions/upload-artifact@v7
with:
name: frontend-bundle-visualizer
path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-visualizer.html
if-no-files-found: error
archive: false
retention-days: 7
- name: Generate report markdown
if: steps.check-base-visualizer.outputs.supported == 'true'
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }}
run: |
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
node after/.github/scripts/frontend-js-size.mts before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-js-size-report.md"
printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt"
printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt"
printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt"
printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt"
- name: Check report
if: steps.check-base-visualizer.outputs.supported == 'true'
run: |
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
test -s "$REPORT_DIR/before-stats.json"
test -s "$REPORT_DIR/after-stats.json"
test -s "$REPORT_DIR/frontend-js-size-report.md"
cat "$REPORT_DIR/frontend-js-size-report.md" >> "$GITHUB_STEP_SUMMARY"
- name: Upload bundle report
if: steps.check-base-visualizer.outputs.supported == 'true'
uses: actions/upload-artifact@v7
with:
name: frontend-bundle-report
path: ${{ runner.temp }}/frontend-bundle-report/
if-no-files-found: error
retention-days: 7

View File

@@ -25,12 +25,12 @@ jobs:
ref: refs/pull/${{ github.event.number }}/merge
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:

View File

@@ -9,14 +9,7 @@ on:
paths:
- packages/backend/**
- packages/misskey-js/**
- .github/scripts/utility.mts
- .github/scripts/backend-memory-report.mts
- .github/scripts/measure-backend-memory-comparison.mts
- .github/scripts/backend-js-footprint.mjs
- .github/scripts/backend-js-footprint-loader.mjs
- .github/scripts/backend-js-footprint-require.cjs
- .github/workflows/get-backend-memory.yml
- .github/workflows/report-backend-memory.yml
jobs:
get-memory-usage:
@@ -24,6 +17,15 @@ jobs:
permissions:
contents: read
strategy:
matrix:
memory-json-name: [memory-base.json, memory-head.json]
include:
- memory-json-name: memory-base.json
ref: ${{ github.base_ref }}
- memory-json-name: memory-head.json
ref: refs/pull/${{ github.event.number }}/merge
services:
postgres:
image: postgres:18
@@ -38,84 +40,37 @@ jobs:
- 56312:6379
steps:
- name: Checkout base
uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
ref: ${{ github.base_ref }}
path: base
submodules: true
- name: Checkout head
uses: actions/checkout@v6.0.3
with:
ref: refs/pull/${{ github.event.number }}/merge
path: head
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
with:
package_json_file: head/package.json
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:
node-version-file: 'head/.node-version'
node-version-file: '.node-version'
cache: 'pnpm'
cache-dependency-path: |
base/pnpm-lock.yaml
head/pnpm-lock.yaml
- name: Install base dependencies
working-directory: base
run: pnpm i --frozen-lockfile
- name: Check base pnpm-lock.yaml
working-directory: base
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Configure base
working-directory: base
run: |
cp .github/misskey/test.yml .config/default.yml
pnpm compile-config
- name: Build base
working-directory: base
- name: Copy Configure
run: cp .github/misskey/test.yml .config/default.yml
- name: Compile Configure
run: pnpm compile-config
- name: Build
run: pnpm build
- name: Install head dependencies
working-directory: head
run: pnpm i --frozen-lockfile
- name: Check head pnpm-lock.yaml
working-directory: head
run: git diff --exit-code pnpm-lock.yaml
- name: Configure head
working-directory: head
- name: Run migrations
run: pnpm --filter backend migrate
- name: Measure memory usage
run: |
cp .github/misskey/test.yml .config/default.yml
pnpm compile-config
- name: Build head
working-directory: head
run: pnpm build
- name: Measure backend memory usage
env:
MK_MEMORY_COMPARE_ROUNDS: 5
MK_MEMORY_COMPARE_WARMUP_ROUNDS: 1
MK_MEMORY_HEAP_SNAPSHOT: 1
run: node head/.github/scripts/measure-backend-memory-comparison.mts base head memory-base.json memory-head.json
- name: Upload head heap snapshot
uses: actions/upload-artifact@v7
with:
name: backend-memory-head-heap-snapshot
path: head-heap-snapshot.heapsnapshot
if-no-files-found: error
retention-days: 7
- name: Measure backend loaded JS footprint
run: |
node head/.github/scripts/backend-js-footprint.mjs base js-footprint-base.json
node head/.github/scripts/backend-js-footprint.mjs head js-footprint-head.json
# Start the server and measure memory usage
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
- name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: memory-artifact-results
path: |
memory-base.json
memory-head.json
js-footprint-base.json
js-footprint-head.json
name: memory-artifact-${{ matrix.memory-json-name }}
path: ${{ matrix.memory-json-name }}
save-pr-number:
runs-on: ubuntu-latest

View File

@@ -16,10 +16,10 @@ on:
- packages/misskey-js/**
- packages/misskey-bubble-game/**
- packages/misskey-reversi/**
- packages/misskey-world/**
- packages/frontend-misskey-world-engine/**
- packages/shared/eslint.config.js
- scripts/check-dts*.mjs
- .github/workflows/lint.yml
- package.json
pull_request:
paths:
- packages/backend/**
@@ -32,20 +32,20 @@ on:
- packages/misskey-js/**
- packages/misskey-bubble-game/**
- packages/misskey-reversi/**
- packages/misskey-world/**
- packages/frontend-misskey-world-engine/**
- packages/shared/eslint.config.js
- scripts/check-dts*.mjs
- .github/workflows/lint.yml
- package.json
jobs:
pnpm_install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
@@ -69,16 +69,18 @@ jobs:
- misskey-js
- misskey-bubble-game
- misskey-reversi
- misskey-world
- frontend-misskey-world-engine
env:
eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
@@ -104,25 +106,6 @@ jobs:
- sw
- misskey-js
steps:
- uses: actions/checkout@v6.0.3
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- run: pnpm --filter "${{ matrix.workspace }}^..." run build
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
check-dts:
needs: [pnpm_install]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
@@ -134,5 +117,5 @@ jobs:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- run: node --test scripts/check-dts.test.mjs
- run: pnpm check-dts
- run: pnpm --filter "${{ matrix.workspace }}^..." run build
- run: pnpm --filter ${{ matrix.workspace }} run typecheck

View File

@@ -16,12 +16,12 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: ".node-version"

View File

@@ -16,11 +16,11 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:

View File

@@ -11,26 +11,44 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
# api-artifact
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
- name: Download artifact
uses: actions/github-script@v9
with:
pattern: api-artifact-*
path: artifacts
merge-multiple: true
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
- name: Check artifacts
run: ls -lh artifacts/
script: |
const fs = require('fs');
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name.startsWith("api-artifact-") || artifact.name == "api-artifact"
});
await Promise.all(matchArtifacts.map(async (artifact) => {
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact.id,
archive_format: 'zip',
});
await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data));
}));
- name: Extract all artifacts
run: |
find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';'
ls -la
- name: Load PR Number
id: load-pr-num
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
- name: Output base
run: cat ./artifacts/api-base.json
- name: Output head
run: cat ./artifacts/api-head.json
- name: Arrange json files
run: |
jq '.' ./artifacts/api-base.json > ./api-base.json
@@ -39,6 +57,8 @@ jobs:
run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff
- name: Get full diff
run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff
- name: Echo full diff
run: cat ./api-full.json.diff
- name: Upload full diff to Artifact
uses: actions/upload-artifact@v7
with:

View File

@@ -11,49 +11,157 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
- name: Download artifacts
uses: actions/download-artifact@v8
- name: Download artifact
uses: actions/github-script@v9
with:
pattern: memory-artifact-*
path: artifacts
merge-multiple: true
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
- name: Check artifacts
run: ls -lh artifacts/
script: |
const fs = require('fs');
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name.startsWith("memory-artifact-") || artifact.name == "memory-artifact"
});
await Promise.all(matchArtifacts.map(async (artifact) => {
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact.id,
archive_format: 'zip',
});
await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data));
}));
- name: Extract all artifacts
run: |
find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';'
ls -la artifacts/
- name: Load PR Number
id: load-pr-num
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
- name: Find head heap snapshot artifact
id: find-heap-snapshot-artifact
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const run_id = context.payload.workflow_run.id;
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id,
});
const artifact = artifacts.find(artifact => artifact.name === 'backend-memory-head-heap-snapshot');
if (artifact == null) return;
core.setOutput('url', `https://github.com/${owner}/${repo}/actions/runs/${run_id}/artifacts/${artifact.id}`);
- name: Output base
run: cat ./artifacts/memory-base.json
- name: Output head
run: cat ./artifacts/memory-head.json
- name: Compare memory usage
id: compare
run: |
BASE_MEMORY=$(cat ./artifacts/memory-base.json)
HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
variation() {
calc() {
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
DIFF=$((HEAD - BASE))
if [ "$BASE" -gt 0 ]; then
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
else
DIFF_PERCENT=0
fi
# Convert KB to MB for readability
BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
JSON=$(jq -c -n \
--argjson base "$BASE_MB" \
--argjson head "$HEAD_MB" \
--argjson diff "$DIFF_MB" \
--argjson diff_percent "$DIFF_PERCENT" \
'{base: $base, head: $head, diff: $diff, diff_percent: $diff_percent}')
echo "$JSON"
}
JSON=$(jq -c -n \
--argjson VmRSS "$(calc $1 VmRSS)" \
--argjson VmHWM "$(calc $1 VmHWM)" \
--argjson VmSize "$(calc $1 VmSize)" \
--argjson VmData "$(calc $1 VmData)" \
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
echo "$JSON"
}
JSON=$(jq -c -n \
--argjson beforeGc "$(variation beforeGc)" \
--argjson afterGc "$(variation afterGc)" \
--argjson afterRequest "$(variation afterRequest)" \
'{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
echo "res=$JSON" >> "$GITHUB_OUTPUT"
- id: build-comment
name: Build memory comment
env:
MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_HEAD: ${{ steps.find-heap-snapshot-artifact.outputs.url }}
run: node .github/scripts/backend-memory-report.mts ./artifacts/memory-base.json ./artifacts/memory-head.json ./output.md ./artifacts/js-footprint-base.json ./artifacts/js-footprint-head.json
RES: ${{ steps.compare.outputs.res }}
run: |
HEADER="## Backend memory usage comparison"
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
echo "$HEADER" > ./output.md
echo >> ./output.md
table() {
echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
echo "|--------|------:|------:|------:|------:|" >> ./output.md
line() {
METRIC=$2
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
DIFF="+$DIFF"
DIFF_PERCENT="+$DIFF_PERCENT"
fi
# highlight VmRSS
if [ "$2" = "VmRSS" ]; then
METRIC="**${METRIC}**"
BASE="**${BASE}**"
HEAD="**${HEAD}**"
DIFF="**${DIFF}**"
DIFF_PERCENT="**${DIFF_PERCENT}**"
fi
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
}
line $1 VmRSS
line $1 VmHWM
line $1 VmSize
line $1 VmData
}
echo "### Before GC" >> ./output.md
table beforeGc
echo >> ./output.md
echo "### After GC" >> ./output.md
table afterGc
echo >> ./output.md
echo "### After Request" >> ./output.md
table afterRequest
echo >> ./output.md
# Determine if this is a significant change (more than 5% increase)
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
echo >> ./output.md
fi
echo "$FOOTER" >> ./output.md
- uses: thollander/actions-comment-pull-request@v3
with:
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}

View File

@@ -22,12 +22,12 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0
@@ -37,7 +37,7 @@ jobs:
if: github.event_name == 'pull_request_target'
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:

View File

@@ -19,6 +19,12 @@ on:
- .github/workflows/test-backend.yml
- .github/misskey/test.yml
workflow_dispatch:
inputs:
force_ffmpeg_cache_update:
description: 'Force update ffmpeg cache'
required: false
default: false
type: boolean
jobs:
unit:
@@ -43,7 +49,7 @@ jobs:
ports:
- 56312:6379
meilisearch:
image: getmeili/meilisearch:v1.48.1
image: getmeili/meilisearch:v1.42.1
ports:
- 57712:7700
env:
@@ -51,14 +57,41 @@ jobs:
MEILI_ENV: development
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Setup and Restore ffmpeg/ffprobe Cache
id: cache-ffmpeg
uses: actions/cache@v5
with:
path: |
/usr/local/bin/ffmpeg
/usr/local/bin/ffprobe
# daily cache
key: ${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }}
restore-keys: |
${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }}
- name: Install FFmpeg
if: steps.cache-ffmpeg.outputs.cache-hit != 'true' || github.event.inputs.force_ffmpeg_cache_update == true
run: |
sudo apt install -y ffmpeg
for i in {1..3}; do
echo "Attempt $i: Installing FFmpeg..."
curl -s -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o ffmpeg.tar.xz && \
tar -xf ffmpeg.tar.xz && \
mv ffmpeg-*-static/ffmpeg /usr/local/bin/ && \
mv ffmpeg-*-static/ffprobe /usr/local/bin/ && \
rm -rf ffmpeg.tar.xz ffmpeg-*-static/ && \
break || sleep 10
if [ $i -eq 3 ]; then
echo "Failed to install FFmpeg after 3 attempts"
exit 1
fi
done
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:
@@ -103,11 +136,11 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:
@@ -147,11 +180,11 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT

View File

@@ -15,6 +15,12 @@ on:
- packages/misskey-js/**
- .github/workflows/test-federation.yml
workflow_dispatch:
inputs:
force_ffmpeg_cache_update:
description: 'Force update ffmpeg cache'
required: false
default: false
type: boolean
jobs:
test:
@@ -30,10 +36,37 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Setup and Restore ffmpeg/ffprobe Cache
id: cache-ffmpeg
uses: actions/cache@v5
with:
path: |
/usr/local/bin/ffmpeg
/usr/local/bin/ffprobe
# daily cache
key: ${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }}
restore-keys: |
${{ runner.os }}-ffmpeg-${{ steps.current-date.outputs.today }}
- name: Install FFmpeg
if: steps.cache-ffmpeg.outputs.cache-hit != 'true' || github.event.inputs.force_ffmpeg_cache_update == true
run: |
sudo apt install -y ffmpeg
for i in {1..3}; do
echo "Attempt $i: Installing FFmpeg..."
curl -s -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o ffmpeg.tar.xz && \
tar -xf ffmpeg.tar.xz && \
mv ffmpeg-*-static/ffmpeg /usr/local/bin/ && \
mv ffmpeg-*-static/ffprobe /usr/local/bin/ && \
rm -rf ffmpeg.tar.xz ffmpeg-*-static/ && \
break || sleep 10
if [ $i -eq 3 ]; then
echo "Failed to install FFmpeg after 3 attempts"
exit 1
fi
done
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:

View File

@@ -28,11 +28,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:
@@ -57,6 +57,11 @@ jobs:
name: E2E tests (frontend)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chrome]
services:
postgres:
image: postgres:18
@@ -71,11 +76,17 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150
#- name: Install mplayer for FireFox
# run: sudo apt install mplayer -y
# if: ${{ matrix.browser == 'firefox' }}
#- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:
@@ -86,14 +97,29 @@ jobs:
run: cp .github/misskey/test.yml .config
- name: Build
run: pnpm build
- name: Playwright install
working-directory: packages/frontend
run: pnpm exec playwright install --with-deps chromium
- name: Test
run: pnpm start-server-and-test start:test http://localhost:61812 "pnpm --filter frontend test:e2e"
# https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
- name: ALSA Env
run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
# XXX: This tries reinstalling Cypress if the binary is not cached
# Remove this when the cache issue is fixed
- name: Cypress install
run: pnpm exec cypress install
- name: Cypress run
uses: cypress-io/github-action@v7.1.9
timeout-minutes: 15
with:
install: false
start: pnpm start:test
wait-on: 'http://localhost:61812'
headed: true
browser: ${{ matrix.browser }}
- uses: actions/upload-artifact@v7
if: failure()
with:
name: playwright-e2e-artifacts
path: packages/frontend/test/e2e/artifacts
name: ${{ matrix.browser }}-cypress-screenshots
path: cypress/screenshots
- uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.browser }}-cypress-videos
path: cypress/videos

View File

@@ -22,10 +22,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.3
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Setup Node.js
uses: actions/setup-node@v6.4.0

View File

@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:

View File

@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.3
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.4.0
with:

7
.gitignore vendored
View File

@@ -23,8 +23,9 @@ packages/sw/.yarn/cache
# pnpm
.pnpm-store
# Playwright
packages/frontend/test/e2e/artifacts
# Cypress
cypress/screenshots
cypress/videos
# Coverage
coverage
@@ -34,7 +35,7 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
!/.config/playwright-devcontainer.yml
!/.config/cypress-devcontainer.yml
docker-compose.yml
./compose.yml
.devcontainer/compose.yml

View File

@@ -1 +1 @@
26.4.0
22.15.0

View File

@@ -63,16 +63,14 @@
9. **ユーザーの明示指示なしに PR を merge / close / force-push しない**
10. **ユーザーの明示指示なしに external service (GitHub comments / Slack / メール 等) へ送信しない**
11. **secrets / 認証情報をリポジトリにコミットしない** (`.config/*.yml` の本番値、`.env` ファイル、API token、private key 等)
12. **脆弱性報告を通常の Issue / PR 経由で行わない** (脆弱性報告を行う場合のルールは `creating-issues-and-prs` スキルを参照すること)
### スキル呼び出し
上流スキルの実行・事前知識・memory の内容に関わらず免除されない。
13. **`working-on-backend` スキルを参照せずに `packages/backend/` 配下のファイルを編集・追加しない**
14. **`working-on-frontend` スキルを参照せずに `packages/frontend/` 配下のファイルを編集・追加しない**
15. **`shipping-misskey-change` スキルを参照せずに commit / PR 作成 / 作業をユーザーに返さない**
16. **`creating-issues-and-prs` スキルを参照せずに Issue / PR を起票しない** (脆弱性報告のルールも含む)
12. **`working-on-backend` スキルを参照せずに `packages/backend/` 配下のファイルを編集・追加しない**
13. **`working-on-frontend` スキルを参照せずに `packages/frontend/` 配下のファイルを編集・追加しない**
14. **`shipping-misskey-change` スキルを参照せずに commit / PR 作成 / 作業をユーザーに返さない**
---

View File

@@ -1,55 +1,3 @@
## 2026.7.0
### Note
**今回のリリースではMisskeyの各種動作要件が変更されます。必ずアップグレード前にお使いの環境をご確認ください。**
- センシティブメディアの判定 (NSFW検出) が、本体に内蔵された nsfwjs による推論から、外部サービス [sensitive-detector](https://github.com/misskey-dev/sensitive-detector) への HTTP 呼び出し方式に変更されました。
- これに伴い、本体から `nsfwjs` / `@tensorflow/tfjs` / `@tensorflow/tfjs-node` および同梱の NSFW 判定モデルが削除され、インストール要件 (ネイティブ ML スタック) が緩和されました。
- **センシティブ判定機能を利用しているサーバーは対応が必要です。** 別途 [sensitive-detector](https://github.com/misskey-dev/sensitive-detector) サービスを立ち上げ、コントロールパネルの「モデレーション > センシティブなメディアの検出」で接続先 URL を設定してください。接続先が未設定の場合、センシティブ判定は行われません (すべて非センシティブ扱い)。
- 画像の正規化・動画フレームの抽出・しきい値判定・集約は引き続き本体側で行われ、外部サービスには正規化済み画像の推論のみを委譲します。
- Node.js v24, v26 をサポートしました。**Node.js v22 でも動作しますが、今後のリリースで v22 のサポートを終了する予定**ですので、Node.js のアップデートをご検討ください。
- Node.js のセキュリティアップデートに伴い、最低動作バージョンを 22.22.2 / 24.17.0 / 26.4.0 に引き上げました。
- Docker Image は Node.js 26.4.0-trixie に更新されています。
- バックエンドで画像処理に用いているライブラリ sharp のシステム要件の変更により、**SSE4.2 命令セットをサポートしていない x86_64 CPU では Misskey が正しく動作しなくなります**。仮想マシンに Misskey をデプロイしている場合や、古いハードウェアをお使いの場合は、アップデート前にお使いの環境をご確認ください。なお、ARM64 など x86_64 ではない環境においてはこの変更による影響はありません。
### General
- Feat: コントロールパネルから二要素認証を解除できるように
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
(Based on https://github.com/MisskeyIO/misskey/pull/214)
### Client
- 2025.4.0 以前の設定情報の移行処理が削除されました
- 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。
- Enhance: 画像ビューワーを独自実装に変更・動画プレイヤーを統合
- 操作性の改善
- ビューワーとMisskeyの各種機能との統合を強化
- パフォーマンスの向上
- Enhance: マウスホイールで拡大縮小できるように
- Fix: 幅が狭い画面で動画の再生が困難な問題を修正
- Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正
- Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正
- Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
- Fix: 自分へのメンションに対する色分けで、判定が大文字/小文字を区別していた問題を修正
- Fix: いくつかのイベントリスナーが正しく解除されない問題を修正(メモリ使用量の改善)
- Fix: 非ログイン時トップページをスクロール操作できないことがある問題を修正
### Server
- Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804)
- Enhance: バックエンドの `otelForBackend` 設定で OpenTelemetry Traces を OTLP Collector に送信できるように
- Enhance: OpenTelemetry を単独で利用する際、外向き HTTP リクエストを自動計装するように
- Enhance: OpenTelemetry 利用時に BullMQ ジョブを enqueue 元の trace と Span Link で関連付けられるように
- Enhance: Sentry バックエンドの自動計装を `sentryForBackend.disabledIntegrations` で個別に無効化できるように
- Enhance: Node.js 22.23.0以降、24.17.0以降、26.4.0以降をサポートするように
- Enhance: Docker Image の Node.js を 26.4.0 に、Debian を trixie (v13) に更新
- Enhance: URLプレビューの結果を内部でキャッシュするように
- Fix: `/stats` API のレスポンス型が正しくない問題を修正
- Fix: ハッシュタグに関連するデータを更新する際のエラーハンドリングを修正
- Fix: Sentry 使用環境下にて、Misskey が発行した SQL クエリが span に含まれない問題を修正
- Fix: Sentry 使用環境下にて、外部送信リクエストへ `sentry-trace` / `baggage` ヘッダーが既定で付与されないように
## 2026.6.0
### General
@@ -77,16 +25,12 @@
- Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善
- Enhance: リモートノートクリーニングジョブの削除対象検索処理のパフォーマンス改善
- Enhance: ActivityPub の画像添付に width/height を含めるように
- Enhance: URLプレビューのデフォルトの User Agent に Misskey サーバーのURLを含めるように
- Fix: backend バンドルで `@tensorflow/tfjs-node` を external に含めず、起動時に `@mapbox/node-pre-gyp``find()` が backend の package.json を誤検出して `is not node-pre-gyp ready` エラーを永続的に吐く問題を修正
- Fix: MemoryKVCacheのキャッシュGC処理において、更新されたキャッシュが期限切れにならないことがある問題を修正
- Fix: PerUserDriveChart がシステム所有ファイル (userId が null) の更新で `"group"` の非NULL制約違反によりクラッシュする問題を修正 (#17498)
- Fix: センシティブメディア自動検出周りの依存関係・ファイルの解決に失敗する問題を修正
- Fix: フォロワー限定投稿を指名投稿で引用した際に、引用した投稿の公開範囲が意図せず変更される問題を修正
- Fix: `actor` を持たない不正なInboxアクティビティを受信した際に配送ジョブが `TypeError` でクラッシュする問題を修正 (受信時に検証して400で返し、ジョブを積まないように変更)
- Fix: Startup and shutdown failures (port-in-use, socket permission denied, plugin timeouts, leaked WebSocket connections) are now reported through the misskey logger instead of an UnhandledPromiseRejectionWarning stack trace
- Fix: リモートのノートに対するメンション数制限が、サーバーが解決できたユーザー数ベースで行われていた問題を修正
- Fix: セキュリティに関する修正
## 2026.5.4

View File

@@ -600,90 +600,6 @@ TypeScriptでjsonをimportすると、tscでコンパイルするときにその
コンポーネント自身がmarginを設定するのは問題の元となることはよく知られている
marginはそのコンポーネントを使う側が設定する
### 命名規則
本来それが略称であっても、通常それでひとつのワードとして用いられるものは、略称として扱わない。
#### 例: IP address
Good: `ipAddress` / `IpAddress`
Bad: `IPAddress`
#### 例: User ID
Good: `userId` / `UserId`
Bad: `userID` / `UserID`
#### 例: XMLなHTTPのRequest
Good: `xmlHttpRequest` / `XmlHttpRequest`
Bad: `XMLHttpRequest` / `XMLHTTPRequest`
### 関数化の基準
汎用性が低く(例えばそれを関数化したとしてもその呼び出しが元の場所一か所しか存在しない)、内容も短い処理(例えば10行以下)は、かえって読みにくくなるため、関数化しない。
また、関数化する場合でも、呼び出しがある特定のスコープに限られる場合は、そのスコープ内に閉じ込めた方が分かりやすく簡潔になる場合がある(ただし本来その処理に不要であっても、構造上親のスコープにある関係のない変数や引数にもアクセスできるようになるため、必ずしもそうすれば設計上綺麗になるというわけでもない。状況に応じて判断すべし)。
Bad:
``` ts
function withBrankets(x) {
return `(${x})`;
}
function formatPercent(x) {
return `${x}%`;
}
function formatValue(x) {
return withBrankets(formatPercent(x));
}
function showData(a, b) {
console.log(formatValue(a));
console.log(formatValue(b));
}
```
Good:
``` ts
function formatValue(x) {
return `(${x}%)`;
}
function showData(a, b) {
console.log(formatValue(a));
console.log(formatValue(b));
}
```
or
``` ts
function showData(a, b) {
function formatValue(x) {
return `(${x}%)`;
}
console.log(formatValue(a));
console.log(formatValue(b));
}
```
or
``` ts
function showData(a, b) {
console.log(`(${a}%)`);
console.log(`(${b}%)`);
}
```
## その他
### HTMLのクラス名で follow という単語は使わない
広告ブロッカーで誤ってブロックされる

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.23
ARG NODE_VERSION=26.4.0-trixie
ARG NODE_VERSION=22.22.2-bookworm
# build assets & compile TypeScript
@@ -30,6 +30,8 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
ARG NODE_ENV=production
@@ -61,6 +63,8 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
ARG NODE_ENV=production
@@ -99,12 +103,18 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/node_modules ./packages/misskey-world/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/node_modules ./packages/frontend-misskey-world-engine/node_modules
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/built ./packages/misskey-world/built
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/built ./packages/frontend-misskey-world-engine/built
COPY --chown=misskey:misskey . ./
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so

View File

@@ -224,8 +224,9 @@ id: "aidx"
# Media Proxy
#mediaProxy: https://example.com/proxy
allowedPrivateNetworks:
- '127.0.0.1/32'
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

7
cypress.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:61812',
},
})

265
cypress/e2e/basic.cy.ts Normal file
View File

@@ -0,0 +1,265 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Before setup instance', () => {
beforeEach(() => {
cy.resetState();
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.visitHome();
});
it('setup instance', () => {
cy.visitHome();
cy.intercept('POST', '/api/admin/accounts/create').as('signup');
cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked');
cy.get('[data-cy-admin-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click();
// なぜか動かない
//cy.wait('@signup').should('have.property', 'response.statusCode');
cy.wait('@signup');
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
cy.get('[data-cy-next]').click();
cy.get('[data-cy-server-name] input').type('Testskey');
cy.get('[data-cy-server-setup-wizard-apply]').click();
cy.wait('@update-meta');
});
});
describe('After setup instance', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.visitHome();
});
it('signup', () => {
cy.visitHome();
cy.intercept('POST', '/api/signup').as('signup');
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-invitation-code] input').type('test-invitation-code');
cy.get('[data-cy-signup-submit]').should('not.be.disabled');
cy.get('[data-cy-signup-submit]').click();
cy.wait('@signup');
});
it('signup with duplicated username', () => {
cy.registerUser('alice', 'alice1234');
cy.visitHome();
// ユーザー名が重複している場合の挙動確認
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
});
});
describe('After user signup', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.visitHome();
});
it('signin', () => {
cy.visitHome();
cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-username] input').type('alice{enter}');
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin');
});
it('suspend', function() {
cy.request('POST', '/api/admin/suspend-user', {
i: this.admin.token,
userId: this.alice.id,
});
cy.visitHome();
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type('alice{enter}');
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
});
});
describe('After user signed in', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible');
});
it('account setup wizard', () => {
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click();
cy.get('[data-cy-user-setup-user-name] input').type('ありす');
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
// TODO: アイコン設定テスト
cy.get('[data-cy-user-setup-continue]').click();
// プライバシー設定
cy.get('[data-cy-user-setup-continue]').click();
// フォローはスキップ
cy.get('[data-cy-user-setup-continue]').click();
// プッシュ通知設定はスキップ
cy.get('[data-cy-user-setup-continue]').click();
cy.get('[data-cy-user-setup-continue]').click();
});
});
describe('After user setup', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click();
cy.get('[data-cy-modal-dialog-ok]').click();
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('note', () => {
cy.get('[data-cy-open-post-form]').should('be.visible');
cy.get('[data-cy-open-post-form]').click();
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click();
cy.contains('Hello, Misskey!', { timeout: 15000 });
});
it('open note form with hotkey', () => {
// Wait until the page loads
cy.get('[data-cy-open-post-form]').should('be.visible');
// Use trigger() to give different `code` to test if hotkeys also work on non-QWERTY keyboards.
cy.document().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "n", code: "KeyL" });
// See if the form is opened
cy.get('[data-cy-post-form-text]').should('be.visible');
// Close it
cy.focused().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "Escape", code: "Escape" });
// See if the form is closed
cy.get('[data-cy-post-form-text]').should('not.be.visible');
});
});
// TODO: 投稿フォームの公開範囲指定のテスト
// TODO: 投稿フォームのファイル添付のテスト
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト

35
cypress/e2e/router.cy.ts Normal file
View File

@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Router transition', () => {
describe('Redirect', () => {
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う使いまわした方が早い
before(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click();
cy.wait(500);
cy.get('[data-cy-modal-dialog-ok]').click();
});
it('redirect to user profile', () => {
// テストのためだけに用意されたリダイレクト用ルートに飛ぶ
cy.visit('/redirect-test');
// プロフィールページのURLであることを確認する
cy.url().should('include', '/@alice')
});
});
});

76
cypress/e2e/widgets.cy.ts Normal file
View File

@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* flaky
describe('After user signed in', () => {
beforeEach(() => {
cy.resetState();
cy.viewport('macbook-16');
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('widget edit toggle is visible', () => {
cy.get('[data-cy-widget-edit]').should('be.visible');
});
it('widget select should be visible in edit mode', () => {
cy.get('[data-cy-widget-edit]').click();
cy.get('[data-cy-widget-select]').should('be.visible');
});
it('first widget should be removed', () => {
cy.get('[data-cy-widget-edit]').click();
cy.get('[data-cy-customize-container]:first-child [data-cy-customize-container-remove]._button').click();
cy.get('[data-cy-customize-container]').should('have.length', 2);
});
function buildWidgetTest(widgetName) {
it(`${widgetName} widget should get added`, () => {
cy.get('[data-cy-widget-edit]').click();
cy.get('[data-cy-widget-select] select').select(widgetName, { force: true });
cy.get('[data-cy-bg]._modalBg[data-cy-transparent]').click({ multiple: true, force: true });
cy.get('[data-cy-widget-add]').click({ force: true });
cy.get(`[data-cy-mkw-${widgetName}]`).should('exist');
});
}
buildWidgetTest('memo');
buildWidgetTest('notifications');
buildWidgetTest('timeline');
buildWidgetTest('calendar');
buildWidgetTest('rss');
buildWidgetTest('trends');
buildWidgetTest('clock');
buildWidgetTest('activity');
buildWidgetTest('photos');
buildWidgetTest('digitalClock');
buildWidgetTest('federation');
buildWidgetTest('postForm');
buildWidgetTest('slideshow');
buildWidgetTest('serverMetric');
buildWidgetTest('onlineUsers');
buildWidgetTest('jobQueue');
buildWidgetTest('button');
buildWidgetTest('aiscript');
buildWidgetTest('aichan');
});
*/

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,67 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.add('visitHome', () => {
cy.visit('/');
cy.get('button', { timeout: 30000 }).should('be.visible');
})
Cypress.Commands.add('resetState', () => {
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
/*
cy.window().then(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
*/
cy.request('POST', '/api/reset-db', {}).as('reset');
cy.get('@reset').its('status').should('equal', 204);
cy.reload(true);
});
Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup';
cy.request('POST', route, {
username: username,
password: password,
...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
}).its('body').as(username);
});
Cypress.Commands.add('login', (username, password) => {
cy.visitHome();
cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
cy.wait('@signin').as('signedIn');
});

34
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,34 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.on('uncaught:exception', (err, runnable) => {
if ([
'The source image cannot be decoded',
// Chrome
'ResizeObserver loop limit exceeded',
// Firefox
'ResizeObserver loop completed with undelivered notifications',
].some(msg => err.message.includes(msg))) {
return false;
}
});

19
cypress/support/index.ts Normal file
View File

@@ -0,0 +1,19 @@
declare global {
namespace Cypress {
interface Chainable {
login(username: string, password: string): Chainable<void>;
registerUser(
username: string,
password: string,
isAdmin?: boolean
): Chainable<void>;
resetState(): Chainable<void>;
visitHome(): Chainable<void>;
}
}
}
export {}

8
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"lib": ["dom"],
"target": "esnext",
"types": ["cypress", "node"]
},
"include": ["./**/*.ts"]
}

View File

@@ -580,7 +580,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
s3ForcePathStyleDesc: "Si s3ForcePathStyle esta habilitado el nombre del bucket debe ser especificado como parte de la URL en lugar del nombre de host en la URL. Puede ser necesario activar esta opción cuando se utilice, por ejemplo, Minio en un servidor propio."
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar formulario de publicación sobre la línea de tiempo."
showFixedPostForm: "Visualizar la ventana de publicación en la parte superior de la línea de tiempo."
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
withRepliesByDefaultForNewlyFollowed: "Incluir por defecto respuestas de usuarios recién seguidos en la línea de tiempo"
newNoteRecived: "Tienes una nota nueva"
@@ -2657,7 +2657,7 @@ _postForm:
submit_title: "Botón de publicar"
submit_description: "Publica tus notas pulsando este botón. También puedes publicar utilizando Ctrl + Intro / Cmd + Intro."
_placeholders:
a: "¿Qué está pasando?"
a: "What are you up to?"
b: "¿Te pasó algo?"
c: "¿Qué estás pensando?"
d: "¿Algo que quieras decir?"

View File

@@ -132,16 +132,16 @@ sensitive: "Konten sensitif"
add: "Tambahkan"
reaction: "Reaksi"
reactions: "Reaksi"
emojiPicker: "Palet emoji"
pinnedEmojisForReactionSettingDescription: "Atur emoji yang akan disematkan dan ditampilkan saat memberi reaksi."
pinnedEmojisSettingDescription: "Atur emoji yang akan disematkan dan ditampilkan saat melihat palet emoji"
emojiPickerDisplay: "Tampilan palet emoji"
emojiPicker: "Emoji Picker"
pinnedEmojisForReactionSettingDescription: "Atur sematan emoji pada reaksi"
pinnedEmojisSettingDescription: "Atur sematan emoji pada masukan emoji"
emojiPickerDisplay: "Tampilan Emoji Picker"
overwriteFromPinnedEmojisForReaction: "Timpa dari pengaturan reaksi"
overwriteFromPinnedEmojis: "Timpa dari pengaturan umum"
reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan"
rememberNoteVisibility: "Ingat pengaturan visibilitas catatan"
attachCancel: "Hapus lampiran"
deleteFile: "Hapus berkas"
deleteFile: "Berkas dihapus"
markAsSensitive: "Tandai sebagai konten sensitif"
unmarkAsSensitive: "Hapus tanda konten sensitif"
enterFileName: "Masukkan nama berkas"
@@ -162,7 +162,7 @@ editList: "Sunting daftar"
selectChannel: "Pilih kanal"
selectAntenna: "Pilih Antena"
editAntenna: "Sunting antena"
createAntenna: "Membuat antena"
createAntenna: "Membuat antena."
selectWidget: "Pilih gawit"
editWidgets: "Sunting gawit"
editWidgetsExit: "Selesai"
@@ -301,7 +301,7 @@ uploadFromUrl: "Unggah dari URL"
uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
uploadFromUrlRequested: "Pengunggahan telah diminta"
uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesai"
uploadNFiles: "Unggah berkas {n}"
uploadNFiles: "Unggah file {n}"
explore: "Jelajahi"
messageRead: "Telah dibaca"
readAllChatMessages: "Tandai semua pesan menjadi terbaca"
@@ -339,7 +339,7 @@ selectFiles: "Pilih berkas"
selectFolder: "Pilih folder"
unselectFolder: "Membatalkan seleksi folder"
selectFolders: "Pilih folder"
fileNotSelected: "Tidak ada berkas yang terpilih"
fileNotSelected: "Tidak ada file yang dipilih"
renameFile: "Ubah nama berkas"
folderName: "Nama folder"
createFolder: "Buat folder"
@@ -350,7 +350,7 @@ addFile: "Tambahkan berkas"
showFile: "Tampilkan berkas"
emptyDrive: "Drive kosong"
emptyFolder: "Folder kosong"
dropHereToUpload: "Lepas berkas di sini untuk diunggah"
dropHereToUpload: "Lepas file di sini untuk diunggah"
unableToDelete: "Tidak dapat menghapus"
inputNewFileName: "Masukkan nama berkas yang baru"
inputNewDescription: "Masukkan keterangan disini"
@@ -1012,7 +1012,7 @@ failedToUpload: "Gagal mengunggah"
cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW."
cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive."
cannotUploadBecauseExceedsFileSizeLimit: "Berkas ini tidak dapat diunggah karena melebihi batas ukuran berkas."
cannotUploadBecauseUnallowedFileType: "Tidak dapat mengunggah karena tipe berkas yang tidak diijinkan."
cannotUploadBecauseUnallowedFileType: "Tidak dapat mengunggah karena tipe file yang tidak diijinkan."
beta: "Beta"
enableAutoSensitive: "Penandaan NSFW otomatis"
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Pembelajaran Mesin jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
@@ -1256,7 +1256,6 @@ releaseToRefresh: "Lepaskan untuk memuat ulang"
refreshing: "Sedang memuat ulang..."
pullDownToRefresh: "Tarik ke bawah untuk memuat ulang"
useGroupedNotifications: "Tampilkan notifikasi secara dikelompokkan"
emailVerificationFailedError: "Ada masalah saat memverifikasi alamat surel anda. Tautannya mungkin sudah kadaluarsa."
cwNotationRequired: "Jika \"Sembunyikan konten\" diaktifkan, deskripsi harus disediakan."
doReaction: "Tambahkan reaksi"
code: "Kode"
@@ -1290,13 +1289,12 @@ useTotp: "Gunakan TOTP"
useBackupCode: "Gunakan kode cadangan"
launchApp: "Luncurkan Aplikasi"
useNativeUIForVideoAudioPlayer: "Gunakan antarmuka peramban ketika memainkan video dan audio"
keepOriginalFilename: "Gunakan nama asli berkas"
keepOriginalFilename: "Simpan nama berkas asli"
keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas akan diganti dengan string acak secara otomatis ketika kamu mengunggah berkas."
noDescription: "Tidak ada deskripsi"
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
inquiry: "Hubungi kami"
tryAgain: "Silahkan coba lagi."
confirmWhenRevealingSensitiveMedia: "Konfirmasi saat membuka media sensitif"
sensitiveMediaRevealConfirm: "Media sensitif. Apakah ingin melihat?"
createdLists: "Senarai yang dibuat"
createdAntennas: "Antena yang dibuat"
@@ -1319,7 +1317,6 @@ federationSpecified: "Peladen ini dioperasikan dalam federasi daftar putih. Inte
federationDisabled: "Federasi dimatikan di peladen ini. Anda tidak dapat berinteraksi dengan pengguna di peladen lain."
draft: "Draf"
draftsAndScheduledNotes: "Draf dan note terjadwal"
preferencesProfile: "Pengaturan profil"
noName: "Tidak ada nama"
skip: "Lewati"
restore: "Kembalikan"
@@ -1333,10 +1330,7 @@ directMessage: "Obrolan pengguna"
right: "Kanan"
bottom: "Bawah"
top: "Atas"
driveAboutTip: "Dalam Drive, daftar berkas yang telah anda unggah sebelumnya akan ditampilkan. <br>\nAnda dapat menggunakan kembali berkas-berkas tersebut dalam lampiran note, atau mengunggah berkas sekarang untuk dipublikasikan nanti. <br>\n<b>Harap berhati-hati ketika menghapus berkas, karena berkas tersebut akan tidak bisa diakses di semua tempat yang menggunakan berkas tersebut (seperti note, halaman, avatar, banner, dll.)</b><br>\nAnda juga dapat membuat folder untuk menata berkas-berkas anda."
advice: "Saran"
defaultImageCompressionLevel_description: "Level yang rendah akan menjaga kualitas gambar namun memperbesar ukuran berkas.<br>Level yang tinggi akan mengurangi ukuran berkas, namun mengurangi kualitas gambar."
defaultCompressionLevel_description: "Kompresi yang rendah akan menjaga kualitas namun memperbesar ukuran berkas. Kompresi yang tinggi akan mengurangi ukuran berkas namun mengurangi kualitas."
inMinutes: "menit"
inDays: "hari"
widgets: "Widget"
@@ -1344,9 +1338,7 @@ presets: "Prasetel"
previewingThemeRestore: "Kembalikan"
_imageEditing:
_vars:
caption: "Keterangan berkas"
filename: "Nama berkas"
filename_without_ext: "Nama berkas tanpa ekstensi"
_imageFrameEditor:
header: "Header"
withQrCode: "QR Code"
@@ -1363,23 +1355,16 @@ _chat:
send: "Kirim"
chatWithThisUser: "Obrolan pengguna"
_settings:
driveBanner: "Anda dapat mengelola dan mengatur drive, melihat penggunaan, dan mengatur pengaturan unggahan berkas."
notificationsBanner: "Anda dapat mengatur tipe dan rentang notifikasi dari peladen dan notifikasi push."
webhook: "Webhook"
contentsUpdateFrequency: "Frekuensi pembaruan konten"
_preferencesProfile:
profileName: "Nama profil"
profileNameDescription: "Tulis nama untuk mengidentifikasi perangkat ini."
profileNameDescription2: "Contoh: \"PC Utama\", \"Smartphone\""
manageProfiles: "Kelola Profil"
shareSameProfileBetweenDevicesIsNotRecommended: "Kami tidak menyarankan menggunakan profil yang sama diantara beberapa perangkat yang berbeda."
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "Jika terdapat pengaturan yang ingin anda sinkronkan diantara beberapa perangkat yang berbeda, nyalakan opsi \"Sinkronisasi pada perangkat yang berbeda\" satu per satu untuk setiap perangkat."
_preferencesBackup:
autoBackup: "Pencadangan otomatis"
restoreFromBackup: "Kembalikan dari pencadangan"
noBackupsFoundDescription: "Tidak ada pencadangan otomatis yang ditemukan, namun jika anda pernah membuat cadangan secara manual, anda bisa mengimpor dan mengembalikan pencadangan tersebut."
selectBackupToRestore: "Pilih pencadangan untuk dikembalikan"
youNeedToNameYourProfileToEnableAutoBackup: "Nama profil harus dibuat untuk menyalakan cadangan otomatis."
_accountSettings:
makeNotesFollowersOnlyBeforeDescription: "Ketika fitur ini diaktifkan, hanya pengikut yang dapat melihat note sebelum tanggal dan waktu yang ditentukan atau telah terlihat untuk waktu tertentu. Setelah dinonaktifkan, status publikasi note juga akan dikembalikan seperti semula."
makeNotesHiddenBeforeDescription: "Saat fitur ini diaktifkan, note sebelum tanggal dan waktu tertentu hanya akan terlihat oleh anda. Setelah dinonaktifkan, status publikasi note juga akan dikembalikan seperti semula."
@@ -1425,7 +1410,7 @@ _announcement:
silenceDescription: "Apabila diaktifkan, notifikasi dari pengumuman ini akan dilewatkan dan pengguna tidak perlu membacanya."
_initialAccountSetting:
accountCreated: "Akun kamu telah sukses dibuat!"
letsStartAccountSetup: "Pertama-tama, ayo atur profilmu dulu."
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
letsFillYourProfile: "Pertama, ayo atur profilmu dulu."
profileSetting: "Pengaturan profil"
privacySetting: "Pengaturan privasi"
@@ -1523,8 +1508,6 @@ _serverSettings:
reactionsBufferingDescription: "Ketika diaktifkan, performa saat membuat reaksi akan meningkat drastis, mengurangi beban database. Namun, penggunaan memori Redis akan meningkat."
remoteNotesCleaning_description: "Ketika diaktifkan, note yang tidak terpakai dan kadaluarsa dari instansi luar akan dibersihkan secara berkala untuk mencegah membengkaknya database."
inquiryUrlDescription: "Cantumkan URL untuk menghubungi pengelola peladen atau laman web berisikan informasi kontak."
proxyRemoteFiles: "Berkas proksi remote"
proxyRemoteFiles_description: "Ketika dinyalakan, peladen akan berperan sebagai proksi menyajikan berkas secara remote. Ini dapat berguna untuk membuat keluku gambar dan melindungi privasi pengguna."
_accountMigration:
moveFrom: "Pindahkan akun lain ke akun ini"
moveFromSub: "Buat alias ke akun lain"
@@ -1840,9 +1823,6 @@ _role:
canManageCustomEmojis: "Dapat mengelola Emoji kustom"
canManageAvatarDecorations: "Kelola dekorasi avatar"
driveCapacity: "Kapasitas Drive"
maxFileSize: "Ukuran berkas maksimal yang dapat diunggah"
maxFileSize_caption: "Proksi terbalik, CDN, dan komponen antarmuka-depan bisa memiliki pengaturan tersendiri."
maxFileSize_caption2: "Ukuran berkas maksimal di keseluruhan peladen adalah {max}. Untuk memperbolehkan unggahan berkas yang lebih besar dari ini, silahkan mengubah pengaturan ini di dalam berkas pengaturan Misskey."
alwaysMarkNsfw: "Selalu tandai berkas sebagai NSFW"
pinMax: "Jumlah maksimal catatan yang disematkan"
antennaMax: "Jumlah maksimum antena"
@@ -1860,7 +1840,6 @@ _role:
avatarDecorationLimit: "Jumlah maksimum dekorasi avatar yang dapat diterapkan"
canImportAntennas: "Izinkan mengimpor antena"
canImportUserLists: "Izinkan mengimpor senarai"
uploadableFileTypes: "Jenis berkas yang dapat diunggah"
noteDraftLimit: "Jumlah dari draf yang dapat dibuat dari sisi peladen"
_condition:
roleAssignedTo: "Ditugaskan ke peran manual"
@@ -2769,8 +2748,6 @@ _search:
searchScopeAll: "Semua"
searchScopeLocal: "Lokal"
searchScopeUser: "Pengguna spesifik"
_uploader:
allowedTypes: "Jenis berkas yang dapat diunggah"
_watermarkEditor:
driveFileTypeWarn: "Berkas ini tidak didukung"
opacity: "Opasitas"

View File

@@ -1415,11 +1415,6 @@ viewRenotedChannel: "Visualizza il canale del Rinota"
previewingTheme: "Anteprima del Tema"
previewingThemeRestore: "Ripristina"
accessToken: "Codice di accesso"
chooseEmojiPalette: "Scegli la tavolozza emoji"
addToEmojiPalette: "Aggiungi alla tavolozza emoji"
emojiPaletteAlreadyAddedConfirm: "Questa emoji è già inclusa in nella tavolozza. Vuoi davvero aggiungerla?"
append: "Accodare"
prepend: "Anteporre"
_imageEditing:
_vars:
caption: "Didascalia dell'immagine"

View File

@@ -619,8 +619,6 @@ output: "出力"
script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にする"
updateRemoteUser: "リモートユーザー情報の更新"
unsetMfa: "二要素認証を解除"
unsetMfaConfirm: "二要素認証を解除しますか?"
unsetUserAvatar: "アイコンを解除"
unsetUserAvatarConfirm: "アイコンを解除しますか?"
unsetUserBanner: "バナーを解除"
@@ -1363,11 +1361,14 @@ information: "情報"
chat: "チャット"
directMessage: "ダイレクトメッセージ"
directMessage_short: "メッセージ"
migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
compress: "圧縮"
right: "右"
bottom: "下"
top: "上"
embed: "埋め込み"
settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)"
readonly: "読み取り専用"
goToDeck: "デッキへ戻る"
federationJobs: "連合ジョブ"
@@ -1414,13 +1415,13 @@ viewRenotedChannel: "リノート先のチャンネルを見る"
previewingTheme: "テーマのプレビュー中"
previewingThemeRestore: "元に戻す"
accessToken: "アクセストークン"
choose: "選択"
rotate: "回転"
chooseEmojiPalette: "絵文字パレットを選択"
addToEmojiPalette: "絵文字パレットに追加"
emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?"
append: "末尾に追加"
prepend: "先頭に追加"
urlPreviewSensitiveList: "サムネイルの表示を制限するURL"
urlPreviewSensitiveListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルが表示されなくなります。"
_imageEditing:
_vars:
@@ -2178,15 +2179,6 @@ _sensitiveMediaDetection:
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
analyzeVideos: "動画の解析を有効化"
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
externalServiceInfo: "センシティブメディアの判定は外部サービス (sensitive-detector) に分離されました。この機能を利用するには、別途サイドカーサービスをセットアップし、下記の接続先を設定する必要があります。接続先が未設定の場合、判定は行われません (非センシティブ扱い)。"
apiUrl: "判定サービスの接続先URL"
apiUrlDescription: "sensitive-detector サービスのベースURL (例: http://localhost:3009)。プライベートネットワーク上のサービスに接続する場合は、設定ファイルの allowedPrivateNetworks で接続先ネットワークを許可してください。プロキシを使用している場合は、proxyBypassHosts も設定してください。空欄の場合、センシティブ判定は行われません。"
apiKey: "APIキー"
apiKeyDescription: "判定サービス側で認証 (Bearerトークン) を設定している場合に入力します。設定していない場合は空欄のままにしてください。"
timeout: "タイムアウト (ミリ秒)"
timeoutDescription: "判定リクエスト1回あたりのタイムアウト時間です。"
maxImagesPerRequest: "1リクエストあたりの最大画像数"
maxImagesPerRequestDescription: "動画など複数フレームを判定する際、1回のリクエストにまとめて送る画像の最大枚数です。これを超える分は分割して順次送信されます。sensitive-detector 側の maxParts 設定(デフォルト: 10を超えないように設定してください。超えた場合、そのチャンクは全件非センシティブ扱いとなります。"
_emailUnavailable:
used: "既に使用されています"
@@ -2527,7 +2519,6 @@ _permissions:
"read:admin:show-moderation-log": "モデレーションログを見る"
"read:admin:show-user": "ユーザーのプライベートな情報を見る"
"write:admin:suspend-user": "ユーザーを凍結する"
"write:admin:unset-mfa": "ユーザーの二要素認証を解除する"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する"
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
@@ -3073,7 +3064,6 @@ _moderationLogTypes:
createAvatarDecoration: "アイコンデコレーションを作成"
updateAvatarDecoration: "アイコンデコレーションを更新"
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetMfa: "ユーザーの二要素認証を解除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
createSystemWebhook: "SystemWebhookを作成"
@@ -3580,3 +3570,476 @@ _qr:
scanFile: "端末の画像をスキャン"
raw: "テキスト"
mfm: "MFM"
worldAvatar: "Worldアバター"
worldAvatar_description: "MisskeyWorld / MisskeyRoomsで使用可能な3Dアバターを作成できます。"
_miWorld:
separateRenderingThread: "描画を別スレッドに分離"
separateRenderingThread_description: "有効にするとパフォーマンスが向上します。不安定になる場合は無効すると改善する可能性があります。"
graphicsQuality: "グラフィックの品質"
graphicsSettings: "グラフィック設定"
frameRateLimitation: "フレームレート制限"
higherValuePerformanceNote: "高くすると体験が向上しますが、消費電力が増加するなどパフォーマンスに影響を与えます。"
resolution: "解像度"
fov: "視野角"
failedToInitialize: "初期化に失敗しました"
crushed_description: "描画が継続できなくなりました。デバイスのリソース不足の可能性が考えられます。"
antialiasing: "アンチエイリアス"
avatar: "アバター"
advancedCustomize: "高度なアレンジ"
attachAccessory: "アクセサリーをつける"
takeScreenShot: "スクリーンショット"
onlineMenu: "オンラインメニュー"
connectToOnline: "オンラインに接続"
disconnectToOnline: "オンラインから切断"
character: "キャラクター"
sit: "座る"
lyingDown: "寝そべる"
standUp: "立ち上がる"
showUsernameOnAvatar: "アバターにユーザー名を表示"
show2dAvatarOnAvatar: "アバターにユーザーアイコンを表示"
_avatars:
_default:
body: "ボディ"
eyes: "目"
mouth: "口"
_avatarAccessories:
mug: "マグカップ"
_mug:
bodyMat: "コップの素材"
liquidMat: "液体の素材"
mikan: "みかん"
bolt: "ボルト"
_bolt:
mat: "素材"
_miRoom:
snapToGrid: "グリッドにスナップ"
gridScale: "グリッドサイズ"
thereAreUnsavedChanges: "未保存の変更があります"
revertAllChangesConfirmation: "全ての変更を取り消し、部屋を最後に保存した状態まで戻しますか?"
yourDeviceNotSupported_title: "MisskeyRoomを起動できません"
yourDeviceNotSupported_description: "お使いのデバイスがMisskeyRoomをサポートしていないか、デバイスのリソース不足などにより一時的に利用できなくなっています。\nMisskeyRoomを動作させるには、WebGPUをサポートするデバイス・ブラウザが必要です。"
imageFit: "画像のはめ込み"
imageFit_cover: "覆う"
imageFit_contain: "収める"
imageFit_stretch: "伸縮"
material_metallic: "光沢"
material_roughness: "粗さ"
light_brightness: "明るさ"
advancedCustomize: "高度なアレンジ"
enterEditMode: "エディットモードを始める"
exitEditMode: "エディットモードを終了"
installFurniture: "家具を設置"
roomCustomize: "部屋のカスタマイズ"
changeRoomName: "ルーム名を編集"
roomInfo: "ルーム情報"
duplicate: "複製"
grab: "掴む"
furnitureCustomize: "家具のアレンジ"
uninstallFurniture: "しまう"
furnituresCount: "家具の数"
attachedFilesCount: "添付ファイル数"
_furniturePlacement:
top: "上面設置"
bottom: "下面設置"
side: "側面設置"
_furnitures:
haniwa: "はにわ"
_haniwa:
bodyMat: "本体の素材"
insideColor: "中身の色"
woodRingFloorLamp: "リングシェードフロアランプ"
_woodRingFloorLamp:
shadeMat: "シェードの素材"
bodyMat: "本体の素材"
light: "照明"
a4Case: "A4ケース"
_a4Case:
mat: "素材"
aircon: "エアコン"
allInOnePc: "一体型PC"
_allInOnePc:
bezelMat: "ベゼルの素材"
bodyMat: "本体の素材"
image: "画面の画像"
image_desktop: "デスクトップ"
screenBrightness: "画面の明るさ"
aquarium: "水槽"
aromaReedDiffuser: "アロマリードディフューザー"
_aromaReedDiffuser:
bottleMat: "ボトルの素材"
oilMat: "オイルの素材"
banknote: "紙幣"
beamLamp: "ビームランプ"
bed: "ベッド"
_bed:
frameMat: "フレームの素材"
blind: "ブラインド"
_blind:
angle: "羽根の回転角度"
blades: "羽根の枚数"
open: "開閉状態"
book: "本"
_book:
height: "高さ"
thickness: "厚み"
variation: "バリエーション"
width: "幅"
books: "本の束"
_books:
variation: "バリエーション"
boxWallShelf: "ボックス型ウォールシェルフ"
_boxWallShelf:
bodyMat: "本体の素材"
height: "高さ"
width: "幅"
withBack: "背板"
cactusS: "サボテン S"
_cactusS:
potMat: "鉢の素材"
cardboardBox: "段ボール箱"
_cardboardBox:
variation: "種類"
variation_aizon: "Aizon"
variation_default: "デフォルト"
variation_mikan: "みかん"
ceilingFanLight: "シーリングファンライト"
_ceilingFanLight:
shadeMat: "シェードの素材"
bodyMat: "本体の素材"
ceilingFan: "シーリングファン"
_ceilingFan:
shadeMat: "シェードの素材"
bodyMat: "本体の素材"
chair: "椅子"
_chair:
primaryMat: "メインの素材"
secondaryMat: "サブの素材"
frameMat: "フレームの素材"
clippedPicture: "留められた写真"
_clippedPicture:
height: "高さ"
image: "画像"
width: "幅"
coffeeCup: "コーヒーカップ"
colorBox: "カラーボックス"
_colorBox:
mat: "素材"
cuboid: "直方体"
_cuboid:
mat: "素材"
x: "X"
y: "Y"
z: "Z"
cupNoodle: "インスタントラーメン"
curtain: "カーテン"
custardPudding: "プリン"
descriptionPlate: "説明が書かれたプレート"
desk: "デスク"
_desk:
boardMat: "天板の素材"
depth: "奥行き"
frameMat: "フレームの素材"
width: "幅"
desktopPc: "デスクトップPC"
_desktopPc:
bodyMat: "本体の素材"
coverMat: "カバーの素材"
inner1Mat: "内部素材1"
inner2Mat: "内部素材2"
inner3Mat: "内部素材3"
ledColor: "LEDの色"
djMixer: "DJミキサー"
djPlayer: "DJプレーヤー"
_djPlayer:
image: "画像"
"image:waveform": "波形"
screenBrightness: "画面の明るさ"
ductRailSpotLights: "スポットライト付きダクトレール"
_ductRailSpotLights:
angleH: "水平角度"
angleV: "垂直角度"
bodyMat: "本体の素材"
light: "照明"
ductTape: "ガムテープ"
herbarium: "ハーバリウム"
electronicDisplayBoard: "電光掲示板"
_electronicDisplayBoard:
frameMat: "フレームの素材"
ledBrightness: "LEDの明るさ"
ledColor: "LEDの色"
text: "テキスト"
emptyBento: "空の弁当容器"
energyDrink: "エナジードリンク"
envelope: "封筒"
facialTissue: "ティッシュ"
glassCylinderPotPlant: "ガラスシリンダーの鉢植えと植物"
handheldGameConsole: "携帯ゲーム機"
_handheldGameConsole:
bodyMat: "本体の素材"
image: "画像"
screenBrightness: "画面の明るさ"
hangingDuctRail: "吊り下げダクトレール"
_hangingDuctRail:
bodyMat: "本体の素材"
height: "高さ"
width: "幅"
hangingTShirt: "吊り下げTシャツ"
icosahedron: "正二十面体のオブジェ"
_icosahedron:
mat: "素材"
ironFrameTable: "アイアンフレームテーブル"
_ironFrameTable:
boardMat: "天板の素材"
depth: "奥行き"
frameMat: "フレームの素材"
height: "高さ"
width: "幅"
issyoubin: "一升瓶"
_issyoubin:
variation: "種類"
keyboard: "キーボード"
_keyboard:
bodyMat: "本体の素材"
keyMat: "キーの素材"
laptopPc: "ートPC"
_laptopPc:
bezelMat: "ベゼルの素材"
bodyMat: "本体の素材"
image: "画像"
openAngle: "開き具合"
screenBrightness: "画面の明るさ"
largeMousepad: "大きいマウスパッド"
_largeMousepad:
image: "画像"
lavaLamp: "ラバランプ"
_lavaLamp:
bodyMat: "本体の素材"
glassMat: "ガラスの素材"
lavaColor: "オイルの色"
lightColor: "ランプの色"
letterCase: "レターケース"
lowPartitionBar: "低いパーティションバー"
_lowPartitionBar:
bodyMat: "本体の素材"
width: "幅"
miObjet: "Miオブジェ"
miPlate: "Miプレート"
miPlateDisplayed: "飾られたMiプレート"
milk: "牛乳"
mixer: "ミキサー"
monitor: "モニター"
_monitor:
bodyMat: "本体の素材"
image: "画像"
screenBrightness: "画面の明るさ"
monitorSpeaker: "モニタースピーカー"
_monitorSpeaker:
mat: "素材"
monstera: "モンステラ"
_monstera:
potMat: "鉢の素材"
mug: "マグカップ"
_mug:
bodyMat: "コップの素材"
liquidMat: "液体の素材"
newtonsCradle: "ニュートンクレードル"
_newtonsCradle:
frameMat: "フレームの素材"
openedCardboardBox: "開いた段ボール箱"
pachira: "パキラ"
_pachira:
potMat: "鉢の素材"
petBottle: "ペットボトル"
_petBottle:
empty: "空"
variation: "種類"
variation_greenTea: "緑茶"
variation_mineralWater: "ミネラルウォーター"
withCap: "キャップ"
withLabel: "ラベル"
piano: "ピアノ"
_piano:
bodyMat: "本体の素材"
pictureFrame: "シンプルな額縁"
_pictureFrame:
depth: "厚さ"
frameMat: "フレームの素材"
frameThickness: "フレームの幅"
height: "高さ"
image: "画像"
matHThickness: "マットの横幅"
matVThickness: "マットの縦幅"
width: "幅"
withCover: "カバーあり"
pizza: "ピザ"
plant: "植物"
plant2: "植物 2"
poster: "ポスター"
_poster:
height: "高さ"
image: "画像"
width: "幅"
powerStrip: "電源タップ"
radiometer: "ラジオメーター"
randomBooks: "雑多な本"
_randomBooks:
count: "数"
seed: "シード"
stackVertically: "平積み"
variation: "バリエーション"
variation_mix: "いろいろ"
variation_mixPlain: "いろいろ(無地)"
recordPlayer: "レコードプレーヤー"
rolledUpPoster: "丸めたポスター"
roundRug: "円形のラグ"
router: "ルーター"
siphon: "サイフォン"
snakeplant: "サンセベリア"
_snakeplant:
potMat: "鉢の素材"
sofa: "ソファ"
_sofa:
bodyMat: "本体の素材"
speaker: "スピーカー"
_speaker:
innerMat: "内側の素材"
outerMat: "外側の素材"
speakerStand: "スピーカースタンド"
_speakerStand:
bodyMat: "本体の素材"
height: "高さ"
spotLight: "スポットライト"
_spotLight:
angleH: "横方向の角度"
angleV: "縦方向の角度"
bodyMat: "本体の素材"
light: "照明"
downlight: "ダウンライト"
_downlight:
bodyMat: "本体の素材"
light: "照明"
sprayer: "霧吹き"
stanchionPole: "スタンションポール"
_stanchionPole:
bodyMat: "本体の素材"
ropeMat: "ロープの素材"
steelRack: "スチールラック"
_steelRack:
height: "高さ"
numberOfShelfs: "シェルフの数"
poleMat: "ポールの素材"
shelfPositionOf: "シェルフの位置"
shelfMat: "シェルフの素材"
widthAndDepthVariation: "W x D"
stormGlass: "ストームグラス"
tableSalt: "食卓塩"
tabletopCalendar: "卓上カレンダー"
tabletopDigitalClock: "卓上デジタル時計"
_tabletopDigitalClock:
bodyMat: "本体の素材"
lcdColor: "LCDの色"
tabletopFlag: "卓上旗"
_tabletopFlag:
image: "画像"
tabletopGlassPictureFrame: "卓上ガラス製フォトフレーム"
_tabletopGlassPictureFrame:
height: "高さ"
image: "画像"
width: "幅"
tabletopIronFrameStand: "卓上アイアンフレームスタンド"
_tabletopIronFrameStand:
boardMat: "板の素材"
depth: "奥行き"
frameMat: "フレームの素材"
height: "高さ"
width: "幅"
tabletopLcdButtonsController: "LCDボタン付き卓上コントローラー"
_tabletopLcdButtonsController:
bodyMat: "本体の素材"
image: "画像"
screenBrightness: "画面の明るさ"
tabletopPictureFrame: "卓上フォトフレーム"
_tabletopPictureFrame:
depth: "厚さ"
frameMat: "フレームの素材"
frameThickness: "フレームの幅"
height: "高さ"
image: "画像"
matHThickness: "マットの横幅"
matVThickness: "マットの縦幅"
width: "幅"
tapestry: "タペストリー"
_tapestry:
height: "高さ"
image: "画像"
width: "幅"
tetrapod: "波消ブロックの置物"
tv: "テレビ"
_tv:
bodyMat: "本体の素材"
screenBrightness: "画面の明るさ"
twistedCubeObjet: "ねじれた立方体のオブジェ"
usedTissue: "使用済みティッシュ"
wallCanvas: "壁掛けキャンバス"
_wallCanvas:
height: "高さ"
image: "画像"
width: "幅"
wallClock: "壁掛け時計"
_wallClock:
frameMat: "フレームの素材"
faceMat: "文字盤の素材"
handsMat: "針の素材"
wallGlassPictureFrame: "ガラスの壁掛けフォトフレーム"
_wallGlassPictureFrame:
height: "高さ"
image: "画像"
width: "幅"
wallMirror: "壁掛けミラー"
_wallMirror:
frameMat: "フレームの素材"
frameThickness: "フレームの厚み"
height: "高さ"
width: "幅"
wallMountSpotLight: "ウォールマウントスポットライト"
_wallMountSpotLight:
angleH: "横方向の角度"
angleV: "縦方向の角度"
bodyMat: "本体の素材"
light: "照明"
wallShelf: "ウォールシェルフ"
_wallShelf:
boardMat: "板の素材"
boardStyle: "板のスタイル"
"boardStyle:color": "単色"
"boardStyle:wood": "木目"
style: "スタイル"
wireBasket: "ワイヤーバスケット"
_wireBasket:
bodyMat: "本体の素材"
wireNet: "ワイヤーネット"
_wireNet:
bodyMat: "本体の素材"
woodRingsPendantLight: "リングペンダントライト"
_woodRingsPendantLight:
bodyMat: "本体の素材"
length: "長さ"
light: "照明"
shadeMat: "シェードの素材"
woodSoundAbsorbingPanel: "木製吸音パネル"
ironFrameShelf: "アイアンフレームシェルフ"
_ironFrameShelf:
boardMat: "板の素材"
frameMat: "フレームの素材"
height: "段数"
width: "幅"
kakejiku: "掛軸"
_kakejiku:
image: "画像"
"image:kitsuaishinsei": "喫藍心整"

View File

@@ -219,7 +219,7 @@ perDay: "每日"
stopActivityDelivery: "停止發送活動"
blockThisInstance: "封鎖此伺服器"
silenceThisInstance: "禁言此伺服器"
mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言(隱藏媒體預覽)"
mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
operations: "操作"
software: "軟體"
softwareName: "軟體名稱"

View File

@@ -1,18 +1,20 @@
{
"name": "misskey",
"version": "2026.7.0-alpha.4",
"version": "2026.6.0-beta.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@11.9.0",
"packageManager": "pnpm@11.5.2",
"workspaces": [
"packages/misskey-js",
"packages/i18n",
"packages/misskey-reversi",
"packages/misskey-bubble-game",
"packages/misskey-world",
"packages/icons-subsetter",
"packages/frontend-misskey-world-engine",
"packages/frontend-shared",
"packages/frontend-builder",
"packages/sw",
@@ -39,10 +41,11 @@
"migrateandstart": "pnpm migrate && pnpm start",
"watch": "pnpm dev",
"dev": "node scripts/dev.mjs",
"check-dts": "node scripts/check-dts.mjs",
"lint": "pnpm --no-bail -r lint && pnpm check-dts",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 \"pnpm --filter frontend test:e2e\"",
"e2e-dev-container": "ncp ./.config/playwright-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 \"pnpm --filter frontend test:e2e\"",
"lint": "pnpm --no-bail -r lint",
"cy:open": "pnpm cypress open --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"backend-unit-test": "cd packages/backend && pnpm test",
"backend-unit-test-and-coverage": "cd packages/backend && pnpm test-and-coverage",
"test": "pnpm -r test",
@@ -52,25 +55,33 @@
"cleanall": "pnpm clean-all"
},
"dependencies": {
"cssnano": "8.0.1",
"esbuild": "0.28.1",
"execa": "9.6.1",
"ignore-walk": "9.0.0",
"js-yaml": "5.2.0",
"tar": "7.5.19"
"ignore-walk": "8.0.0",
"js-yaml": "4.2.0",
"postcss": "8.5.15",
"tar": "7.5.16",
"terser": "5.48.0"
},
"devDependencies": {
"@eslint/js": "9.39.4",
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@types/js-yaml": "4.0.9",
"@types/node": "24.13.1",
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@typescript/native-preview": "7.0.0-dev.20260426.1",
"cross-env": "10.1.0",
"cypress": "15.17.0",
"eslint": "9.39.4",
"globals": "17.7.0",
"globals": "17.6.0",
"ncp": "2.0.0",
"pnpm": "11.9.0",
"start-server-and-test": "3.0.11",
"pnpm": "11.5.2",
"start-server-and-test": "3.0.9",
"typescript": "5.9.3"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
}
}

View File

@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class WorldRoom1778744540138 {
name = 'WorldRoom1778744540138'
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "world_room" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "likedCount" integer NOT NULL DEFAULT '0', "visibility" character varying(128) NOT NULL DEFAULT 'public', "def" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_40cfacaf35b0b54bb2281c89767" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_88289375952050da4a7752a366" ON "world_room" ("updatedAt") `);
await queryRunner.query(`CREATE INDEX "IDX_f803a5efb4125c5fd8a414285e" ON "world_room" ("userId") `);
await queryRunner.query(`ALTER TABLE "world_room" ADD CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "world_room" DROP CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f803a5efb4125c5fd8a414285e"`);
await queryRunner.query(`DROP INDEX "public"."IDX_88289375952050da4a7752a366"`);
await queryRunner.query(`DROP TABLE "world_room"`);
}
}

View File

@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class WorldAvatar1779921322355 {
name = 'WorldAvatar1779921322355'
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "world_avatar" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "def" jsonb NOT NULL DEFAULT '{}', "active" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_e7a27262285cc2c27114871f866" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_0f1d0bdfaca455cc2f13defabe" ON "world_avatar" ("updatedAt") `);
await queryRunner.query(`CREATE INDEX "IDX_4eba43c8e2540a92e99dd7f5a9" ON "world_avatar" ("userId") `);
await queryRunner.query(`ALTER TABLE "world_room" ADD "accessCount" integer NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "world_avatar" ADD CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "world_avatar" DROP CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a"`);
await queryRunner.query(`ALTER TABLE "world_room" DROP COLUMN "accessCount"`);
await queryRunner.query(`DROP INDEX "public"."IDX_4eba43c8e2540a92e99dd7f5a9"`);
await queryRunner.query(`DROP INDEX "public"."IDX_0f1d0bdfaca455cc2f13defabe"`);
await queryRunner.query(`DROP TABLE "world_avatar"`);
}
}

View File

@@ -10,8 +10,10 @@ export class NoteIdIndexForPinAndFavorite1780059833698 {
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
async up(queryRunner) {
await this.ensureValidIndex(queryRunner, 'IDX_0e00498f180193423c992bc437', 'note_favorite', 'noteId');
await this.ensureValidIndex(queryRunner, 'IDX_68881008f7c3588ad7ecae471c', 'user_note_pining', 'noteId');
const concurrently = isConcurrentIndexMigrationEnabled ? 'CONCURRENTLY' : '';
await queryRunner.query(`CREATE INDEX ${concurrently} IF NOT EXISTS "IDX_0e00498f180193423c992bc437" ON "note_favorite" ("noteId")`);
await queryRunner.query(`CREATE INDEX ${concurrently} IF NOT EXISTS "IDX_68881008f7c3588ad7ecae471c" ON "user_note_pining" ("noteId")`);
}
async down(queryRunner) {
@@ -20,16 +22,4 @@ export class NoteIdIndexForPinAndFavorite1780059833698 {
await queryRunner.query(`DROP INDEX ${concurrently} IF EXISTS "public"."IDX_68881008f7c3588ad7ecae471c"`);
await queryRunner.query(`DROP INDEX ${concurrently} IF EXISTS "public"."IDX_0e00498f180193423c992bc437"`);
}
async ensureValidIndex(queryRunner, indexName, tableName, columnName) {
if (isConcurrentIndexMigrationEnabled) {
const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = '${indexName}'`);
if (hasValidIndex.length === 0 || hasValidIndex[0].indisvalid !== true) {
await queryRunner.query(`DROP INDEX IF EXISTS "${indexName}"`);
await queryRunner.query(`CREATE INDEX CONCURRENTLY "${indexName}" ON "${tableName}" ("${columnName}")`);
}
} else {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${tableName}" ("${columnName}")`);
}
}
}

View File

@@ -1,22 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SensitiveMediaDetectionExternalService1780488454126 {
name = 'SensitiveMediaDetectionExternalService1780488454126'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionApiUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionApiKey" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionTimeout" integer NOT NULL DEFAULT '60000'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionMaxImagesPerRequest" integer NOT NULL DEFAULT '4'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionMaxImagesPerRequest"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionTimeout"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionApiKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionApiUrl"`);
}
}

View File

@@ -1,16 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UrlPreviewSensitiveList1782581064131 {
name = 'UrlPreviewSensitiveList1782581064131'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewSensitiveList" character varying(3072) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewSensitiveList"`);
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": "^22.22.2 || ^24.17.0 || ^26.4.0"
"node": "^22.15.0 || ^24.10.0 || ^26.0.0"
},
"scripts": {
"start": "pnpm compile-config && node ./built/entry.js",
@@ -34,6 +34,8 @@
"generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js"
},
"optionalDependencies": {
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.1.0",
"slacc-android-arm-eabi": "0.1.5",
"slacc-android-arm64": "0.1.5",
@@ -51,8 +53,8 @@
"utf-8-validate": "6.0.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1075.0",
"@aws-sdk/lib-storage": "3.1075.0",
"@aws-sdk/client-s3": "3.1065.0",
"@aws-sdk/lib-storage": "3.1065.0",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
"@fastify/http-proxy": "11.5.0",
@@ -61,31 +63,26 @@
"@kitajs/html": "4.2.13",
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/emoji-data": "17.0.3",
"@misskey-dev/sharp-read-bmp": "1.3.1",
"@misskey-dev/summaly": "5.5.1",
"@napi-rs/canvas": "1.0.1",
"@nestjs/common": "11.1.27",
"@nestjs/core": "11.1.27",
"@nestjs/testing": "11.1.27",
"@opentelemetry/api": "1.9.1",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.214.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1",
"@opentelemetry/sdk-trace-node": "2.7.1",
"@opentelemetry/semantic-conventions": "1.40.0",
"@oxc-project/runtime": "0.137.0",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.3.0",
"@napi-rs/canvas": "1.0.0",
"@nestjs/common": "11.1.26",
"@nestjs/core": "11.1.26",
"@nestjs/testing": "11.1.26",
"@oxc-project/runtime": "0.135.0",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.62.0",
"@sentry/profiling-node": "10.62.0",
"@simplewebauthn/server": "13.3.2",
"@smithy/node-http-handler": "4.8.2",
"@sentry/node": "10.57.0",
"@sentry/profiling-node": "10.57.0",
"@simplewebauthn/server": "13.3.1",
"@sinonjs/fake-timers": "15.4.0",
"@smithy/node-http-handler": "4.7.7",
"accepts": "1.3.8",
"ajv": "8.20.0",
"archiver": "8.0.0",
"async-mutex": "0.5.0",
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"bullmq": "5.79.2",
"bullmq": "5.78.0",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
@@ -94,12 +91,13 @@
"content-disposition": "2.0.1",
"date-fns": "4.4.0",
"deep-email-validator": "0.1.27",
"fastify": "5.9.0",
"fastify": "5.8.5",
"fastify-raw-body": "5.0.0",
"feed": "5.2.1",
"file-type": "22.0.1",
"fluent-ffmpeg": "2.1.3",
"got": "15.0.7",
"form-data": "4.0.6",
"got": "15.0.5",
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
@@ -109,39 +107,41 @@
"is-svg": "6.1.0",
"json5": "2.2.3",
"jsonld": "9.0.0",
"juice": "12.1.1",
"juice": "12.1.0",
"meilisearch": "0.58.0",
"mfm-js": "0.26.0",
"mime-types": "3.0.2",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.16",
"nanoid": "5.1.11",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-html-parser": "8.0.3",
"nodemailer": "9.0.1",
"node-html-parser": "7.1.0",
"nodemailer": "8.0.10",
"nsfwjs": "4.3.0",
"os-utils": "0.0.14",
"otpauth": "9.5.1",
"pg": "8.22.0",
"pg": "8.21.0",
"pkce-challenge": "6.0.0",
"probe-image-size": "7.3.0",
"promise-limit": "2.7.0",
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.25.0",
"re2": "1.24.1",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"sanitize-html": "2.17.5",
"rxjs": "7.8.2",
"sanitize-html": "2.17.4",
"secure-json-parse": "4.1.0",
"semver": "7.8.5",
"sharp": "0.35.2",
"semver": "7.8.4",
"sharp": "0.33.5",
"slacc": "0.1.5",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.31.11",
"systeminformation": "5.31.7",
"tinycolor2": "1.6.0",
"tmp": "0.2.7",
"tsc-alias": "1.8.17",
@@ -154,19 +154,19 @@
},
"devDependencies": {
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.27",
"@nestjs/platform-express": "11.1.26",
"@rollup/plugin-esm-shim": "0.1.8",
"@sentry/vue": "10.62.0",
"@sinonjs/fake-timers": "15.4.0",
"@sentry/vue": "10.57.0",
"@types/accepts": "1.3.7",
"@types/archiver": "8.0.0",
"@types/fluent-ffmpeg": "2.1.28",
"@types/http-link-header": "1.0.7",
"@types/js-yaml": "4.0.9",
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
"@types/node": "26.0.1",
"@types/nodemailer": "8.0.1",
"@types/node": "24.13.1",
"@types/nodemailer": "8.0.0",
"@types/pg": "8.20.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
@@ -176,26 +176,28 @@
"@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.8",
"@types/sinonjs__fake-timers": "15.0.1",
"@types/supertest": "7.2.0",
"@types/tinycolor2": "1.4.6",
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@vitest/coverage-v8": "4.1.9",
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/coverage-v8": "4.1.8",
"aws-sdk-client-mock": "4.1.0",
"cbor2": "2.3.0",
"cbor": "10.0.12",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"execa": "9.6.1",
"fkill": "10.0.3",
"js-yaml": "5.2.0",
"js-yaml": "4.2.0",
"pid-port": "2.1.1",
"rolldown": "1.1.3",
"rolldown": "1.1.0",
"simple-oauth2": "5.1.0",
"vite": "8.1.0",
"vitest": "4.1.9",
"supertest": "7.2.2",
"vite": "8.0.16",
"vitest": "4.1.8",
"vitest-mock-extended": "4.0.0"
}
}

View File

@@ -1,9 +1,7 @@
import { defineConfig } from 'rolldown';
import { version as summalyVersion } from '@misskey-dev/summaly';
import type { Plugin, ExternalOption } from 'rolldown';
import { execa, execaNode } from 'execa';
import type { ResultPromise } from 'execa';
import fkill from 'fkill';
import esmShim from '@rollup/plugin-esm-shim';
/**
@@ -11,7 +9,6 @@ import esmShim from '@rollup/plugin-esm-shim';
*/
function backendDevServerPlugin(): Plugin {
let backendProcess: ResultPromise | null = null;
let backendShutdownPromise: Promise<void> | null = null;
async function runBuildAssets() {
await execa('pnpm', ['run', 'build-assets'], {
@@ -22,31 +19,12 @@ function backendDevServerPlugin(): Plugin {
}
async function killBackendProcess() {
if (backendShutdownPromise) return backendShutdownPromise;
if (!backendProcess) return;
const processToKill = backendProcess;
backendProcess = null;
processToKill.catch(() => {}); // プロセスの終了によって発生する例外を無視するためにcatch()を呼び出す
backendShutdownPromise = (async () => {
if (process.platform === 'win32' && processToKill.pid != null) {
await fkill(processToKill.pid, {
force: true,
tree: true,
silent: true,
waitForExit: 5000,
});
} else {
processToKill.kill();
}
await processToKill.catch(() => {});
})().finally(() => {
backendShutdownPromise = null;
});
return backendShutdownPromise;
if (backendProcess) {
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
backendProcess.kill();
await new Promise(resolve => backendProcess!.on('exit', resolve));
backendProcess = null;
}
}
return {
@@ -70,9 +48,6 @@ function backendDevServerPlugin(): Plugin {
await runBuildAssets();
}
},
async closeWatcher() {
await killBackendProcess();
},
};
}
@@ -87,11 +62,18 @@ export default defineConfig((args) => {
'class-validator',
/^@sentry\/.*/,
/^@sentry-internal\/.*/,
/^@opentelemetry\/.*/,
'@nestjs/websockets/socket-module',
'@nestjs/microservices/microservices-module',
'@nestjs/microservices',
/^@napi-rs\/.*/,
// @tensorflow/tfjs-node はネイティブバインディングを持つため external 必須 (#17501)。
// あわせて nsfwjs と @tensorflow/* 全体を external にする。bundle 内の nsfwjs が
// 抱える @tensorflow/tfjs-core と、external な tfjs-node が使う tfjs-core が
// 別インスタンスに分裂すると、tfjs-node が登録する file:// IOHandler を nsfwjs 側が
// 共有できず、モデル読み込みが HTTP handler(node-fetch) にフォールバックして
// 「URL scheme "file" is not supported」で失敗するため。
/^@tensorflow\/.*/,
'nsfwjs',
'mock-aws-s3',
'aws-sdk',
'nock',
@@ -100,15 +82,8 @@ export default defineConfig((args) => {
're2',
'ipaddr.js',
'file-type',
// バンドルするとSentryの自動計装が正しく行われなくなるため外しておく
'pg',
];
const define: Record<string, string> = {
// Summalyのバージョンを埋め込む
'_SUMMALY_VERSION_': JSON.stringify(summalyVersion),
};
if (isE2E) {
return {
input: './test-server/entry.ts',
@@ -117,9 +92,6 @@ export default defineConfig((args) => {
plugins: [
esmShim(),
],
transform: {
define,
},
output: {
keepNames: true,
sourcemap: true,
@@ -144,9 +116,6 @@ export default defineConfig((args) => {
esmShim(),
(isWatchMode ? backendDevServerPlugin() : undefined),
],
transform: {
define,
},
output: {
keepNames: true,
minify: !isWatchMode,

View File

@@ -11,7 +11,7 @@
import fs from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { load as loadYaml } from 'js-yaml';
import yaml from 'js-yaml';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -34,7 +34,7 @@ function yamlToJson(ymlPath) {
console.log(`${ymlPath}${OUTPUT_PATH}`);
const yamlContent = fs.readFileSync(ymlPath, 'utf-8');
const jsonContent = loadYaml(yamlContent);
const jsonContent = yaml.load(yamlContent);
if (!fs.existsSync(dirname(OUTPUT_PATH))) {
fs.mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
}

View File

@@ -0,0 +1,231 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* This script starts the Misskey backend server, waits for it to be ready,
* measures memory usage, and outputs the result as JSON.
*
* Usage: node scripts/measure-memory.mjs
*/
import { fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import * as http from 'node:http';
import * as fs from 'node:fs/promises';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SAMPLE_COUNT = 3; // Number of samples to measure
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
const keys = {
VmPeak: 0,
VmSize: 0,
VmHWM: 0,
VmRSS: 0,
VmData: 0,
VmStk: 0,
VmExe: 0,
VmLib: 0,
VmPTE: 0,
VmSwap: 0,
};
async function getMemoryUsage(pid) {
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
const result = {};
for (const key of Object.keys(keys)) {
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
if (match) {
result[key] = parseInt(match[1], 10);
} else {
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
}
}
return result;
}
async function measureMemory() {
// Start the Misskey backend server using fork to enable IPC
const serverProcess = fork(join(__dirname, '../built/entry.js'), ['expose-gc'], {
cwd: join(__dirname, '..'),
env: {
...process.env,
NODE_ENV: 'production',
MK_DISABLE_CLUSTERING: '1',
},
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
execArgv: [...process.execArgv, '--expose-gc'],
});
let serverReady = false;
// Listen for the 'ok' message from the server indicating it's ready
serverProcess.on('message', (message) => {
if (message === 'ok') {
serverReady = true;
}
});
// Handle server output
serverProcess.stdout?.on('data', (data) => {
process.stderr.write(`[server stdout] ${data}`);
});
serverProcess.stderr?.on('data', (data) => {
process.stderr.write(`[server stderr] ${data}`);
});
// Handle server error
serverProcess.on('error', (err) => {
process.stderr.write(`[server error] ${err}\n`);
});
async function triggerGc() {
const ok = new Promise((resolve) => {
serverProcess.once('message', (message) => {
if (message === 'gc ok') resolve();
});
});
serverProcess.send('gc');
await ok;
await setTimeout(1000);
}
function createRequest() {
return new Promise((resolve, reject) => {
const req = http.request({
host: 'localhost',
port: 61812,
path: '/api/meta',
method: 'POST',
}, (res) => {
res.on('data', () => { });
res.on('end', () => {
resolve();
});
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
// Wait for server to be ready or timeout
const startupStartTime = Date.now();
while (!serverReady) {
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
serverProcess.kill('SIGTERM');
throw new Error('Server startup timeout');
}
await setTimeout(100);
}
const startupTime = Date.now() - startupStartTime;
process.stderr.write(`Server started in ${startupTime}ms\n`);
// Wait for memory to settle
await setTimeout(MEMORY_SETTLE_TIME);
const pid = serverProcess.pid;
const beforeGc = await getMemoryUsage(pid);
await triggerGc();
const afterGc = await getMemoryUsage(pid);
// create some http requests to simulate load
const REQUEST_COUNT = 10;
await Promise.all(
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
);
await triggerGc();
const afterRequest = await getMemoryUsage(pid);
// Stop the server
serverProcess.kill('SIGTERM');
// Wait for process to exit
let exited = false;
await new Promise((resolve) => {
serverProcess.on('exit', () => {
exited = true;
resolve(undefined);
});
// Force kill after 10 seconds if not exited
setTimeout(10000).then(() => {
if (!exited) {
serverProcess.kill('SIGKILL');
}
resolve(undefined);
});
});
const result = {
timestamp: new Date().toISOString(),
beforeGc,
afterGc,
afterRequest,
};
return result;
}
async function main() {
// 直列の方が時間的に分散されて正確そうだから直列でやる
const results = [];
for (let i = 0; i < SAMPLE_COUNT; i++) {
const res = await measureMemory();
results.push(res);
}
// Calculate averages
const beforeGc = structuredClone(keys);
const afterGc = structuredClone(keys);
const afterRequest = structuredClone(keys);
for (const res of results) {
for (const key of Object.keys(keys)) {
beforeGc[key] += res.beforeGc[key];
afterGc[key] += res.afterGc[key];
afterRequest[key] += res.afterRequest[key];
}
}
for (const key of Object.keys(keys)) {
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
}
const result = {
timestamp: new Date().toISOString(),
beforeGc,
afterGc,
afterRequest,
};
// Output as JSON to stdout
console.log(JSON.stringify(result, null, 2));
}
main().catch((err) => {
console.error(JSON.stringify({
error: err.message,
timestamp: new Date().toISOString(),
}));
process.exit(1);
});

View File

@@ -1,631 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ChildProcess, fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
//import * as http from 'node:http';
import * as fs from 'node:fs/promises';
import { heapSnapshotCategory, type HeapSnapshotData } from '../../../.github/scripts/heap-snapshot-util.mts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function readIntegerEnv(name, defaultValue, min) {
const rawValue = process.env[name];
if (rawValue == null || rawValue === '') return defaultValue;
if (!/^\d+$/.test(rawValue)) throw new Error(`${name} must be an integer`);
const value = Number(rawValue);
if (!Number.isSafeInteger(value) || value < min) throw new Error(`${name} must be >= ${min}`);
return value;
}
function readBooleanEnv(name, defaultValue) {
const rawValue = process.env[name];
if (rawValue == null || rawValue === '') return defaultValue;
if (rawValue === '1' || rawValue === 'true') return true;
if (rawValue === '0' || rawValue === 'false') return false;
throw new Error(`${name} must be one of: 1, 0, true, false`);
}
const SAMPLE_COUNT = readIntegerEnv('MK_MEMORY_SAMPLE_COUNT', 3, 1); // Number of samples to measure
const STARTUP_TIMEOUT = readIntegerEnv('MK_MEMORY_STARTUP_TIMEOUT_MS', 120000, 1); // Timeout for server startup
const MEMORY_SETTLE_TIME = readIntegerEnv('MK_MEMORY_SETTLE_TIME_MS', 10000, 0); // Wait after startup for memory to settle
const IPC_TIMEOUT = readIntegerEnv('MK_MEMORY_IPC_TIMEOUT_MS', 30000, 1); // Timeout for IPC responses
const REQUEST_COUNT = readIntegerEnv('MK_MEMORY_REQUEST_COUNT', 10, 0);
const HEAP_SNAPSHOT = readBooleanEnv('MK_MEMORY_HEAP_SNAPSHOT', false);
const HEAP_SNAPSHOT_TIMEOUT = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_TIMEOUT_MS', 120000, 1);
const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', 6, 1);
const HEAP_SNAPSHOT_SAVE_PATH = process.env.MK_MEMORY_HEAP_SNAPSHOT_SAVE_PATH;
const procStatusKeys = ['VmPeak', 'VmSize', 'VmHWM', 'VmRSS', 'VmData', 'VmStk', 'VmExe', 'VmLib', 'VmPTE', 'VmSwap'] as const;
const smapsRollupKeys = ['Pss', 'Shared_Clean', 'Shared_Dirty', 'Private_Clean', 'Private_Dirty', 'Swap', 'SwapPss'] as const;
type GcMessage = 'gc ok' | 'gc unavailable';
type RuntimeMemoryUsageMessage = {
type: 'memory usage';
value: NodeJS.MemoryUsage;
};
type HeapSnapshotMessage = {
type: 'heap snapshot';
path?: string;
};
type HeapSnapshotErrorMessage = {
type: 'heap snapshot error';
message: string;
};
type HeapSnapshotResponseMessage = HeapSnapshotMessage | HeapSnapshotErrorMessage;
function parseMemoryFile<KS extends readonly string[]>(content: string, keys: KS, path: string, required: boolean): Record<KS[number], number> {
const result = {} as Record<KS[number], number>;
for (const _key of keys) {
const key = _key as KS[number];
const match = content.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
if (match) {
result[key] = parseInt(match[1], 10);
} else if (required) {
throw new Error(`Failed to parse ${key} from ${path}`);
}
}
return result;
}
function bytesToKiB(value: number) {
return Math.round(value / 1024);
}
function sanitizeHeapSnapshotBreakdownLabel(value, fallback = 'unknown') {
const label = String(value ?? '').replace(/\s+/g, ' ').trim();
if (label === '') return fallback;
if (label.length <= 80) return label;
return `${label.slice(0, 77)}...`;
}
function classifyHeapSnapshotBreakdown(category: keyof typeof heapSnapshotCategory, type, name) {
if (category === 'strings') return type;
if (category === 'jsArrays') {
if (type === 'array elements') return 'Array elements';
if (type === 'object' && name === 'Array') return 'Array objects';
return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`);
}
if (category === 'typedArrays') {
if (name === 'system / JSArrayBufferData') return 'ArrayBuffer data';
return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`);
}
if (category === 'systemObjects') {
if (name.startsWith('system /')) return sanitizeHeapSnapshotBreakdownLabel(name);
if (name.startsWith('(system ')) return sanitizeHeapSnapshotBreakdownLabel(name);
return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`, type);
}
if (category === 'otherJsObjects') {
if (type === 'object') return sanitizeHeapSnapshotBreakdownLabel(`object: ${name}`, 'object: unknown');
return type;
}
if (category === 'otherNonJsObjects') {
if (type === 'extra native bytes') return 'Extra native bytes';
if (type === 'native') return sanitizeHeapSnapshotBreakdownLabel(`native: ${name}`, 'native: unknown');
return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`, type);
}
if (category === 'code') {
const lowerName = name.toLowerCase();
if (lowerName.includes('bytecode')) return 'bytecode';
if (lowerName.includes('builtin')) return 'builtins';
if (lowerName.includes('regexp')) return 'regexp code';
if (lowerName.includes('stub')) return 'stubs';
return sanitizeHeapSnapshotBreakdownLabel(`code: ${name}`, 'code: unknown');
}
return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`, type);
}
function collapseHeapSnapshotBreakdown(breakdowns: Record<string, Record<string, number>>) {
const collapsed = {} as Record<string, Record<string, number>>;
for (const [category, children] of Object.entries(breakdowns)) {
const entries = Object.entries(children)
.filter(([, value]) => value > 0)
.toSorted((a, b) => b[1] - a[1]);
const topEntries = entries.slice(0, HEAP_SNAPSHOT_BREAKDOWN_TOP_N);
const otherValue = entries
.slice(HEAP_SNAPSHOT_BREAKDOWN_TOP_N)
.reduce((sum, [, value]) => sum + value, 0);
const categoryBreakdown = Object.fromEntries(topEntries);
if (otherValue > 0) categoryBreakdown.Other = otherValue;
if (Object.keys(categoryBreakdown).length > 0) collapsed[category] = categoryBreakdown;
}
return collapsed;
}
// Keep these buckets aligned with Chrome DevTools' heap snapshot Statistics view.
function analyzeHeapSnapshot(snapshot) {
const meta = snapshot?.snapshot?.meta;
const nodes = snapshot?.nodes;
const edges = snapshot?.edges;
const strings = snapshot?.strings;
if (meta == null || !Array.isArray(nodes) || !Array.isArray(edges) || !Array.isArray(strings)) {
throw new Error('Invalid heap snapshot format');
}
const nodeFields = meta.node_fields;
if (!Array.isArray(nodeFields)) throw new Error('Invalid heap snapshot node fields');
const edgeFields = meta.edge_fields;
if (!Array.isArray(edgeFields)) throw new Error('Invalid heap snapshot edge fields');
const typeOffset = nodeFields.indexOf('type');
const nameOffset = nodeFields.indexOf('name');
const selfSizeOffset = nodeFields.indexOf('self_size');
const edgeCountOffset = nodeFields.indexOf('edge_count');
if (typeOffset < 0 || nameOffset < 0 || selfSizeOffset < 0 || edgeCountOffset < 0) {
throw new Error('Heap snapshot is missing required node fields');
}
const edgeTypeOffset = edgeFields.indexOf('type');
const edgeNameOffset = edgeFields.indexOf('name_or_index');
const edgeToNodeOffset = edgeFields.indexOf('to_node');
if (edgeTypeOffset < 0 || edgeNameOffset < 0 || edgeToNodeOffset < 0) {
throw new Error('Heap snapshot is missing required edge fields');
}
const nodeTypeNames = meta.node_types?.[typeOffset];
if (!Array.isArray(nodeTypeNames)) throw new Error('Invalid heap snapshot node types');
const edgeTypeNames = meta.edge_types?.[edgeTypeOffset];
if (!Array.isArray(edgeTypeNames)) throw new Error('Invalid heap snapshot edge types');
function createEmptyHeapSnapshotCategoryMap() {
return Object.fromEntries(Object.keys(heapSnapshotCategory).map(category => [category, 0])) as Record<keyof typeof heapSnapshotCategory, number>;
}
const nodeFieldCount = nodeFields.length;
const edgeFieldCount = edgeFields.length;
const nativeType = nodeTypeNames.indexOf('native');
const codeType = nodeTypeNames.indexOf('code');
const hiddenType = nodeTypeNames.indexOf('hidden');
const stringTypes = new Set([
nodeTypeNames.indexOf('string'),
nodeTypeNames.indexOf('concatenated string'),
nodeTypeNames.indexOf('sliced string'),
]);
const internalEdgeType = edgeTypeNames.indexOf('internal');
const extraNativeBytes = Number.isFinite(snapshot.snapshot.extra_native_bytes) ? snapshot.snapshot.extra_native_bytes : 0;
const categories = createEmptyHeapSnapshotCategoryMap();
const nodeCounts = createEmptyHeapSnapshotCategoryMap();
const breakdowns = Object.fromEntries(
(Object.keys(heapSnapshotCategory) as (keyof typeof heapSnapshotCategory)[])
.filter(category => category !== 'total')
.map(category => [category, {}]),
);
function addValue(map: Record<string, number>, key: string, value: number) {
map[key] = (map[key] ?? 0) + value;
}
const edgeStartIndexes = new Map<number, number>();
const retainerCounts = new Map<number, number>();
let edgeIndex = 0;
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) {
edgeStartIndexes.set(nodeIndex, edgeIndex);
const edgeCount = nodes[nodeIndex + edgeCountOffset] ?? 0;
for (let i = 0; i < edgeCount; i++, edgeIndex += edgeFieldCount) {
const toNodeIndex = edges[edgeIndex + edgeToNodeOffset];
retainerCounts.set(toNodeIndex, (retainerCounts.get(toNodeIndex) ?? 0) + 1);
}
}
const jsArrayElementNodeIndexes = new Set<number>();
function addCategoryValue(category: keyof typeof heapSnapshotCategory, value: number, type: string, name: string, nodeIndex: number | null = null) {
if (value <= 0) return;
categories[category] += value;
addValue(breakdowns[category], classifyHeapSnapshotBreakdown(category, type, name), value);
if (nodeIndex != null) nodeCounts[category]++;
}
function addJsArrayElementSize(nodeIndex: number) {
const beginEdgeIndex = edgeStartIndexes.get(nodeIndex) ?? 0;
const edgeCount = nodes[nodeIndex + edgeCountOffset] ?? 0;
for (let i = 0, currentEdgeIndex = beginEdgeIndex; i < edgeCount; i++, currentEdgeIndex += edgeFieldCount) {
const edgeType = edges[currentEdgeIndex + edgeTypeOffset];
if (edgeType !== internalEdgeType) continue;
const edgeName = strings[edges[currentEdgeIndex + edgeNameOffset]];
if (edgeName !== 'elements') continue;
const elementsNodeIndex = edges[currentEdgeIndex + edgeToNodeOffset];
if ((retainerCounts.get(elementsNodeIndex) ?? 0) === 1) {
const elementsSize = nodes[elementsNodeIndex + selfSizeOffset] ?? 0;
addCategoryValue('jsArrays', elementsSize, 'array elements', 'Array elements', elementsNodeIndex);
jsArrayElementNodeIndexes.add(elementsNodeIndex);
}
break;
}
}
if (extraNativeBytes > 0) {
addCategoryValue('otherNonJsObjects', extraNativeBytes, 'extra native bytes', 'extra native bytes');
}
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) {
const typeId = nodes[nodeIndex + typeOffset];
const type = nodeTypeNames[typeId] ?? 'unknown';
const name = strings[nodes[nodeIndex + nameOffset]] ?? '';
const selfSize = nodes[nodeIndex + selfSizeOffset] ?? 0;
categories.total += selfSize;
nodeCounts.total++;
if (typeId === hiddenType) {
addCategoryValue('systemObjects', selfSize, type, name, nodeIndex);
continue;
}
if (typeId === nativeType) {
if (name === 'system / JSArrayBufferData') {
addCategoryValue('typedArrays', selfSize, type, name, nodeIndex);
} else {
addCategoryValue('otherNonJsObjects', selfSize, type, name, nodeIndex);
}
continue;
}
if (typeId === codeType) {
addCategoryValue('code', selfSize, type, name, nodeIndex);
continue;
}
if (stringTypes.has(typeId)) {
addCategoryValue('strings', selfSize, type, name, nodeIndex);
continue;
}
if (name === 'Array') {
addCategoryValue('jsArrays', selfSize, type, name, nodeIndex);
addJsArrayElementSize(nodeIndex);
continue;
}
}
categories.total += extraNativeBytes;
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) {
if (jsArrayElementNodeIndexes.has(nodeIndex)) continue;
const typeId = nodes[nodeIndex + typeOffset];
if (typeId === hiddenType || typeId === nativeType || typeId === codeType || stringTypes.has(typeId)) continue;
const name = strings[nodes[nodeIndex + nameOffset]] ?? '';
if (name === 'Array') continue;
const type = nodeTypeNames[typeId] ?? 'unknown';
const selfSize = nodes[nodeIndex + selfSizeOffset] ?? 0;
addCategoryValue('otherJsObjects', selfSize, type, name, nodeIndex);
}
return {
categories,
nodeCounts,
breakdowns: collapseHeapSnapshotBreakdown(breakdowns),
};
}
async function getMemoryUsage(pid: number) {
const path = `/proc/${pid}/status`;
const status = await fs.readFile(path, 'utf-8');
return parseMemoryFile(status, procStatusKeys, path, true);
}
async function getSmapsRollupMemoryUsage(pid: number) {
const path = `/proc/${pid}/smaps_rollup`;
const smapsRollup = await fs.readFile(path, 'utf-8');
return parseMemoryFile(smapsRollup, smapsRollupKeys, path, false);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === 'object';
}
function isGcMessage(message: unknown): message is GcMessage {
return message === 'gc ok' || message === 'gc unavailable';
}
function isRuntimeMemoryUsageMessage(message: unknown): message is RuntimeMemoryUsageMessage {
return isRecord(message) && message.type === 'memory usage' && isRecord(message.value);
}
function isHeapSnapshotResponseMessage(message: unknown): message is HeapSnapshotResponseMessage {
if (!isRecord(message)) return false;
if (message.type === 'heap snapshot') return true;
return message.type === 'heap snapshot error' && typeof message.message === 'string';
}
function waitForMessage<T>(serverProcess: ChildProcess, predicate: (message: unknown) => message is T, description: string, timeout = IPC_TIMEOUT) {
return new Promise<T>((resolve, reject) => {
const timer = globalThis.setTimeout(() => {
serverProcess.off('message', onMessage);
reject(new Error(`Timed out waiting for ${description}`));
}, timeout);
const onMessage = (message: unknown) => {
if (!predicate(message)) return;
globalThis.clearTimeout(timer);
serverProcess.off('message', onMessage);
resolve(message);
};
serverProcess.on('message', onMessage);
});
}
async function getRuntimeMemoryUsage(serverProcess: ChildProcess) {
const response = waitForMessage(
serverProcess,
isRuntimeMemoryUsageMessage,
'memory usage',
);
serverProcess.send('memory usage');
const message = await response;
const memoryUsage = message.value;
return {
HeapTotal: bytesToKiB(memoryUsage.heapTotal),
HeapUsed: bytesToKiB(memoryUsage.heapUsed),
External: bytesToKiB(memoryUsage.external),
ArrayBuffers: bytesToKiB(memoryUsage.arrayBuffers),
};
}
async function getHeapSnapshotStatistics(serverProcess: ChildProcess): Promise<HeapSnapshotData | null> {
if (!HEAP_SNAPSHOT) return null;
const snapshotPath = join(tmpdir(), `misskey-backend-heap-${process.pid}-${serverProcess.pid}-${Date.now()}.heapsnapshot`);
const response = waitForMessage(
serverProcess,
isHeapSnapshotResponseMessage,
'heap snapshot',
HEAP_SNAPSHOT_TIMEOUT,
);
serverProcess.send({
type: 'heap snapshot',
path: snapshotPath,
});
const message = await response;
if (message.type === 'heap snapshot error') {
throw new Error(`Failed to write heap snapshot: ${message.message}`);
}
const writtenPath = typeof message.path === 'string' ? message.path : snapshotPath;
try {
if (HEAP_SNAPSHOT_SAVE_PATH != null && HEAP_SNAPSHOT_SAVE_PATH !== '') {
await fs.mkdir(dirname(HEAP_SNAPSHOT_SAVE_PATH), { recursive: true });
await fs.copyFile(writtenPath, HEAP_SNAPSHOT_SAVE_PATH);
}
const snapshot = JSON.parse(await fs.readFile(writtenPath, 'utf-8'));
return analyzeHeapSnapshot(snapshot);
} finally {
await fs.unlink(writtenPath).catch(err => {
process.stderr.write(`Failed to delete heap snapshot ${writtenPath}: ${err.message}\n`);
});
}
}
async function getAllMemoryUsage(serverProcess: ChildProcess) {
const pid = serverProcess.pid!;
return {
...await getMemoryUsage(pid),
...await getSmapsRollupMemoryUsage(pid),
...await getRuntimeMemoryUsage(serverProcess),
};
}
async function measureMemory() {
// Start the Misskey backend server using fork to enable IPC
const serverProcess = fork(join(__dirname, '../built/entry.js'), [], {
cwd: join(__dirname, '..'),
env: {
...process.env,
NODE_ENV: 'production',
MK_DISABLE_CLUSTERING: '1',
MK_ONLY_SERVER: '1',
MK_NO_DAEMONS: '1',
},
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
execArgv: [...process.execArgv, '--expose-gc'],
});
let serverReady = false;
// Listen for the 'ok' message from the server indicating it's ready
serverProcess.on('message', (message) => {
if (message === 'ok') {
serverReady = true;
}
});
// Handle server output
serverProcess.stdout?.on('data', (data) => {
process.stderr.write(`[server stdout] ${data}`);
});
serverProcess.stderr?.on('data', (data) => {
process.stderr.write(`[server stderr] ${data}`);
});
// Handle server error
serverProcess.on('error', (err) => {
process.stderr.write(`[server error] ${err}\n`);
});
async function triggerGc() {
const ok = waitForMessage(
serverProcess,
isGcMessage,
'GC completion',
);
serverProcess.send('gc');
const message = await ok;
if (message === 'gc unavailable') {
throw new Error('GC is unavailable. Start the process with --expose-gc to enable this feature.');
}
await setTimeout(1000);
}
//function createRequest() {
// return new Promise((resolve, reject) => {
// const req = http.request({
// host: 'localhost',
// port: 61812,
// path: '/api/meta',
// method: 'POST',
// }, (res) => {
// res.on('data', () => { });
// res.on('end', () => {
// resolve();
// });
// });
// req.on('error', (err) => {
// reject(err);
// });
// req.end();
// });
//}
// Wait for server to be ready or timeout
const startupStartTime = Date.now();
while (!serverReady) {
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
serverProcess.kill('SIGTERM');
throw new Error('Server startup timeout');
}
await setTimeout(100);
}
const startupTime = Date.now() - startupStartTime;
process.stderr.write(`Server started in ${startupTime}ms\n`);
// Wait for memory to settle
await setTimeout(MEMORY_SETTLE_TIME);
//const beforeGc = await getAllMemoryUsage(serverProcess);
await triggerGc();
const memoryUsageAfterGC = await getAllMemoryUsage(serverProcess);
//// create some http requests to simulate load
//await Promise.all(
// Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
//);
//await triggerGc();
//const afterRequest = await getAllMemoryUsage(serverProcess);
const heapSnapshotAfterGc = await getHeapSnapshotStatistics(serverProcess);
// Stop the server
serverProcess.kill('SIGTERM');
// Wait for process to exit
let exited = false;
await new Promise((resolve) => {
serverProcess.on('exit', () => {
exited = true;
resolve(undefined);
});
// Force kill after 10 seconds if not exited
setTimeout(10000).then(() => {
if (!exited) {
serverProcess.kill('SIGKILL');
}
resolve(undefined);
});
});
const result = {
timestamp: new Date().toISOString(),
phases: {
//beforeGc,
afterGc: {
memoryUsage: memoryUsageAfterGC,
heapSnapshot: heapSnapshotAfterGc,
},
//afterRequest,
},
};
return result;
}
export type MemoryReportRaw = {
timestamp: string;
sampleCount: number;
measurement: {
startupTimeoutMs: number;
memorySettleTimeMs: number;
ipcTimeoutMs: number;
requestCount: number;
heapSnapshot: {
enabled: boolean;
timeoutMs: number;
breakdownTopN: number;
};
};
samples: Awaited<ReturnType<typeof measureMemory>>[];
};
async function main() {
const results = [];
for (let i = 0; i < SAMPLE_COUNT; i++) {
process.stderr.write(`Starting sample ${i + 1}/${SAMPLE_COUNT}\n`);
const res = await measureMemory();
results.push(res);
}
const result: MemoryReportRaw = {
timestamp: new Date().toISOString(),
sampleCount: SAMPLE_COUNT,
measurement: {
startupTimeoutMs: STARTUP_TIMEOUT,
memorySettleTimeMs: MEMORY_SETTLE_TIME,
ipcTimeoutMs: IPC_TIMEOUT,
requestCount: REQUEST_COUNT,
heapSnapshot: {
enabled: HEAP_SNAPSHOT,
timeoutMs: HEAP_SNAPSHOT_TIMEOUT,
breakdownTopN: HEAP_SNAPSHOT_BREAKDOWN_TOP_N,
},
},
samples: results,
};
// Output as JSON to stdout
console.log(JSON.stringify(result, null, 2));
}
main().catch((err) => {
console.error(JSON.stringify({
error: err.message,
timestamp: new Date().toISOString(),
}));
process.exit(1);
});

View File

@@ -1,6 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare const _SUMMALY_VERSION_: string;

View File

@@ -26,6 +26,7 @@ declare module 'probe-image-size' {
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
// eslint-disable-next-line import/no-default-export
export { probeImageSize as default };
namespace probeImageSize {} // Hack
export = probeImageSize;
}

View File

@@ -6,7 +6,6 @@
import { NestFactory } from '@nestjs/core';
import { init } from 'slacc';
import { NestLogger } from '@/NestLogger.js';
import { envOption } from '@/env.js';
import type { Config } from '@/config.js';
let slaccInitialized = false;
@@ -32,7 +31,7 @@ export async function server() {
const serverService = app.get(ServerService);
await serverService.launch();
if (process.env.NODE_ENV !== 'test' && !envOption.noDaemons) {
if (process.env.NODE_ENV !== 'test') {
const { ChartManagementService } = await import('../core/chart/ChartManagementService.js');
const { QueueStatsService } = await import('../daemons/QueueStatsService.js');
const { ServerStatsService } = await import('../daemons/ServerStatsService.js');
@@ -55,9 +54,7 @@ export async function jobQueue() {
});
jobQueue.get(QueueProcessorService).start();
if (!envOption.noDaemons) {
jobQueue.get(ChartManagementService).start();
}
jobQueue.get(ChartManagementService).start();
return jobQueue;
}

View File

@@ -9,11 +9,9 @@
import cluster from 'node:cluster';
import { EventEmitter } from 'node:events';
import { writeHeapSnapshot } from 'node:v8';
import chalk from 'chalk';
import Xev from 'xev';
import Logger from '@/logger.js';
import { isTelemetryShutdownInProgress } from '@/core/telemetry/telemetry-shutdown.js';
import { envOption } from '../env.js';
import { readyRef } from './ready.js';
@@ -42,11 +40,6 @@ cluster.on('online', worker => {
// Listen for dying workers
cluster.on('exit', worker => {
if (isTelemetryShutdownInProgress()) {
clusterLogger.info(`Process exited during shutdown: [${worker.id}]`);
return;
}
// Replace the dead worker,
// we're not sentimental
clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
@@ -98,35 +91,10 @@ process.on('message', msg => {
if (msg === 'gc') {
if (global.gc != null) {
logger.info('Manual GC triggered');
for (let i = 0; i < 3; i++) {
global.gc();
}
global.gc();
if (process.send != null) process.send('gc ok');
} else {
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
if (process.send != null) process.send('gc unavailable');
}
} else if (msg === 'memory usage') {
if (process.send != null) {
process.send({
type: 'memory usage',
value: process.memoryUsage(),
});
}
} else if (msg != null && typeof msg === 'object' && 'type' in msg && msg.type === 'heap snapshot' && 'path' in msg && typeof msg.path === 'string') {
if (process.send != null) {
try {
const path = writeHeapSnapshot(msg.path);
process.send({
type: 'heap snapshot',
path,
});
} catch (err) {
process.send({
type: 'heap snapshot error',
message: err instanceof Error ? err.message : String(err),
});
}
}
}
});

View File

@@ -13,8 +13,6 @@ import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
import { showMachineInfo } from '@/misc/show-machine-info.js';
import { envOption } from '@/env.js';
import { initTelemetry } from '@/core/telemetry/telemetry-registry.js';
import { installTelemetrySignalHandlers } from '@/core/telemetry/telemetry-shutdown.js';
import { initExtraThreadPool, jobQueue, server } from './common.js';
const logger = new Logger('core', 'cyan');
@@ -68,13 +66,26 @@ export async function masterMain() {
initExtraThreadPool(config);
try {
await initTelemetry(config);
} catch (e) {
bootLogger.error(e instanceof Error ? e : new Error(String(e)), null, true);
process.exit(1);
if (config.sentryForBackend) {
const Sentry = await import('@sentry/node');
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
Sentry.init({
integrations: [
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,
maxBreadcrumbs: 0,
...config.sentryForBackend.options,
});
}
installTelemetrySignalHandlers();
bootLogger.info(
`mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`,

View File

@@ -4,16 +4,10 @@
*/
import cluster from 'node:cluster';
import Logger from '@/logger.js';
import { envOption } from '@/env.js';
import { loadConfig } from '@/config.js';
import { initTelemetry } from '@/core/telemetry/telemetry-registry.js';
import { installTelemetrySignalHandlers } from '@/core/telemetry/telemetry-shutdown.js';
import { initExtraThreadPool, jobQueue, server } from './common.js';
const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta');
/**
* Init worker process
*/
@@ -22,13 +16,26 @@ export async function workerMain() {
initExtraThreadPool(config);
try {
await initTelemetry(config);
} catch (e) {
bootLogger.error(e instanceof Error ? e : new Error(String(e)), null, true);
process.exit(1);
if (config.sentryForBackend) {
const Sentry = await import('@sentry/node');
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
Sentry.init({
integrations: [
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,
maxBreadcrumbs: 0,
...config.sentryForBackend.options,
});
}
installTelemetrySignalHandlers();
if (envOption.onlyServer) {
await server();

View File

@@ -20,21 +20,6 @@ type RedisOptionsSource = Partial<RedisOptions> & {
prefix?: string;
};
type SentryBackendConfig = {
options: Partial<Sentry.NodeOptions>;
enableNodeProfiling: boolean;
disabledIntegrations?: string[];
};
type OtelBackendConfig = {
endpoint?: string;
headers?: Record<string, string>;
sampleRate?: number;
resourceAttributes?: Record<string, string>;
propagateTraceToRemote?: boolean;
jobTraceContextMode?: 'link' | 'parent';
};
/**
* 設定ファイルの型
*/
@@ -79,8 +64,7 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
sentryForBackend?: SentryBackendConfig;
otelForBackend?: OtelBackendConfig;
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
sentryForFrontend?: {
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
vueIntegration?: SentryVue.VueIntegrationOptions | null;
@@ -217,8 +201,7 @@ export type Config = {
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
redisForReactions: RedisOptions & RedisOptionsSource;
sentryForBackend: SentryBackendConfig | undefined;
otelForBackend: OtelBackendConfig | undefined;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: {
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
vueIntegration?: SentryVue.VueIntegrationOptions | null;
@@ -324,7 +307,6 @@ export function loadConfig(): Config {
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
sentryForBackend: config.sentryForBackend,
otelForBackend: config.otelForBackend,
sentryForFrontend: config.sentryForFrontend,
id: config.id,
proxy: config.proxy,

View File

@@ -3,186 +3,92 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';
import { Injectable, Inject } from '@nestjs/common';
import { Mutex } from 'async-mutex';
import fetch from 'node-fetch';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { MiMeta } from '@/models/_.js';
import type Logger from '@/logger.js';
import type { Config } from '@/config.js';
import type { NSFWJS, PredictionType } from 'nsfwjs/core';
/**
* 正規化済み画像に対する nsfwjs 互換の予測値。
* 推論自体は外部サービス (sensitive-detector) が行い、本体はその生の値を受け取って判定する。
*/
export type Prediction = {
className: string;
probability: number;
};
type BatchItemResult =
| { success: true; predictions: Prediction[] }
| { success: false; error: { code: string; message: string } };
type DetectImagesResponse =
| { success: true; result: { results: BatchItemResult[] } }
| { success: false; error: { code: string; message: string } };
// #region type guards
function isPrediction(v: unknown): v is Prediction {
if (typeof v !== 'object' || v === null) return false;
const obj = v as Record<string, unknown>;
return typeof obj['className'] === 'string' && typeof obj['probability'] === 'number';
}
function isBatchItemResult(v: unknown): v is BatchItemResult {
if (typeof v !== 'object' || v === null) return false;
const obj = v as Record<string, unknown>;
if (obj['success'] === true) {
return Array.isArray(obj['predictions']) && (obj['predictions'] as unknown[]).every(isPrediction);
}
if (obj['success'] === false) {
const error = obj['error'];
return typeof error === 'object' && error !== null && typeof (error as Record<string, unknown>)['code'] === 'string';
}
return false;
}
function isDetectImagesResponse(v: unknown): v is DetectImagesResponse {
if (typeof v !== 'object' || v === null) return false;
const obj = v as Record<string, unknown>;
if (obj['success'] === true) {
const result = obj['result'];
if (typeof result !== 'object' || result === null) return false;
const results = (result as Record<string, unknown>)['results'];
return Array.isArray(results) && (results as unknown[]).every(isBatchItemResult);
}
if (obj['success'] === false) {
const error = obj['error'];
return typeof error === 'object' && error !== null && typeof (error as Record<string, unknown>)['code'] === 'string';
}
return false;
}
// #endregion
// サイドカーの判定エンドポイント。baseUrl にパスプレフィックスがあっても連結できるよう先頭スラッシュは付けない。
const DETECT_IMAGES_PATH = 'v1/detect-images';
const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma'];
let isSupportedCpu: undefined | boolean = undefined;
@Injectable()
export class AiService {
private logger: Logger;
private readonly modelDir: string;
private model: NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor(
@Inject(DI.meta)
private meta: MiMeta,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
@Inject(DI.config)
private config: Config,
) {
this.logger = this.loggerService.getLogger('ai');
const md = resolve(this.config.rootDir, 'packages/backend/nsfw-model');
this.modelDir = md.endsWith('/') ? md : md + '/';
}
/**
* 正規化済み画像 1 枚を外部サービスに送り、生の予測値を得る。
* 接続先未設定・通信失敗・タイムアウト時は null= 非センシティブ扱い)を返す。
*/
@bindThis
public async detectSensitive(source: Buffer): Promise<Prediction[] | null> {
return (await this.detectSensitiveMany([source]))[0] ?? null;
}
/**
* 複数の正規化済み画像をまとめて外部サービスに送る。
* maxImagesPerRequest 枚ごとにチャンク分割して順次送信し、送信順を保った結果配列を返す。
* 接続先未設定・通信失敗・タイムアウト時は該当分を null= 非センシティブ扱い)にしてフォールバックする
* API 呼び出し失敗時はセンシティブではない判定とする方針: misskey-dev/misskey#16804
*/
@bindThis
public async detectSensitiveMany(sources: Buffer[]): Promise<(Prediction[] | null)[]> {
if (sources.length === 0) return [];
const baseUrl = this.meta.sensitiveMediaDetectionApiUrl;
if (baseUrl == null || baseUrl.trim() === '') {
// 接続先が未設定なら検出不能。全件 null非センシティブ扱いを返す。
return sources.map(() => null);
}
const apiKey = this.meta.sensitiveMediaDetectionApiKey;
const timeout = this.meta.sensitiveMediaDetectionTimeout;
const chunkSize = Math.max(1, this.meta.sensitiveMediaDetectionMaxImagesPerRequest);
const base = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
let url: string;
public async detectSensitive(source: string | Buffer): Promise<PredictionType[] | null> {
try {
url = new URL(DETECT_IMAGES_PATH, base).href;
} catch {
this.logger.warn(`invalid sensitiveMediaDetectionApiUrl: ${baseUrl}`);
return sources.map(() => null);
}
const results: (Prediction[] | null)[] = [];
for (let i = 0; i < sources.length; i += chunkSize) {
const chunk = sources.slice(i, i + chunkSize);
results.push(...await this.detectChunk(url, apiKey, timeout, chunk));
}
return results;
}
@bindThis
private async detectChunk(url: string, apiKey: string | null, timeout: number, chunk: Buffer[]): Promise<(Prediction[] | null)[]> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const form = new FormData();
for (let i = 0; i < chunk.length; i++) {
const image = Uint8Array.from(chunk[i]);
form.append(`image${i}`, new Blob([image], { type: 'image/png' }), `${i}.png`);
if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu();
}
// Content-Type は FormData から boundary 付きで自動設定させるため、手動設定はしない。
// 手動指定すると boundary の欠落・不一致で multipart として読めなくなる。
const headers: Record<string, string> = {};
if (apiKey != null && apiKey !== '') {
headers['Authorization'] = `Bearer ${apiKey}`;
if (!isSupportedCpu) {
console.error('This CPU cannot use TensorFlow.');
return null;
}
const res = await fetch(url, {
method: 'POST',
headers,
body: form,
// 外部サービスとして通常の proxy / private address 制限を適用する。
// サイドカーへの private network 接続は allowedPrivateNetworks 等で明示的に許可する。
agent: (u) => this.httpRequestService.getAgentByUrl(u),
signal: controller.signal,
});
const tf = await import('@tensorflow/tfjs-node');
tf.env().global.fetch = fetch;
if (!res.ok) {
this.logger.warn(`sensitive detection request failed: ${res.status} ${res.statusText}`);
return chunk.map(() => null);
if (this.model == null) {
const nsfw = await import('nsfwjs/core');
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.model = await nsfw.load(pathToFileURL(this.modelDir).toString(), { size: 299 });
}
});
}
const body = await res.json();
if (!isDetectImagesResponse(body)) {
this.logger.warn(`sensitive detection responded with unexpected shape: ${JSON.stringify(body)}`);
return chunk.map(() => null);
const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
const image = await tf.node.decodeImage(buffer, 3) as any;
try {
const predictions = await this.model.classify(image);
return predictions;
} finally {
image.dispose();
}
if (!body.success) {
this.logger.warn(`sensitive detection responded with failure: ${body.error.code}`);
return chunk.map(() => null);
}
const items = body.result.results;
return chunk.map((_, i) => {
const item = items[i];
return (item.success) ? item.predictions : null;
});
} catch (err) {
this.logger.warn(`sensitive detection error: ${err instanceof Error ? err.message : String(err)}`);
return chunk.map(() => null);
} finally {
clearTimeout(timer);
console.error(err);
return null;
}
}
private async computeIsSupportedCpu(): Promise<boolean> {
switch (process.arch) {
case 'x64': {
const cpuFlags = await this.getCpuFlags();
return REQUIRED_CPU_FLAGS_X64.every(required => cpuFlags.includes(required));
}
case 'arm64': {
// As far as I know, no required CPU flags for ARM64.
return true;
}
default: {
return false;
}
}
}
@bindThis
private async getCpuFlags(): Promise<string[]> {
const si = await import('systeminformation');
const str = await si.cpuFlags();
return str.split(/\s+/);
}
}

View File

@@ -154,12 +154,15 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
import { QueueModule } from './QueueModule.js';
import { QueueService } from './QueueService.js';
import { LoggerService } from './LoggerService.js';
import { TelemetryService } from './telemetry/TelemetryService.js';
import { WorldRoomService } from './WorldRoomService.js';
import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js';
import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js';
import { WorldAvatarService } from './WorldAvatarService.js';
import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js';
import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $TelemetryService: Provider = { provide: 'TelemetryService', useExisting: TelemetryService };
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
@@ -231,6 +234,9 @@ const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatServic
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService };
const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService };
const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@@ -286,6 +292,8 @@ const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $WorldRoomEntityService: Provider = { provide: 'WorldRoomEntityService', useExisting: WorldRoomEntityService };
const $WorldAvatarEntityService: Provider = { provide: 'WorldAvatarEntityService', useExisting: WorldAvatarEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -384,6 +392,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RegistryApiService,
ReversiService,
PageService,
WorldRoomService,
WorldRoomMultiplayService,
WorldAvatarService,
ChartLoggerService,
FederationChart,
@@ -439,6 +450,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ReversiGameEntityService,
MetaEntityService,
SystemWebhookEntityService,
WorldRoomEntityService,
WorldAvatarEntityService,
ApAudienceService,
ApDbResolverService,
@@ -460,7 +473,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApPersonService,
ApQuestionService,
QueueService,
TelemetryService,
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
@@ -535,6 +547,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RegistryApiService,
$ReversiService,
$PageService,
$WorldRoomService,
$WorldRoomMultiplayService,
$WorldAvatarService,
$ChartLoggerService,
$FederationChart,
@@ -590,6 +605,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ReversiGameEntityService,
$MetaEntityService,
$SystemWebhookEntityService,
$WorldRoomEntityService,
$WorldAvatarEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -609,7 +626,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApNoteService,
$ApPersonService,
$ApQuestionService,
$TelemetryService,
//#endregion
],
exports: [
@@ -686,6 +702,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RegistryApiService,
ReversiService,
PageService,
WorldRoomService,
WorldRoomMultiplayService,
WorldAvatarService,
FederationChart,
NotesChart,
@@ -740,6 +759,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ReversiGameEntityService,
MetaEntityService,
SystemWebhookEntityService,
WorldRoomEntityService,
WorldAvatarEntityService,
ApAudienceService,
ApDbResolverService,
@@ -761,7 +782,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApPersonService,
ApQuestionService,
QueueService,
TelemetryService,
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
@@ -835,6 +855,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RegistryApiService,
$ReversiService,
$PageService,
$WorldRoomService,
$WorldRoomMultiplayService,
$WorldAvatarService,
$FederationChart,
$NotesChart,
@@ -889,6 +912,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ReversiGameEntityService,
$MetaEntityService,
$SystemWebhookEntityService,
$WorldRoomEntityService,
$WorldAvatarEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -908,7 +933,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApNoteService,
$ApPersonService,
$ApQuestionService,
$TelemetryService,
//#endregion
],
})

View File

@@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
import sharp from 'sharp';
import type { Sharp } from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { In, IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
@@ -301,7 +300,7 @@ export class DriveService {
};
}
let img: Sharp | null = null;
let img: sharp.Sharp | null = null;
let satisfyWebpublic: boolean;
let isAnimated: boolean;

View File

@@ -3,9 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { URLSearchParams } from 'node:url';
import * as nodemailer from 'nodemailer';
import juice from 'juice';
import { Inject, Injectable } from '@nestjs/common';
import { validate as validateEmail } from 'deep-email-validator';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@@ -195,7 +197,6 @@ export class EmailService {
} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
} else {
const { validate: validateEmail } = await import('deep-email-validator');
validated = await validateEmail({
email: emailAddress,
validateRegex: true,

Some files were not shown because too many files have changed in this diff Show More