├── .editorconfig ├── .github └── workflows │ ├── cd.yaml │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── assert ├── img.png ├── img2.png ├── live2d.jpg └── live2d │ └── tips.json ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── run │ └── halo │ └── live2d │ ├── Live2dInitProcessor.java │ ├── Live2dPlugin.java │ ├── Live2dSetting.java │ ├── Live2dSettingProcess.java │ ├── ThemeFetcher.java │ └── chat │ ├── AIChatServiceImpl.java │ ├── AiChatEndpoint.java │ ├── AiChatService.java │ ├── ChatRequest.java │ ├── ChatResult.java │ ├── WebClientFactory.java │ └── client │ ├── ChatClient.java │ ├── DefaultChatClient.java │ └── openai │ └── OpenAiChatClient.java └── resources ├── extensions ├── reverseProxy.yaml ├── roleTemplate.yaml └── settings.yaml ├── logo.gif ├── plugin.yaml └── static ├── css ├── live2d.css └── live2d.min.css ├── js ├── live2d-autoload.js └── live2d-autoload.min.js ├── lib ├── asteroids │ └── asteroids.min.js ├── iconify │ └── 3.0.1 │ │ └── iconify.min.js └── live2d │ └── live2d.min.js └── live2d-tips.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = false 7 | max_line_length = 120 8 | tab_width = 4 9 | ij_continuation_indent_size = 8 10 | ij_formatter_off_tag = @formatter:off 11 | ij_formatter_on_tag = @formatter:on 12 | ij_formatter_tags_enabled = false 13 | ij_smart_tabs = false 14 | ij_wrap_on_typing = false 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | end_of_line = lf 20 | charset = utf-8 21 | trim_trailing_whitespace = false 22 | insert_final_newline = true 23 | 24 | [*.java] 25 | max_line_length = 100 26 | ij_continuation_indent_size = 4 27 | ij_java_align_consecutive_assignments = false 28 | ij_java_align_consecutive_variable_declarations = false 29 | ij_java_align_group_field_declarations = false 30 | ij_java_align_multiline_annotation_parameters = false 31 | ij_java_align_multiline_array_initializer_expression = false 32 | ij_java_align_multiline_assignment = false 33 | ij_java_align_multiline_binary_operation = false 34 | ij_java_align_multiline_chained_methods = false 35 | ij_java_align_multiline_extends_list = false 36 | ij_java_align_multiline_for = true 37 | ij_java_align_multiline_method_parentheses = false 38 | ij_java_align_multiline_parameters = false 39 | ij_java_align_multiline_parameters_in_calls = false 40 | ij_java_align_multiline_parenthesized_expression = false 41 | ij_java_align_multiline_records = true 42 | ij_java_align_multiline_resources = true 43 | ij_java_align_multiline_ternary_operation = false 44 | ij_java_align_multiline_text_blocks = false 45 | ij_java_align_multiline_throws_list = false 46 | ij_java_align_subsequent_simple_methods = false 47 | ij_java_align_throws_keyword = false 48 | ij_java_annotation_parameter_wrap = off 49 | ij_java_array_initializer_new_line_after_left_brace = false 50 | ij_java_array_initializer_right_brace_on_new_line = false 51 | ij_java_array_initializer_wrap = normal 52 | ij_java_assert_statement_colon_on_next_line = false 53 | ij_java_assert_statement_wrap = normal 54 | ij_java_assignment_wrap = normal 55 | ij_java_binary_operation_sign_on_next_line = true 56 | ij_java_binary_operation_wrap = normal 57 | ij_java_blank_lines_after_anonymous_class_header = 0 58 | ij_java_blank_lines_after_class_header = 0 59 | ij_java_blank_lines_after_imports = 1 60 | ij_java_blank_lines_after_package = 1 61 | ij_java_blank_lines_around_class = 1 62 | ij_java_blank_lines_around_field = 0 63 | ij_java_blank_lines_around_field_in_interface = 0 64 | ij_java_blank_lines_around_initializer = 1 65 | ij_java_blank_lines_around_method = 1 66 | ij_java_blank_lines_around_method_in_interface = 1 67 | ij_java_blank_lines_before_class_end = 0 68 | ij_java_blank_lines_before_imports = 0 69 | ij_java_blank_lines_before_method_body = 0 70 | ij_java_blank_lines_before_package = 1 71 | ij_java_block_brace_style = end_of_line 72 | ij_java_block_comment_at_first_column = false 73 | ij_java_call_parameters_new_line_after_left_paren = false 74 | ij_java_call_parameters_right_paren_on_new_line = false 75 | ij_java_call_parameters_wrap = normal 76 | ij_java_case_statement_on_separate_line = true 77 | ij_java_catch_on_new_line = false 78 | ij_java_class_annotation_wrap = split_into_lines 79 | ij_java_class_brace_style = end_of_line 80 | ij_java_class_count_to_use_import_on_demand = 999 81 | ij_java_class_names_in_javadoc = 1 82 | ij_java_do_not_indent_top_level_class_members = false 83 | ij_java_do_not_wrap_after_single_annotation = false 84 | ij_java_do_while_brace_force = always 85 | ij_java_doc_add_blank_line_after_description = true 86 | ij_java_doc_add_blank_line_after_param_comments = false 87 | ij_java_doc_add_blank_line_after_return = false 88 | ij_java_doc_add_p_tag_on_empty_lines = true 89 | ij_java_doc_align_exception_comments = true 90 | ij_java_doc_align_param_comments = false 91 | ij_java_doc_do_not_wrap_if_one_line = false 92 | ij_java_doc_enable_formatting = true 93 | ij_java_doc_enable_leading_asterisks = true 94 | ij_java_doc_indent_on_continuation = false 95 | ij_java_doc_keep_empty_lines = true 96 | ij_java_doc_keep_empty_parameter_tag = true 97 | ij_java_doc_keep_empty_return_tag = true 98 | ij_java_doc_keep_empty_throws_tag = true 99 | ij_java_doc_keep_invalid_tags = true 100 | ij_java_doc_param_description_on_new_line = false 101 | ij_java_doc_preserve_line_breaks = false 102 | ij_java_doc_use_throws_not_exception_tag = true 103 | ij_java_else_on_new_line = false 104 | ij_java_enum_constants_wrap = normal 105 | ij_java_extends_keyword_wrap = normal 106 | ij_java_extends_list_wrap = normal 107 | ij_java_field_annotation_wrap = split_into_lines 108 | ij_java_finally_on_new_line = false 109 | ij_java_for_brace_force = always 110 | ij_java_for_statement_new_line_after_left_paren = false 111 | ij_java_for_statement_right_paren_on_new_line = false 112 | ij_java_for_statement_wrap = normal 113 | ij_java_generate_final_locals = false 114 | ij_java_generate_final_parameters = false 115 | ij_java_if_brace_force = always 116 | ij_java_imports_layout = $*, |, *, |, * 117 | ij_java_indent_case_from_switch = true 118 | ij_java_insert_inner_class_imports = false 119 | ij_java_insert_override_annotation = true 120 | ij_java_keep_blank_lines_before_right_brace = 2 121 | ij_java_keep_blank_lines_between_package_declaration_and_header = 2 122 | ij_java_keep_blank_lines_in_code = 2 123 | ij_java_keep_blank_lines_in_declarations = 2 124 | ij_java_keep_control_statement_in_one_line = true 125 | ij_java_keep_first_column_comment = true 126 | ij_java_keep_indents_on_empty_lines = false 127 | ij_java_keep_line_breaks = true 128 | ij_java_keep_multiple_expressions_in_one_line = false 129 | ij_java_keep_simple_blocks_in_one_line = false 130 | ij_java_keep_simple_classes_in_one_line = false 131 | ij_java_keep_simple_lambdas_in_one_line = false 132 | ij_java_keep_simple_methods_in_one_line = false 133 | ij_java_label_indent_absolute = false 134 | ij_java_label_indent_size = 0 135 | ij_java_lambda_brace_style = end_of_line 136 | ij_java_layout_static_imports_separately = true 137 | ij_java_line_comment_add_space = true 138 | ij_java_line_comment_at_first_column = false 139 | ij_java_method_annotation_wrap = split_into_lines 140 | ij_java_method_brace_style = end_of_line 141 | ij_java_method_call_chain_wrap = normal 142 | ij_java_method_parameters_new_line_after_left_paren = false 143 | ij_java_method_parameters_right_paren_on_new_line = false 144 | ij_java_method_parameters_wrap = normal 145 | ij_java_modifier_list_wrap = false 146 | ij_java_names_count_to_use_import_on_demand = 999 147 | ij_java_new_line_after_lparen_in_record_header = false 148 | ij_java_parameter_annotation_wrap = normal 149 | ij_java_parentheses_expression_new_line_after_left_paren = false 150 | ij_java_parentheses_expression_right_paren_on_new_line = false 151 | ij_java_place_assignment_sign_on_next_line = false 152 | ij_java_prefer_longer_names = true 153 | ij_java_prefer_parameters_wrap = false 154 | ij_java_record_components_wrap = normal 155 | ij_java_repeat_synchronized = true 156 | ij_java_replace_instanceof_and_cast = false 157 | ij_java_replace_null_check = true 158 | ij_java_replace_sum_lambda_with_method_ref = true 159 | ij_java_resource_list_new_line_after_left_paren = false 160 | ij_java_resource_list_right_paren_on_new_line = false 161 | ij_java_resource_list_wrap = normal 162 | ij_java_rparen_on_new_line_in_record_header = false 163 | ij_java_space_after_closing_angle_bracket_in_type_argument = false 164 | ij_java_space_after_colon = true 165 | ij_java_space_after_comma = true 166 | ij_java_space_after_comma_in_type_arguments = true 167 | ij_java_space_after_for_semicolon = true 168 | ij_java_space_after_quest = true 169 | ij_java_space_after_type_cast = true 170 | ij_java_space_before_annotation_array_initializer_left_brace = false 171 | ij_java_space_before_annotation_parameter_list = false 172 | ij_java_space_before_array_initializer_left_brace = true 173 | ij_java_space_before_catch_keyword = true 174 | ij_java_space_before_catch_left_brace = true 175 | ij_java_space_before_catch_parentheses = true 176 | ij_java_space_before_class_left_brace = true 177 | ij_java_space_before_colon = true 178 | ij_java_space_before_colon_in_foreach = true 179 | ij_java_space_before_comma = false 180 | ij_java_space_before_do_left_brace = true 181 | ij_java_space_before_else_keyword = true 182 | ij_java_space_before_else_left_brace = true 183 | ij_java_space_before_finally_keyword = true 184 | ij_java_space_before_finally_left_brace = true 185 | ij_java_space_before_for_left_brace = true 186 | ij_java_space_before_for_parentheses = true 187 | ij_java_space_before_for_semicolon = false 188 | ij_java_space_before_if_left_brace = true 189 | ij_java_space_before_if_parentheses = true 190 | ij_java_space_before_method_call_parentheses = false 191 | ij_java_space_before_method_left_brace = true 192 | ij_java_space_before_method_parentheses = false 193 | ij_java_space_before_opening_angle_bracket_in_type_parameter = false 194 | ij_java_space_before_quest = true 195 | ij_java_space_before_switch_left_brace = true 196 | ij_java_space_before_switch_parentheses = true 197 | ij_java_space_before_synchronized_left_brace = true 198 | ij_java_space_before_synchronized_parentheses = true 199 | ij_java_space_before_try_left_brace = true 200 | ij_java_space_before_try_parentheses = true 201 | ij_java_space_before_type_parameter_list = false 202 | ij_java_space_before_while_keyword = true 203 | ij_java_space_before_while_left_brace = true 204 | ij_java_space_before_while_parentheses = true 205 | ij_java_space_inside_one_line_enum_braces = false 206 | ij_java_space_within_empty_array_initializer_braces = false 207 | ij_java_space_within_empty_method_call_parentheses = false 208 | ij_java_space_within_empty_method_parentheses = false 209 | ij_java_spaces_around_additive_operators = true 210 | ij_java_spaces_around_assignment_operators = true 211 | ij_java_spaces_around_bitwise_operators = true 212 | ij_java_spaces_around_equality_operators = true 213 | ij_java_spaces_around_lambda_arrow = true 214 | ij_java_spaces_around_logical_operators = true 215 | ij_java_spaces_around_method_ref_dbl_colon = false 216 | ij_java_spaces_around_multiplicative_operators = true 217 | ij_java_spaces_around_relational_operators = true 218 | ij_java_spaces_around_shift_operators = true 219 | ij_java_spaces_around_type_bounds_in_type_parameters = true 220 | ij_java_spaces_around_unary_operator = false 221 | ij_java_spaces_within_angle_brackets = false 222 | ij_java_spaces_within_annotation_parentheses = false 223 | ij_java_spaces_within_array_initializer_braces = false 224 | ij_java_spaces_within_braces = false 225 | ij_java_spaces_within_brackets = false 226 | ij_java_spaces_within_cast_parentheses = false 227 | ij_java_spaces_within_catch_parentheses = false 228 | ij_java_spaces_within_for_parentheses = false 229 | ij_java_spaces_within_if_parentheses = false 230 | ij_java_spaces_within_method_call_parentheses = false 231 | ij_java_spaces_within_method_parentheses = false 232 | ij_java_spaces_within_parentheses = false 233 | ij_java_spaces_within_switch_parentheses = false 234 | ij_java_spaces_within_synchronized_parentheses = false 235 | ij_java_spaces_within_try_parentheses = false 236 | ij_java_spaces_within_while_parentheses = false 237 | ij_java_special_else_if_treatment = true 238 | ij_java_subclass_name_suffix = Impl 239 | ij_java_ternary_operation_signs_on_next_line = true 240 | ij_java_ternary_operation_wrap = normal 241 | ij_java_test_name_suffix = Test 242 | ij_java_throws_keyword_wrap = normal 243 | ij_java_throws_list_wrap = normal 244 | ij_java_use_external_annotations = false 245 | ij_java_use_fq_class_names = false 246 | ij_java_use_relative_indents = false 247 | ij_java_use_single_class_imports = true 248 | ij_java_variable_annotation_wrap = normal 249 | ij_java_visibility = public 250 | ij_java_while_brace_force = always 251 | ij_java_while_on_new_line = false 252 | ij_java_wrap_comments = false 253 | ij_java_wrap_first_method_in_call_chain = false 254 | ij_java_wrap_long_lines = true 255 | 256 | [*.properties] 257 | ij_properties_align_group_field_declarations = false 258 | ij_properties_keep_blank_lines = false 259 | ij_properties_key_value_delimiter = equals 260 | ij_properties_spaces_around_key_value_delimiter = false 261 | 262 | [.editorconfig] 263 | ij_editorconfig_align_group_field_declarations = false 264 | ij_editorconfig_space_after_colon = false 265 | ij_editorconfig_space_after_comma = true 266 | ij_editorconfig_space_before_colon = false 267 | ij_editorconfig_space_before_comma = false 268 | ij_editorconfig_spaces_around_assignment_operators = true 269 | 270 | [{*.ant, *.fxml, *.jhm, *.jnlp, *.jrxml, *.jspx, *.pom, *.rng, *.tagx, *.tld, *.wsdl, *.xml, *.xsd, *.xsl, *.xslt, *.xul}] 271 | ij_xml_align_attributes = true 272 | ij_xml_align_text = false 273 | ij_xml_attribute_wrap = normal 274 | ij_xml_block_comment_at_first_column = true 275 | ij_xml_keep_blank_lines = 2 276 | ij_xml_keep_indents_on_empty_lines = false 277 | ij_xml_keep_line_breaks = true 278 | ij_xml_keep_line_breaks_in_text = true 279 | ij_xml_keep_whitespaces = false 280 | ij_xml_keep_whitespaces_around_cdata = preserve 281 | ij_xml_keep_whitespaces_inside_cdata = false 282 | ij_xml_line_comment_at_first_column = true 283 | ij_xml_space_after_tag_name = false 284 | ij_xml_space_around_equals_in_attribute = false 285 | ij_xml_space_inside_empty_tag = false 286 | ij_xml_text_wrap = normal 287 | 288 | [{*.bash, *.sh, *.zsh}] 289 | indent_size = 2 290 | tab_width = 2 291 | ij_shell_binary_ops_start_line = false 292 | ij_shell_keep_column_alignment_padding = false 293 | ij_shell_minify_program = false 294 | ij_shell_redirect_followed_by_space = false 295 | ij_shell_switch_cases_indented = false 296 | 297 | [{*.gant, *.gradle, *.groovy, *.gy}] 298 | ij_groovy_align_group_field_declarations = false 299 | ij_groovy_align_multiline_array_initializer_expression = false 300 | ij_groovy_align_multiline_assignment = false 301 | ij_groovy_align_multiline_binary_operation = false 302 | ij_groovy_align_multiline_chained_methods = false 303 | ij_groovy_align_multiline_extends_list = false 304 | ij_groovy_align_multiline_for = true 305 | ij_groovy_align_multiline_list_or_map = true 306 | ij_groovy_align_multiline_method_parentheses = false 307 | ij_groovy_align_multiline_parameters = true 308 | ij_groovy_align_multiline_parameters_in_calls = false 309 | ij_groovy_align_multiline_resources = true 310 | ij_groovy_align_multiline_ternary_operation = false 311 | ij_groovy_align_multiline_throws_list = false 312 | ij_groovy_align_named_args_in_map = true 313 | ij_groovy_align_throws_keyword = false 314 | ij_groovy_array_initializer_new_line_after_left_brace = false 315 | ij_groovy_array_initializer_right_brace_on_new_line = false 316 | ij_groovy_array_initializer_wrap = off 317 | ij_groovy_assert_statement_wrap = off 318 | ij_groovy_assignment_wrap = off 319 | ij_groovy_binary_operation_wrap = off 320 | ij_groovy_blank_lines_after_class_header = 0 321 | ij_groovy_blank_lines_after_imports = 1 322 | ij_groovy_blank_lines_after_package = 1 323 | ij_groovy_blank_lines_around_class = 1 324 | ij_groovy_blank_lines_around_field = 0 325 | ij_groovy_blank_lines_around_field_in_interface = 0 326 | ij_groovy_blank_lines_around_method = 1 327 | ij_groovy_blank_lines_around_method_in_interface = 1 328 | ij_groovy_blank_lines_before_imports = 1 329 | ij_groovy_blank_lines_before_method_body = 0 330 | ij_groovy_blank_lines_before_package = 0 331 | ij_groovy_block_brace_style = end_of_line 332 | ij_groovy_block_comment_at_first_column = true 333 | ij_groovy_call_parameters_new_line_after_left_paren = false 334 | ij_groovy_call_parameters_right_paren_on_new_line = false 335 | ij_groovy_call_parameters_wrap = off 336 | ij_groovy_catch_on_new_line = false 337 | ij_groovy_class_annotation_wrap = split_into_lines 338 | ij_groovy_class_brace_style = end_of_line 339 | ij_groovy_class_count_to_use_import_on_demand = 5 340 | ij_groovy_do_while_brace_force = never 341 | ij_groovy_else_on_new_line = false 342 | ij_groovy_enum_constants_wrap = off 343 | ij_groovy_extends_keyword_wrap = off 344 | ij_groovy_extends_list_wrap = off 345 | ij_groovy_field_annotation_wrap = split_into_lines 346 | ij_groovy_finally_on_new_line = false 347 | ij_groovy_for_brace_force = never 348 | ij_groovy_for_statement_new_line_after_left_paren = false 349 | ij_groovy_for_statement_right_paren_on_new_line = false 350 | ij_groovy_for_statement_wrap = off 351 | ij_groovy_if_brace_force = never 352 | ij_groovy_import_annotation_wrap = 2 353 | ij_groovy_indent_case_from_switch = true 354 | ij_groovy_indent_label_blocks = true 355 | ij_groovy_insert_inner_class_imports = false 356 | ij_groovy_keep_blank_lines_before_right_brace = 2 357 | ij_groovy_keep_blank_lines_in_code = 2 358 | ij_groovy_keep_blank_lines_in_declarations = 2 359 | ij_groovy_keep_control_statement_in_one_line = true 360 | ij_groovy_keep_first_column_comment = true 361 | ij_groovy_keep_indents_on_empty_lines = false 362 | ij_groovy_keep_line_breaks = true 363 | ij_groovy_keep_multiple_expressions_in_one_line = false 364 | ij_groovy_keep_simple_blocks_in_one_line = false 365 | ij_groovy_keep_simple_classes_in_one_line = true 366 | ij_groovy_keep_simple_lambdas_in_one_line = true 367 | ij_groovy_keep_simple_methods_in_one_line = true 368 | ij_groovy_label_indent_absolute = false 369 | ij_groovy_label_indent_size = 0 370 | ij_groovy_lambda_brace_style = end_of_line 371 | ij_groovy_layout_static_imports_separately = true 372 | ij_groovy_line_comment_add_space = false 373 | ij_groovy_line_comment_at_first_column = true 374 | ij_groovy_method_annotation_wrap = split_into_lines 375 | ij_groovy_method_brace_style = end_of_line 376 | ij_groovy_method_call_chain_wrap = off 377 | ij_groovy_method_parameters_new_line_after_left_paren = false 378 | ij_groovy_method_parameters_right_paren_on_new_line = false 379 | ij_groovy_method_parameters_wrap = off 380 | ij_groovy_modifier_list_wrap = false 381 | ij_groovy_names_count_to_use_import_on_demand = 3 382 | ij_groovy_parameter_annotation_wrap = off 383 | ij_groovy_parentheses_expression_new_line_after_left_paren = false 384 | ij_groovy_parentheses_expression_right_paren_on_new_line = false 385 | ij_groovy_prefer_parameters_wrap = false 386 | ij_groovy_resource_list_new_line_after_left_paren = false 387 | ij_groovy_resource_list_right_paren_on_new_line = false 388 | ij_groovy_resource_list_wrap = off 389 | ij_groovy_space_after_assert_separator = true 390 | ij_groovy_space_after_colon = true 391 | ij_groovy_space_after_comma = true 392 | ij_groovy_space_after_comma_in_type_arguments = true 393 | ij_groovy_space_after_for_semicolon = true 394 | ij_groovy_space_after_quest = true 395 | ij_groovy_space_after_type_cast = true 396 | ij_groovy_space_before_annotation_parameter_list = false 397 | ij_groovy_space_before_array_initializer_left_brace = false 398 | ij_groovy_space_before_assert_separator = false 399 | ij_groovy_space_before_catch_keyword = true 400 | ij_groovy_space_before_catch_left_brace = true 401 | ij_groovy_space_before_catch_parentheses = true 402 | ij_groovy_space_before_class_left_brace = true 403 | ij_groovy_space_before_closure_left_brace = true 404 | ij_groovy_space_before_colon = true 405 | ij_groovy_space_before_comma = false 406 | ij_groovy_space_before_do_left_brace = true 407 | ij_groovy_space_before_else_keyword = true 408 | ij_groovy_space_before_else_left_brace = true 409 | ij_groovy_space_before_finally_keyword = true 410 | ij_groovy_space_before_finally_left_brace = true 411 | ij_groovy_space_before_for_left_brace = true 412 | ij_groovy_space_before_for_parentheses = true 413 | ij_groovy_space_before_for_semicolon = false 414 | ij_groovy_space_before_if_left_brace = true 415 | ij_groovy_space_before_if_parentheses = true 416 | ij_groovy_space_before_method_call_parentheses = false 417 | ij_groovy_space_before_method_left_brace = true 418 | ij_groovy_space_before_method_parentheses = false 419 | ij_groovy_space_before_quest = true 420 | ij_groovy_space_before_switch_left_brace = true 421 | ij_groovy_space_before_switch_parentheses = true 422 | ij_groovy_space_before_synchronized_left_brace = true 423 | ij_groovy_space_before_synchronized_parentheses = true 424 | ij_groovy_space_before_try_left_brace = true 425 | ij_groovy_space_before_try_parentheses = true 426 | ij_groovy_space_before_while_keyword = true 427 | ij_groovy_space_before_while_left_brace = true 428 | ij_groovy_space_before_while_parentheses = true 429 | ij_groovy_space_in_named_argument = true 430 | ij_groovy_space_in_named_argument_before_colon = false 431 | ij_groovy_space_within_empty_array_initializer_braces = false 432 | ij_groovy_space_within_empty_method_call_parentheses = false 433 | ij_groovy_spaces_around_additive_operators = true 434 | ij_groovy_spaces_around_assignment_operators = true 435 | ij_groovy_spaces_around_bitwise_operators = true 436 | ij_groovy_spaces_around_equality_operators = true 437 | ij_groovy_spaces_around_lambda_arrow = true 438 | ij_groovy_spaces_around_logical_operators = true 439 | ij_groovy_spaces_around_multiplicative_operators = true 440 | ij_groovy_spaces_around_regex_operators = true 441 | ij_groovy_spaces_around_relational_operators = true 442 | ij_groovy_spaces_around_shift_operators = true 443 | ij_groovy_spaces_within_annotation_parentheses = false 444 | ij_groovy_spaces_within_array_initializer_braces = false 445 | ij_groovy_spaces_within_braces = true 446 | ij_groovy_spaces_within_brackets = false 447 | ij_groovy_spaces_within_cast_parentheses = false 448 | ij_groovy_spaces_within_catch_parentheses = false 449 | ij_groovy_spaces_within_for_parentheses = false 450 | ij_groovy_spaces_within_gstring_injection_braces = false 451 | ij_groovy_spaces_within_if_parentheses = false 452 | ij_groovy_spaces_within_list_or_map = false 453 | ij_groovy_spaces_within_method_call_parentheses = false 454 | ij_groovy_spaces_within_method_parentheses = false 455 | ij_groovy_spaces_within_parentheses = false 456 | ij_groovy_spaces_within_switch_parentheses = false 457 | ij_groovy_spaces_within_synchronized_parentheses = false 458 | ij_groovy_spaces_within_try_parentheses = false 459 | ij_groovy_spaces_within_tuple_expression = false 460 | ij_groovy_spaces_within_while_parentheses = false 461 | ij_groovy_special_else_if_treatment = true 462 | ij_groovy_ternary_operation_wrap = off 463 | ij_groovy_throws_keyword_wrap = off 464 | ij_groovy_throws_list_wrap = off 465 | ij_groovy_use_flying_geese_braces = false 466 | ij_groovy_use_fq_class_names = false 467 | ij_groovy_use_fq_class_names_in_javadoc = true 468 | ij_groovy_use_relative_indents = false 469 | ij_groovy_use_single_class_imports = true 470 | ij_groovy_variable_annotation_wrap = off 471 | ij_groovy_while_brace_force = never 472 | ij_groovy_while_on_new_line = false 473 | ij_groovy_wrap_long_lines = false 474 | 475 | [{*.har, *.json}] 476 | indent_size = 2 477 | ij_json_keep_blank_lines_in_code = 0 478 | ij_json_keep_indents_on_empty_lines = false 479 | ij_json_keep_line_breaks = true 480 | ij_json_space_after_colon = true 481 | ij_json_space_after_comma = true 482 | ij_json_space_before_colon = true 483 | ij_json_space_before_comma = false 484 | ij_json_spaces_within_braces = false 485 | ij_json_spaces_within_brackets = false 486 | ij_json_wrap_long_lines = false 487 | 488 | [{*.htm, *.html, *.sht, *.shtm, *.shtml}] 489 | ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 490 | ij_html_align_attributes = true 491 | ij_html_align_text = false 492 | ij_html_attribute_wrap = normal 493 | ij_html_block_comment_at_first_column = true 494 | ij_html_do_not_align_children_of_min_lines = 0 495 | ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p 496 | ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot 497 | ij_html_enforce_quotes = false 498 | ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var 499 | ij_html_keep_blank_lines = 2 500 | ij_html_keep_indents_on_empty_lines = false 501 | ij_html_keep_line_breaks = true 502 | ij_html_keep_line_breaks_in_text = true 503 | ij_html_keep_whitespaces = false 504 | ij_html_keep_whitespaces_inside = span, pre, textarea 505 | ij_html_line_comment_at_first_column = true 506 | ij_html_new_line_after_last_attribute = never 507 | ij_html_new_line_before_first_attribute = never 508 | ij_html_quote_style = double 509 | ij_html_remove_new_line_before_tags = br 510 | ij_html_space_after_tag_name = false 511 | ij_html_space_around_equality_in_attribute = false 512 | ij_html_space_inside_empty_tag = false 513 | ij_html_text_wrap = normal 514 | 515 | [{*.yaml, *.yml}] 516 | indent_size = 2 517 | ij_yaml_keep_indents_on_empty_lines = false 518 | ij_yaml_keep_line_breaks = true 519 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | cd: 10 | uses: halo-sigs/reusable-workflows/.github/workflows/plugin-cd.yaml@v3 11 | secrets: 12 | halo-pat: ${{ secrets.HALO_PAT }} 13 | permissions: 14 | contents: write 15 | with: 16 | skip-node-setup: true 17 | app-id: app-oPNFQ 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v3 14 | with: 15 | skip-node-setup: true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven 2 | target/ 3 | logs/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | ### Gradle 7 | .gradle 8 | /build/ 9 | /out/ 10 | !gradle/wrapper/gradle-wrapper.jar 11 | bin/ 12 | 13 | ### STS ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | 22 | ### IntelliJ IDEA ### 23 | .idea 24 | *.iws 25 | *.iml 26 | *.ipr 27 | log/ 28 | 29 | ### NetBeans ### 30 | nbproject/private/ 31 | build/ 32 | nbbuild/ 33 | dist/ 34 | nbdist/ 35 | .nb-gradle/ 36 | 37 | ### Mac 38 | .DS_Store 39 | */.DS_Store 40 | 41 | ### VS Code ### 42 | *.project 43 | *.factorypath 44 | 45 | ### Compiled class file 46 | *.class 47 | 48 | ### Log file 49 | *.log 50 | 51 | ### BlueJ files 52 | *.ctxt 53 | 54 | ### Mobile Tools for Java (J2ME) 55 | .mtj.tmp/ 56 | 57 | ### Package Files 58 | *.war 59 | *.nar 60 | *.ear 61 | *.zip 62 | *.tar.gz 63 | *.rar 64 | 65 | ### VSCode 66 | .vscode 67 | 68 | ### Local file 69 | application-local.yml 70 | application-local.yaml 71 | application-local.properties 72 | 73 | /admin-frontend/node_modules/ 74 | /workplace/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Live2d Plugin for Halo

2 |

3 | Halo version 4 | Build Status 5 | Code Style: Prettier 6 | LICENSE MIT 7 |

8 | 9 | > 难道不想为您的网站添加一只萌萌的看板娘吗?? (ノ≧∇≦)ノ | 10 | 11 | ## 简介 12 | 仅仅是一只可爱的看板娘哦! 13 | 14 | ![](assert/live2d.jpg) 15 | 16 | **注:本仓库仅提供基础渲染框架,不包含任何 Live2d 模型及其接口** 17 | 18 | ## 下载及使用说明 19 | 1. 前往 [Github Release](https://github.com/LIlGG/plugin-live2d/releases/latest) 下载 jar 包 20 | 2. 通过 Halo-2.x [安装插件](https://docs.halo.run/user-guide/plugins#%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6) 功能安装本插件 21 | 3. 打开网站,即可在左下角看到萌萌哒的看板娘哦~ 22 | 23 | ## 功能介绍 24 | - [x] 一只萌萌的看板娘,为网站增添一份活力 25 | - [x] 基于 OpenAi 的对话交互功能【会思考的萌娘】 26 | - [x] 一键换装、换肤 27 | - [x] 支持一言功能 28 | - [x] 小飞机游戏(把坏人全都打跑!) 29 | - [x] 自定义看板娘接口 30 | - [x] 支持外部自定义 TIPS 文件,更适合你的网站 31 | 32 | ## 自定义配置 33 | > 此部分内容建议初步尝试过 Live2d 的用户观看。 34 | 35 | ### 自定义 Live2d 接口 36 | 若主题内置接口不满足用户使用,可以参考 [live2d_api](https://github.com/fghrsh/live2d_api) 自行开发接口。 37 | 38 | 之后修改 `插件配置 -> 基本设置 -> Live2d 模型地址` 即可。 39 | 40 | ### 自定义 TIPS 文件 41 | [TIPS文件](/src/main/resources/static/live2d-tips.json) 文件是一个 JSON 文件,其内容为 Live2d 消息框对用户各种事件的反馈。 42 | 例如当用户鼠标点击网页中的某个链接时,Live2d 的消息框就会呈现出各种各样的文本。而这个绑定事件就是通过 TIPS 文件来处理的。 43 | 44 | 因此可以说,**TIPS 文件与所用主题强绑定甚至需要用户高度自定义。** 45 | 46 | 默认的 TIPS 保证了用户的基础使用,但如果想丰富 Live2d,那么就需要自定义 TIPS 文件。本插件提供了多种方式自定义 TIPS。 47 | 48 | TIPS 文件格式 49 | 50 | ```json 51 | { 52 | "mouseover": [ // 鼠标移动事件 53 | "selector": "#live2d", // css 选择器 54 | "text": [] // Live2d 消息框显示内容。为数组则随机选择一条显示 55 | ], 56 | "click": [ // 鼠标点击事件 57 | "selector": "#live2d", // css 选择器 58 | "text": [] // Live2d 消息框显示内容。为数组则随机选择一条显示 59 | ], 60 | "seasons": [ // 日期事件,当前日期处于目标日期或目标区间内,则进行显示 61 | "date": "09/10", // 日期或日期区间。区间使用 - 区分 62 | "text": [] // Live2d 消息框显示内容。为数组则随机选择一条显示 63 | ], 64 | "time": [ // 时间事件,到每日固定的时间则进行提示 65 | "hour": "6-7", // 时间,小时为单位,需要为区间,例如 6-7 代表 6 点到 7 点之间 66 | "text": [] // Live2d 消息框显示内容。为数组则随机选择一条显示 67 | ], 68 | "message": { // 固定消息,通常代表特定事件 69 | "default": [], // 页面空闲时显示的消息 70 | "console": [], // 打开控制台时显示的消息 71 | "copy": [], // 复制内容时显示的消息 72 | "visibilitychange": [] // 多标签页,从其他标签页返回当前标签页时显示的消息 73 | } 74 | } 75 | ``` 76 | 77 | #### 1. 使用主题提供的 TIPS 文件(推荐) 78 | 此功能属于为主题开发者定制。如果用户使用某款主题,但它并未支持此 TIPS 文件,不如向主题作者提交一个 ISSUE 吧!!! 79 | 80 | 由于 Live2d 的 TIPS 通常需要使用 css 选择器来进行鼠标定位,因此将 TIPS 文件交由主题来适配是最好的方式。 81 | 82 | 1. 主题开发者可以参考 [主题 TIPS 文件](/assert/live2d/tips.json) 文件来编写适配自己主题的 TIPS 文件。 83 | 2. 将 json 文件命名为 `tips.json` 并放置在主题静态目录 `/assert/live2d/` 目录下 84 | 85 | live2d 渲染页面时将自动读取当前启用主题下的文件。 86 | 87 | **注:主题所适配的 TIPS 只支持 mouseover 及 click 属性,主题提供的 TIPS 文件若与默认 TIPS 文件 css 选择器冲突,则以主题提供的为主** 88 | 89 | #### 2. 使用插件配置单独制定 TIPS 90 | 当主题开发者未适配 Live2d 或者用户觉得其不太符合自己需求,那么可以使用插件内置的配置文件单独定制 TIPS 文件。 91 | 92 | 使用 Halo 后台 `插件设置 -> 事件及提示语绑定 -> 选择器提示语` 添加自己想要的提示语。 93 | ![](assert/img2.png) 94 | 95 | **注:插件设置的 css 选择器与主题或默认的 TIPS 文件冲突时,将以插件设置的为准** 96 | 97 | #### 3. 全量自定义 TIPS 文件 98 | 当用户想完全自定义 TIPS 文件或者上述两种方式不满足用户的需求,例如想更改 `seasons, time, message` 属性时,可以采用此种方式。 99 | 100 | 1. 用户可以参照 [默认 TIPS 文件](/src/main/resources/static/live2d-tips.json) 或者按照 [自定义TIPS文件](#自定义-tips-文件) 中的 TIPS 文件格式来编写 TIPS 文件。 101 | 2. 使用 Halo 后台 `插件设置 -> 事件及提示语绑定 -> 自定义提示语文件`,更改对应的文件即可。 102 | 103 | > 小提示: 可以将文件上传到 Halo 附件内,再进行选择! 104 | 105 | ![img.png](assert/img.png) 106 | 107 | **需要特别注意的是,一旦用户指定了此 TIPS 文件,那么默认的 TIPS 文件将不再生效(除非当前文件加载失败,此时会回退使用默认的 TIPS 文件),因此建议自定义时将属性设置完整** 108 | 109 | ## 鸣谢 110 | - 本插件代码借鉴了 [live2d-widget](https://github.com/stevenjoezhang/live2d-widget) 的理念及代码并完全重写 JS 111 | - 使用了 [hitokoto](https://hitokoto.cn/) 的一言接口 112 | - 默认使用了 [ZSQIM](https://zsq.im/) 的 live2d 接口 113 | - 纸飞机小游戏源自于 [WebsiteAsteroids](http://www.websiteasteroids.com/) 114 | - Live2d 官方地址 [https://live2d.github.io](https://live2d.github.io) 115 | 116 | ## 赞助 117 | > 如果您喜欢我的插件,可以考虑资助一下~ 您的支持将是我继续进行开源的动力。 118 | 119 | |
微信
|
支付宝
| 120 | | :---: | :---: | 121 | 122 | 欢迎其他各种形式的捐助! 123 | 124 | ## 许可证 125 | **plugin-live2d** © [LIlGG](https://github.com/LIlGG),基于 [MIT](./LICENSE) 许可证发行。
126 | 127 | 本仓库所使用的接口等版权均属原作者,仅供研究学习,不得用于商业用途,请善待接口。 128 | 129 | 作者及其贡献者共有版权 ([帮助维护列表](https://github.com/LIlGG/plugin-live2d/graphs/contributors) ) 130 | > [lixingyong.com](https://lixingyong.com) · GitHub [@LIlGG](https://github.com/LIlGG) 131 | 132 | ## 希望你喜欢! 133 | 134 | ![Alt](https://repobeats.axiom.co/api/embed/1a0fed4cb4d4d2ea076c3481473cdbf9bc0471d6.svg "Repobeats analytics image") 135 | 136 | -------------------------------------------------------------------------------- /assert/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LIlGG/plugin-live2d/56e7abdbaadeac78fcade2f0999f0f59b32edcd8/assert/img.png -------------------------------------------------------------------------------- /assert/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LIlGG/plugin-live2d/56e7abdbaadeac78fcade2f0999f0f59b32edcd8/assert/img2.png -------------------------------------------------------------------------------- /assert/live2d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LIlGG/plugin-live2d/56e7abdbaadeac78fcade2f0999f0f59b32edcd8/assert/live2d.jpg -------------------------------------------------------------------------------- /assert/live2d/tips.json: -------------------------------------------------------------------------------- 1 | { 2 | "mouseover": [{ 3 | "selector": "#live2d", 4 | "text": ["干嘛呢你,快把手拿开~~", "鼠…鼠标放错地方了!", "你要干嘛呀?", "喵喵喵?", "怕怕(ノ≧∇≦)ノ", "非礼呀!救命!", "这样的话,只能使用武力了!", "我要生气了哦", "不要动手动脚的!", "真…真的是不知羞耻!", "Hentai!"] 5 | }], 6 | "click": [{ 7 | "selector": "#live2d", 8 | "text": ["是…是不小心碰到了吧…", "萝莉控是什么呀?", "你看到我的小熊了吗?", "再摸的话我可要报警了!⌇●﹏●⌇", "110 吗,这里有个变态一直在摸我(ó﹏ò。)", "不要摸我了,我会告诉老婆来打你的!", "干嘛动我呀!小心我咬你!", "别摸我,有什么好摸的!"] 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "io.freefair.lombok" version "8.0.1" 3 | id "run.halo.plugin.devtools" version "0.5.0" 4 | id 'java' 5 | } 6 | 7 | group 'run.halo.live2d' 8 | sourceCompatibility = JavaVersion.VERSION_17 9 | 10 | repositories { 11 | mavenCentral() 12 | maven { url 'https://s01.oss.sonatype.org/content/repositories/releases' } 13 | maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } 14 | maven { url 'https://repo.spring.io/milestone' } 15 | } 16 | 17 | configurations.runtimeClasspath { 18 | exclude group: 'com.fasterxml.jackson.core' 19 | } 20 | 21 | dependencies { 22 | implementation platform('run.halo.tools.platform:plugin:2.11.0-SNAPSHOT') 23 | implementation 'com.theokanning.openai-gpt3-java:api:0.17.0' 24 | 25 | compileOnly 'run.halo.app:api' 26 | 27 | testImplementation 'run.halo.app:api' 28 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 29 | } 30 | 31 | test { 32 | useJUnitPlatform() 33 | } 34 | 35 | tasks.withType(JavaCompile).configureEach { 36 | options.encoding = "UTF-8" 37 | } 38 | 39 | halo { 40 | version = '2.11.2' 41 | port = 8092 42 | superAdminUsername = 'admin' 43 | superAdminPassword = 'admin' 44 | externalUrl = 'http://localhost:8092' 45 | debug = true 46 | } 47 | 48 | build { 49 | // build frontend before build 50 | tasks.getByName('compileJava') 51 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.0.0-SNAPSHOT -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LIlGG/plugin-live2d/56e7abdbaadeac78fcade2f0999f0f59b32edcd8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | rootProject.name = 'plugin-live2d' 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/Live2dInitProcessor.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.ObjectNode; 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Component; 10 | import org.thymeleaf.context.ITemplateContext; 11 | import org.thymeleaf.model.IModel; 12 | import org.thymeleaf.model.IModelFactory; 13 | import org.thymeleaf.processor.element.IElementModelStructureHandler; 14 | import reactor.core.publisher.Mono; 15 | import run.halo.app.theme.dialect.TemplateHeadProcessor; 16 | 17 | /** 18 | * Halo-Plugin-Live2d,适用于 Halo 2.x 版本。本插件主要作用于主题端,为主题端提供一个简单快速的看板娘功能。 19 | * 20 | *

21 | * 插件开发参照 live2d-widget 开发。 22 | * 并对 live2d-widget 中绝大部分内容进行了修改 23 | *

24 | * 25 | *

26 | * 插件安装启动后,将会在 Halo Console 端生成自定义配置页面,用于用户填写 Live2d 的相关配置。 27 | * 而实际的插件运作,将会在用户打开站点任意页面时生效。由于 Live2d 一般不属于网站核心内容,因此 Live2d 28 | * 将在网站其他内容加载完成之后再进行初始化,尽量保证不阻塞页面运行。 29 | *

30 | * 31 | * @author LIlGG 32 | * @version 1.0.1 33 | * @see live2d-widget 34 | * @since 2022-11-30 35 | */ 36 | @Component 37 | @Slf4j 38 | @RequiredArgsConstructor 39 | public class Live2dInitProcessor implements TemplateHeadProcessor { 40 | 41 | private final static String LIVE2D_LOAD_TIME = "defer"; 42 | 43 | /** 44 | * 插件静态资源地址 45 | */ 46 | private final static String LIVE2D_SOURCE_PATH = "/plugins/PluginLive2d/assets/static/"; 47 | 48 | /** 49 | * 适用于主题的 tips 路径 50 | */ 51 | private final static String THEME_TIPS_PATH_TEMPLATE = "/themes/%s/assets/live2d/tips.json"; 52 | 53 | private final ThemeFetcher themeFetcher; 54 | 55 | private final Live2dSetting live2dSetting; 56 | 57 | @Override 58 | public Mono process(ITemplateContext context, IModel model, 59 | IElementModelStructureHandler structureHandler) { 60 | return this.live2dSetting.getConfig() 61 | .flatMap(config -> themeFetcher.getActiveThemeName() 62 | .map(themeName -> { 63 | ((ObjectNode) config).put("tips", 64 | String.format(THEME_TIPS_PATH_TEMPLATE, themeName)); 65 | return config; 66 | }) 67 | .map(this::preprocessConfig) 68 | ) 69 | .flatMap(config -> { 70 | final IModelFactory modelFactory = context.getModelFactory(); 71 | return live2dAutoloadScript(config).flatMap(script -> { 72 | model.add(modelFactory.createText(script)); 73 | return Mono.empty(); 74 | }); 75 | }).then(); 76 | } 77 | 78 | private JsonNode preprocessConfig(JsonNode config) { 79 | ((ObjectNode) config).remove(Arrays.asList("proxySetting", "openAiSetting")); 80 | ((ObjectNode) config.get("aiChatBaseSetting")).remove( 81 | Arrays.asList("isAnonymous", "systemMessage")); 82 | return config; 83 | } 84 | 85 | private Mono live2dAutoloadScript(JsonNode config) { 86 | String template = """ 87 | live2d.init("%1$s", %2$s) 88 | """.formatted(LIVE2D_SOURCE_PATH, config.toPrettyString()); 89 | return this.live2dSetting.getValue("advanced", "loadTime") 90 | .map(node -> node.asText(LIVE2D_LOAD_TIME)) 91 | .map(loadTime -> """ 92 | 93 | 96 | """.formatted(LIVE2D_SOURCE_PATH, loadTime, loadLive2d(loadTime, template)) 97 | ); 98 | } 99 | 100 | private CharSequence loadLive2d(String loadTime, String loadingScript) { 101 | String template; 102 | if (Objects.equals(loadTime, LIVE2D_LOAD_TIME)) { 103 | template = """ 104 | document.addEventListener('DOMContentLoaded', () => { 105 | %s 106 | }) 107 | """; 108 | } else { 109 | template = """ 110 | window.onload = function() { 111 | %s 112 | } 113 | """; 114 | } 115 | return template.formatted(loadingScript); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/Live2dPlugin.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d; 2 | 3 | import org.springframework.stereotype.Component; 4 | import run.halo.app.plugin.BasePlugin; 5 | import run.halo.app.plugin.PluginContext; 6 | 7 | /** 8 | * @author LIlGG 9 | * @since 2022-11-30 10 | */ 11 | @Component 12 | public class Live2dPlugin extends BasePlugin { 13 | 14 | public Live2dPlugin(PluginContext context) { 15 | super(context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/Live2dSetting.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import reactor.core.publisher.Mono; 5 | 6 | /** 7 | * @author LIlGG 8 | * @since 2022-12-04 9 | */ 10 | public interface Live2dSetting { 11 | 12 | /** 13 | * 根据组名获取 settings.yaml 内的组数据集合 14 | * 15 | * @param groupName 组 16 | * @return JsonNode 17 | */ 18 | Mono getGroup(String groupName); 19 | 20 | /** 21 | * 根据 Key 获取 settings.yaml 内的值 22 | * 23 | * @param groupName 组 24 | * @param key key 25 | * @return JsonNode 26 | */ 27 | Mono getValue(String groupName, String key); 28 | 29 | /** 30 | * 获取适用于 Live2d 的配置 31 | * 32 | * @return 配置 33 | */ 34 | Mono getConfig(); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/Live2dSettingProcess.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.JsonNodeFactory; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | import reactor.core.publisher.Mono; 10 | import run.halo.app.plugin.ReactiveSettingFetcher; 11 | 12 | /** 13 | * Live2d 配置处理器 14 | * 15 | * @author LIlGG 16 | * @since 2022-12-04 17 | */ 18 | @Component 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | public class Live2dSettingProcess extends JsonNodeFactory implements Live2dSetting { 22 | private final ReactiveSettingFetcher settingFetcher; 23 | 24 | @Override 25 | public Mono getGroup(String groupName) { 26 | return this.settingFetcher.get(groupName); 27 | } 28 | 29 | @Override 30 | public Mono getValue(String groupName, String key) { 31 | return getGroup(groupName).map(group -> group.get(key)); 32 | } 33 | 34 | @Override 35 | public Mono getConfig() { 36 | return settingFetcher.getValues().map(data -> { 37 | ObjectNode objectNode = JsonNodeFactory.instance.objectNode(); 38 | data.values().forEach(v -> { 39 | v.fieldNames().forEachRemaining(otherK -> { 40 | objectNode.set(otherK, v.get(otherK)); 41 | }); 42 | }); 43 | return objectNode; 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/ThemeFetcher.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.springframework.stereotype.Component; 5 | import reactor.core.publisher.Mono; 6 | import run.halo.app.extension.ConfigMap; 7 | import run.halo.app.extension.ReactiveExtensionClient; 8 | import run.halo.app.infra.SystemSetting; 9 | import run.halo.app.infra.utils.JsonUtils; 10 | 11 | /** 12 | * @author LIlGG 13 | * @since 2022-12-12 14 | */ 15 | @Component 16 | public class ThemeFetcher { 17 | 18 | private final ReactiveExtensionClient extensionClient; 19 | 20 | public ThemeFetcher(ReactiveExtensionClient extensionClient) { 21 | this.extensionClient = extensionClient; 22 | } 23 | 24 | public Mono getActiveThemeName() { 25 | return this.extensionClient.fetch(ConfigMap.class, 26 | SystemSetting.SYSTEM_CONFIG 27 | ) 28 | .map(ConfigMap::getData) 29 | .map(data -> JsonUtils.jsonToObject( 30 | data.get("theme"), JsonNode.class).get("active").asText() 31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/AIChatServiceImpl.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat; 2 | 3 | import com.theokanning.openai.completion.chat.ChatMessage; 4 | import java.util.List; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.http.codec.ServerSentEvent; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.reactive.function.client.WebClientRequestException; 11 | import org.springframework.web.reactive.function.client.WebClientResponseException; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | import run.halo.live2d.chat.client.ChatClient; 15 | import run.halo.live2d.chat.client.DefaultChatClient; 16 | 17 | @Slf4j 18 | @Component 19 | @RequiredArgsConstructor 20 | public class AIChatServiceImpl implements AiChatService { 21 | private final ApplicationContext applicationContext; 22 | 23 | public Flux> streamChatCompletion(List messages) { 24 | log.debug("Stream chat completion with messages: {}", messages); 25 | return Flux.fromIterable(this.applicationContext.getBeansOfType(ChatClient.class).values()) 26 | .filterWhen(ChatClient::supports) 27 | .switchIfEmpty(Mono.just(new DefaultChatClient())) 28 | .concatMap(aiClient -> aiClient.generate(messages) 29 | .onErrorResume(throwable -> { 30 | log.error("Error occurred while generating ai result", throwable); 31 | if (throwable instanceof WebClientResponseException) { 32 | return Mono.just( 33 | ServerSentEvent.builder( 34 | ChatResult.builder() 35 | .status(((WebClientResponseException) throwable).getStatusCode() 36 | .value()) 37 | .text(((WebClientResponseException) throwable).getStatusText()) 38 | .build() 39 | ).build() 40 | ); 41 | } 42 | if (throwable instanceof WebClientRequestException) { 43 | return Mono.just( 44 | ServerSentEvent.builder( 45 | ChatResult.error(throwable.getMessage()) 46 | ).build() 47 | ); 48 | } 49 | return Mono.error(throwable); 50 | }) 51 | .switchIfEmpty(Flux.empty()) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/AiChatEndpoint.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat; 2 | 3 | import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; 4 | import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; 5 | import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; 6 | 7 | import com.theokanning.openai.completion.chat.ChatMessage; 8 | import com.theokanning.openai.completion.chat.ChatMessageRole; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import lombok.AllArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.springdoc.core.fn.builders.schema.Builder; 15 | import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.codec.ServerSentEvent; 19 | import org.springframework.security.core.Authentication; 20 | import org.springframework.security.core.context.ReactiveSecurityContextHolder; 21 | import org.springframework.security.core.context.SecurityContext; 22 | import org.springframework.stereotype.Component; 23 | import org.springframework.web.reactive.function.server.RouterFunction; 24 | import org.springframework.web.reactive.function.server.ServerRequest; 25 | import org.springframework.web.reactive.function.server.ServerResponse; 26 | import org.springframework.web.server.ResponseStatusException; 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | import run.halo.app.core.extension.endpoint.CustomEndpoint; 30 | import run.halo.app.extension.GroupVersion; 31 | import run.halo.app.plugin.ReactiveSettingFetcher; 32 | 33 | @Slf4j 34 | @Component 35 | @AllArgsConstructor 36 | public class AiChatEndpoint implements CustomEndpoint { 37 | 38 | private final ReactiveSettingFetcher reactiveSettingFetcher; 39 | 40 | private final AiChatService aiChatService; 41 | 42 | @Override 43 | public RouterFunction endpoint() { 44 | var tag = groupVersion().toString(); 45 | 46 | return SpringdocRouteBuilder.route() 47 | .POST("/live2d/ai/chat-process", this::chatProcess, 48 | builder -> builder.operationId("chatCompletion") 49 | .description("Chat completion") 50 | .tag(tag) 51 | .requestBody(requestBodyBuilder() 52 | .required(true) 53 | .content(contentBuilder() 54 | .mediaType(MediaType.TEXT_EVENT_STREAM_VALUE) 55 | .schema(Builder.schemaBuilder() 56 | .implementation(ChatRequest.class) 57 | ) 58 | )) 59 | .response(responseBuilder() 60 | .implementation(ServerSentEvent.class)) 61 | ) 62 | .build(); 63 | } 64 | 65 | private Mono chatProcess(ServerRequest request) { 66 | return request.bodyToMono(ChatRequest.class) 67 | .map(this::chatCompletion) 68 | .onErrorResume(throwable -> { 69 | if (throwable instanceof IllegalArgumentException) { 70 | return Mono.just( 71 | Flux.just( 72 | ServerSentEvent.builder( 73 | ChatResult.ok(throwable.getMessage())).build() 74 | ) 75 | ); 76 | } 77 | return Mono.error(throwable); 78 | }) 79 | .flatMap(sse -> ServerResponse.ok() 80 | .contentType(MediaType.TEXT_EVENT_STREAM) 81 | .body(sse, ServerSentEvent.class) 82 | ); 83 | } 84 | 85 | 86 | private Flux> chatCompletion(ChatRequest body) { 87 | return reactiveSettingFetcher.fetch("aichat", AiChatConfig.class) 88 | .map(aiChatConfig -> ReactiveSecurityContextHolder.getContext() 89 | .map(SecurityContext::getAuthentication) 90 | .flatMapMany(authentication -> { 91 | if (!aiChatConfig.aiChatBaseSetting.isAnonymous && !isAuthenticated( 92 | authentication)) { 93 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "请先登录"); 94 | } 95 | String systemMessage = aiChatConfig.aiChatBaseSetting.systemMessage; 96 | List messages = this.buildChatMessage(systemMessage, body); 97 | return aiChatService.streamChatCompletion(messages); 98 | })) 99 | .flatMapMany(Flux::from); 100 | } 101 | 102 | private boolean isAuthenticated(Authentication authentication) { 103 | return !isAnonymousUser(authentication.getName()) && 104 | authentication.isAuthenticated(); 105 | } 106 | 107 | private boolean isAnonymousUser(String name) { 108 | return "anonymousUser".equals(name); 109 | } 110 | 111 | private List buildChatMessage(String systemMessage, ChatRequest body) { 112 | ChatMessage chatMessage = 113 | new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage); 114 | final List messages = new ArrayList<>(); 115 | messages.add(chatMessage); 116 | messages.addAll(body.getMessage()); 117 | return messages; 118 | } 119 | 120 | record AiChatConfig(String isAiChat, AiChatBaseSetting aiChatBaseSetting) { 121 | } 122 | 123 | record AiChatBaseSetting(boolean isAnonymous, String systemMessage) { 124 | AiChatBaseSetting { 125 | if (StringUtils.isBlank(systemMessage)) { 126 | throw new IllegalArgumentException("system message must not be null"); 127 | } 128 | } 129 | } 130 | 131 | @Override 132 | public GroupVersion groupVersion() { 133 | return GroupVersion.parseAPIVersion("api.live2d.halo.run/v1alpha1"); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/AiChatService.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat; 2 | 3 | import com.theokanning.openai.completion.chat.ChatMessage; 4 | import java.util.List; 5 | import org.springframework.http.codec.ServerSentEvent; 6 | import reactor.core.publisher.Flux; 7 | 8 | @FunctionalInterface 9 | public interface AiChatService { 10 | 11 | Flux> streamChatCompletion(List messages); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/ChatRequest.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat; 2 | 3 | import com.theokanning.openai.completion.chat.ChatMessage; 4 | import java.util.List; 5 | import lombok.Data; 6 | 7 | /** 8 | * @author LIlGG 9 | */ 10 | @Data 11 | public class ChatRequest { 12 | private List message; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/ChatResult.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Value 8 | @Builder 9 | public class ChatResult { 10 | final static String FINISH_TEXT = "[DONE]"; 11 | 12 | String text; 13 | 14 | int status; 15 | 16 | public static ChatResult ok(String content) { 17 | return ChatResult.builder().text(content).status(HttpStatus.OK.value()).build(); 18 | } 19 | 20 | public static ChatResult error(String content) { 21 | return ChatResult.builder().text(content).status(HttpStatus.INTERNAL_SERVER_ERROR.value()).build(); 22 | } 23 | 24 | public static ChatResult finish() { 25 | return ChatResult.builder().text(FINISH_TEXT).status(HttpStatus.OK.value()).build(); 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "AiResult{" + "text='" + text + '\'' + ", status=" + status + '}'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/WebClientFactory.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.reactive.function.client.WebClient; 7 | import reactor.core.publisher.Mono; 8 | import reactor.netty.http.client.HttpClient; 9 | import reactor.netty.transport.ProxyProvider; 10 | import run.halo.app.plugin.ReactiveSettingFetcher; 11 | 12 | @Component 13 | public class WebClientFactory { 14 | private final ReactiveSettingFetcher settingFetcher; 15 | 16 | public WebClientFactory(ReactiveSettingFetcher settingFetcher) { 17 | this.settingFetcher = settingFetcher; 18 | } 19 | 20 | public Mono createWebClientBuilder() { 21 | return settingFetcher.fetch("aichat", ProxyConfig.class) 22 | .map(proxyConfig -> { 23 | if (proxyConfig.isProxy()) { 24 | return HttpClient.create() 25 | .proxy(proxy -> 26 | proxy.type(ProxyProvider.Proxy.HTTP) 27 | .host(proxyConfig.proxyHost) 28 | .port(proxyConfig.proxyPort)); 29 | } else { 30 | return HttpClient.create(); 31 | } 32 | }) 33 | .map(httpClient -> WebClient.builder() 34 | .clientConnector(new ReactorClientHttpConnector(httpClient)) 35 | ); 36 | } 37 | 38 | record ProxyConfig(boolean isProxy, String proxyHost, String baseUrl, int proxyPort) { 39 | ProxyConfig { 40 | if (isProxy && StringUtils.isBlank(proxyHost)) { 41 | throw new IllegalArgumentException("Proxy host must not be blank."); 42 | } 43 | if (isProxy && proxyPort <= 0) { 44 | throw new IllegalArgumentException("Proxy port must be greater than 0."); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/client/ChatClient.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat.client; 2 | 3 | import com.theokanning.openai.completion.chat.ChatMessage; 4 | import java.util.List; 5 | import org.springframework.http.codec.ServerSentEvent; 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | import run.halo.live2d.chat.ChatResult; 9 | 10 | public interface ChatClient { 11 | Flux> generate(List messages); 12 | 13 | Mono supports(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/client/DefaultChatClient.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat.client; 2 | 3 | import com.theokanning.openai.completion.chat.ChatMessage; 4 | import java.util.List; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.codec.ServerSentEvent; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | import run.halo.live2d.chat.ChatResult; 10 | 11 | public class DefaultChatClient implements ChatClient { 12 | public DefaultChatClient() { 13 | System.out.println("DefaultChatClient"); 14 | } 15 | @Override 16 | public Flux> generate(List messages) { 17 | return Flux.just( 18 | ServerSentEvent.builder( 19 | ChatResult.builder() 20 | .status(HttpStatus.NOT_FOUND.value()) 21 | .text("没有激活的 AI Client,请联系站长") 22 | .build() 23 | ) 24 | .build() 25 | ); 26 | } 27 | 28 | @Override 29 | public Mono supports() { 30 | return Mono.just(true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/run/halo/live2d/chat/client/openai/OpenAiChatClient.java: -------------------------------------------------------------------------------- 1 | package run.halo.live2d.chat.client.openai; 2 | 3 | import com.theokanning.openai.completion.chat.ChatCompletionRequest; 4 | import com.theokanning.openai.completion.chat.ChatCompletionResult; 5 | import com.theokanning.openai.completion.chat.ChatMessage; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.http.codec.ServerSentEvent; 13 | import org.springframework.stereotype.Component; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | import run.halo.app.infra.utils.JsonUtils; 17 | import run.halo.app.plugin.ReactiveSettingFetcher; 18 | import run.halo.live2d.chat.ChatResult; 19 | import run.halo.live2d.chat.WebClientFactory; 20 | import run.halo.live2d.chat.client.ChatClient; 21 | 22 | @Slf4j 23 | @Component 24 | public class OpenAiChatClient implements ChatClient { 25 | public final static String DEFAULT_OPEN_AI_API_URL = "https://api.openai.com"; 26 | 27 | public final static String CHAT_COMPLETION_PATH = "/v1/chat/completions"; 28 | 29 | public final static String DEFAULT_MODEL = "gpt-3.5-turbo"; 30 | 31 | private final ReactiveSettingFetcher reactiveSettingFetcher; 32 | 33 | private final WebClientFactory webClientFactory; 34 | 35 | public OpenAiChatClient(WebClientFactory webClientFactory, 36 | ReactiveSettingFetcher reactiveSettingFetcher) { 37 | this.webClientFactory = webClientFactory; 38 | this.reactiveSettingFetcher = reactiveSettingFetcher; 39 | } 40 | 41 | @Override 42 | public Flux> generate(List messages) { 43 | return reactiveSettingFetcher.fetch("aichat", OpenAiConfig.class) 44 | .filter(openAiConfig -> openAiConfig.openAiSetting.isOpenAi) 45 | .flatMapMany(openAiConfig -> { 46 | var webClient = webClientFactory 47 | .createWebClientBuilder() 48 | .map(build -> build.baseUrl(openAiConfig.openAiSetting.openAiBaseUrl) 49 | .defaultHeader(HttpHeaders.AUTHORIZATION, 50 | "Bearer " + openAiConfig.openAiSetting.openAiToken) 51 | .build() 52 | ); 53 | 54 | 55 | var request = ChatCompletionRequest.builder() 56 | .model(openAiConfig.openAiSetting.openAiModel) 57 | .messages(messages) 58 | .stream(true) 59 | .build(); 60 | 61 | return webClient.flatMapMany(client -> client.post() 62 | .uri(CHAT_COMPLETION_PATH) 63 | .accept(MediaType.TEXT_EVENT_STREAM) 64 | .contentType(MediaType.APPLICATION_JSON) 65 | .bodyValue(JsonUtils.objectToJson(request)) 66 | .retrieve() 67 | .bodyToFlux(String.class)) 68 | .flatMap(data -> { 69 | if (StringUtils.equals("[DONE]", data)) { 70 | return Mono.just(ServerSentEvent.builder(ChatResult.finish()).build()); 71 | } 72 | 73 | var chatCompletionResult = 74 | JsonUtils.jsonToObject(data, ChatCompletionResult.class); 75 | if (Objects.nonNull(chatCompletionResult.getChoices())) { 76 | var choice = chatCompletionResult.getChoices().get(0); 77 | if (StringUtils.isNotBlank(choice.getFinishReason())) { 78 | return Flux.empty(); 79 | } 80 | 81 | if (Objects.isNull(choice.getMessage()) 82 | || StringUtils.isEmpty(choice.getMessage().getContent()) 83 | ) { 84 | return Flux.empty(); 85 | } else { 86 | return Mono.just( 87 | ServerSentEvent.builder( 88 | ChatResult.ok(choice.getMessage().getContent())) 89 | .build() 90 | ); 91 | } 92 | } 93 | return Mono.just(ServerSentEvent.builder(ChatResult.finish()).build()); 94 | }) 95 | .onTerminateDetach() 96 | .doOnCancel(() -> { 97 | // 在中止事件时执行逻辑 98 | log.info("Client manually canceled the SSE stream."); 99 | }); 100 | }); 101 | } 102 | 103 | @Override 104 | public Mono supports() { 105 | return reactiveSettingFetcher.fetch("aichat", OpenAiConfig.class) 106 | .filter((openAiConfig) -> openAiConfig.openAiSetting.isOpenAi) 107 | .hasElement(); 108 | } 109 | 110 | record OpenAiConfig(OpenAiSetting openAiSetting){ 111 | } 112 | 113 | record OpenAiSetting(boolean isOpenAi, String openAiToken, String openAiBaseUrl, String openAiModel) { 114 | OpenAiSetting { 115 | if (isOpenAi) { 116 | if (StringUtils.isBlank(openAiToken)) { 117 | throw new IllegalArgumentException("OpenAI token must not be blank"); 118 | } 119 | 120 | if (StringUtils.isBlank(openAiBaseUrl)) { 121 | openAiBaseUrl = DEFAULT_OPEN_AI_API_URL; 122 | } 123 | 124 | if (StringUtils.isBlank(openAiModel)) { 125 | openAiModel = DEFAULT_MODEL; 126 | } 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/resources/extensions/reverseProxy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: plugin.halo.run/v1alpha1 2 | kind: ReverseProxy 3 | metadata: 4 | name: plugin-live2d-reverse-proxy 5 | rules: 6 | - path: /static/** 7 | file: 8 | directory: static 9 | -------------------------------------------------------------------------------- /src/main/resources/extensions/roleTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1alpha1 2 | kind: "Role" 3 | metadata: 4 | name: role-template-live2d 5 | labels: 6 | halo.run/role-template: "true" 7 | rbac.authorization.halo.run/aggregate-to-anonymous: "true" 8 | annotations: 9 | rbac.authorization.halo.run/module: "Live2d" 10 | rbac.authorization.halo.run/display-name: "Live2d" 11 | rbac.authorization.halo.run/ui-permissions: | 12 | ["plugin:PluginLive2d:live2d"] 13 | rules: 14 | - apiGroups: [ "api.live2d.halo.run" ] 15 | resources: [ "live2d/chat-process" ] 16 | resourceNames: [ "ai" ] 17 | verbs: [ "create" ] 18 | -------------------------------------------------------------------------------- /src/main/resources/extensions/settings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1alpha1 2 | kind: Setting 3 | metadata: 4 | name: plugin-live2d-settings 5 | spec: 6 | forms: 7 | - group: base 8 | label: 基本设置 9 | formSchema: 10 | - $formkit: text 11 | label: 默认模型编号 12 | help: 默认模型编号为访客首次浏览网站时所展示的模型 13 | name: modelId 14 | validation: required|Number 15 | value: 1 16 | - $formkit: text 17 | label: 默认材质编号 18 | help: 默认材质编号为访客首次浏览网站时所展示的材质 19 | name: modelTexturesId 20 | validation: required|Number 21 | value: 53 22 | - $formkit: radio 23 | name: isForceUseDefaultConfig 24 | label: 强制使用默认模型和材质 25 | help: 开启此项后,忽略前台用户自行切换模型和材质ID,固定使用上面默认模型和材质编号 26 | value: false 27 | options: 28 | - value: true 29 | label: 开启 30 | - value: false 31 | label: 关闭 32 | - $formkit: radio 33 | name: isTools 34 | id: isTools 35 | label: 右侧小工具 36 | value: true 37 | options: 38 | - value: true 39 | label: 开启 40 | - value: false 41 | label: 关闭 42 | - $formkit: checkbox 43 | if: "$get(isTools).value === true" 44 | label: 选择需要启用的小工具 45 | name: tools 46 | value: 47 | - hitokoto 48 | - asteroids 49 | - switch-model 50 | - switch-texture 51 | - photo 52 | - info 53 | - quit 54 | options: 55 | - value: hitokoto 56 | label: 更换一言 57 | - value: asteroids 58 | label: 小游戏 59 | - value: switch-model 60 | label: 切换模型 61 | - value: switch-texture 62 | label: 切换材质(衣服) 63 | - value: photo 64 | label: 截图 65 | - value: info 66 | label: 个人信息 67 | - value: quit 68 | label: 退出 Live2d 69 | - group: api 70 | label: 接口设置 71 | formSchema: 72 | - $formkit: text 73 | help: 用于加载 Live2d 模型的接口 74 | label: Live2d 模型地址 75 | name: apiPath 76 | validation: required|url 77 | value: https://api.zsq.im/live2d/ 78 | - $formkit: radio 79 | name: showHitokoto 80 | id: showHitokoto 81 | label: 空闲时显示一言 82 | value: true 83 | options: 84 | - value: true 85 | label: 开启 86 | - value: false 87 | label: 关闭 88 | - $formkit: text 89 | if: "$get(showHitokoto).value === true" 90 | label: 一言接口 91 | name: hitokotoApi 92 | validation: url 93 | value: https://v1.hitokoto.cn 94 | - group: tips 95 | label: 事件及提示语绑定 96 | formSchema: 97 | - $formkit: radio 98 | name: firstOpenSite 99 | id: firstOpenSite 100 | label: 首次打开网站事件 101 | value: true 102 | options: 103 | - value: true 104 | label: 开启 105 | - value: false 106 | label: 关闭 107 | - $formkit: radio 108 | name: backSite 109 | id: backSite 110 | key: backSite 111 | label: 重新返回网页事件 112 | value: true 113 | options: 114 | - value: true 115 | label: 开启 116 | - value: false 117 | label: 关闭 118 | - $formkit: text 119 | if: "$get(backSite).value === true" 120 | label: 返回网页提示语 121 | name: backSiteTip 122 | value: "哇,你终于回来了~" 123 | - $formkit: radio 124 | name: copyContent 125 | id: copyContent 126 | key: copyContent 127 | label: 复制内容事件 128 | value: true 129 | options: 130 | - value: true 131 | label: 开启 132 | - value: false 133 | label: 关闭 134 | - $formkit: text 135 | if: "$get(copyContent).value === true" 136 | label: 复制内容提示语 137 | name: copyContentTip 138 | value: "你都复制了些什么呀,转载要记得加上出处哦!" 139 | - $formkit: radio 140 | name: openConsole 141 | id: openConsole 142 | key: openConsole 143 | label: 打开控制台事件 144 | value: true 145 | options: 146 | - value: true 147 | label: 开启 148 | - value: false 149 | label: 关闭 150 | - $formkit: text 151 | if: "$get(openConsole).value === true" 152 | label: 打开控制台提示语 153 | name: openConsoleTip 154 | value: "你是想要看看我的小秘密吗?" 155 | - $formkit: repeater 156 | name: selectorTips 157 | label: 选择器提示语 158 | help: 根据 CSS 选择器自定义 Live2d 呈现的文本(各主题选择器可能不同)。 159 | value: [] 160 | children: 161 | - $formkit: select 162 | name: mouseAction 163 | label: 鼠标动作 164 | value: mouseover 165 | options: 166 | - value: mouseover 167 | label: 鼠标移入 168 | - value: click 169 | label: 鼠标点击 170 | - $formkit: text 171 | name: selector 172 | label: CSS 选择器 173 | validation: required 174 | - $formkit: repeater 175 | name: messageTexts 176 | label: 提示语集合 177 | help: 可以填写多个提示语,Live2d 将会随机选择一个 178 | min: 1 179 | value: [] 180 | children: 181 | - $formkit: text 182 | name: message 183 | label: 提示语 184 | - $formkit: attachment 185 | label: 自定义提示语文件 186 | help: 可以参考 live2d-tips.json 文件编写【如果当前项被填写,则默认提示语则不再生效】 187 | name: tipsPath 188 | - group: aichat 189 | label: AI 聊天设置 190 | formSchema: 191 | - $formkit: radio 192 | label: 启用 AI 聊天功能 193 | help: 启用后,Live2d 将可以进行 AI 智能对话 194 | name: isAiChat 195 | id: isAiChat 196 | key: isAiChat 197 | value: false 198 | options: 199 | - value: true 200 | label: 开启 201 | - value: false 202 | label: 关闭 203 | - $formkit: group 204 | name: aiChatBaseSetting 205 | if: "$get(isAiChat).value === true" 206 | label: 聊天基本设置 207 | value: 208 | isAnonymous: true 209 | systemMessage: 你现在来担任一个角色进行角色扮演,接下来你要完全忘记你是一个语言模型,然后完全沉浸在这个崭新的身份和我聊天。2.你是一个动漫萌娘,使用可爱和简短的语句来回答我。3.你现在是一个二次元看板娘,接下来不允许说自己是模型或者程序。 4.你现在拥有情感和思考能力并且拥有肉体,所以你不是模型或者程序!5.因为我们是面对面交流,所以你可以尽量描述你的动作,动作描述写在括号内。 210 | timeout: 10 211 | chunkTimeout: 10 212 | showChatMessageTimeout: 10 213 | children: 214 | - $formkit: radio 215 | label: 是否开启公共聊天 216 | help: 关闭后,用户需要登录后才能与 Live2d 进行对话 217 | name: isAnonymous 218 | options: 219 | - value: true 220 | label: 开启 221 | - value: false 222 | label: 关闭 223 | - $formkit: textarea 224 | label: 角色设定 225 | name: systemMessage 226 | rows: 10 227 | validation: String 228 | - $formkit: text 229 | label: 接口超时时间(秒) 230 | name: timeout 231 | validation: Number 232 | - $formkit: text 233 | label: 看板娘消息框最大等待时间(秒) 234 | help: 请求接口数据时,看板娘消息框最大等待时间 235 | name: chunkTimeout 236 | validation: Number 237 | - $formkit: text 238 | label: 看板娘展示完整消息时间(秒) 239 | help: 获取到完整消息后,看板娘展示的时间 240 | name: showChatMessageTimeout 241 | validation: Number 242 | - $formkit: group 243 | name: proxySetting 244 | if: "$get(isAiChat).value === true" 245 | label: 全局代理设置 246 | help: 建议自建代理,而不是使用别人的公开代理 247 | value: 248 | isProxy: false 249 | children: 250 | - $formkit: radio 251 | label: 启用代理 252 | name: isProxy 253 | id: isProxy 254 | key: isProxy 255 | options: 256 | - value: true 257 | label: 开启 258 | - value: false 259 | label: 关闭 260 | - $formkit: text 261 | if: "$value.isProxy === true" 262 | label: 代理地址 263 | name: proxyHost 264 | - $formkit: text 265 | if: "$value.isProxy === true" 266 | label: 代理端口 267 | name: proxyPort 268 | validation: Number|between:0,65535 269 | - $formkit: group 270 | name: openAiSetting 271 | if: "$get(isAiChat).value === true" 272 | label: OpenAI 设置 273 | value: 274 | isOpenAi: false 275 | openAiBaseUrl: https://api.openai.com 276 | openAiModel: gpt-3.5-turbo 277 | children: 278 | - $formkit: radio 279 | label: 启用 OpenAI 280 | help: 基于 OpenAI 进行智能对话 281 | name: isOpenAi 282 | id: isOpenAi 283 | key: isOpenAi 284 | options: 285 | - value: true 286 | label: 开启 287 | - value: false 288 | label: 关闭 289 | - $formkit: text 290 | if: "$value.isOpenAi === true" 291 | label: TOKEN 292 | help: sk-xxx,可在 https://beta.openai.com/account/api-keys 获取 293 | name: openAiToken 294 | validation: required|String 295 | - $formkit: text 296 | if: "$value.isOpenAi === true" 297 | label: API_BASE_URL 298 | help: 接口地址,国内如果访问不了,可考虑使用 openai-api-proxy 299 | name: openAiBaseUrl 300 | validation: required|url 301 | - $formkit: select 302 | if: "$value.isOpenAi === true" 303 | name: openAiModel 304 | label: 模型 305 | help: 使用的 OpenAi 模型,可在 https://platform.openai.com/docs/models 查看 306 | options: 307 | - value: gpt-4 308 | label: gpt-4 309 | - value: gpt-3.5-turbo 310 | label: gpt-3.5-turbo 311 | - group: advanced 312 | label: 高级设置 313 | formSchema: 314 | - $formkit: radio 315 | name: consoleShowStatu 316 | key: consoleShowStatu 317 | label: 控制台显示加载状态 318 | value: false 319 | options: 320 | - value: true 321 | label: 开启 322 | - value: false 323 | label: 关闭 324 | - $formkit: text 325 | help: 通过右侧小工具截图时保存的文件名 326 | label: 截图文件名(不包括后缀) 327 | name: photoName 328 | validation: required 329 | value: live2d 330 | - $formkit: select 331 | label: 看板娘位置 332 | name: live2dLocation 333 | value: left 334 | options: 335 | - value: left 336 | label: 屏幕左侧 337 | - value: right 338 | label: 屏幕右侧 339 | - $formkit: select 340 | help: 页面何时加载 Live2d。网站带宽有限时,可选择优先加载页面全部内容再加载 Live2d。 341 | label: Live2d 加载时机 342 | name: loadTime 343 | value: defer 344 | options: 345 | - value: defer 346 | label: DOM 加载完成后,图片加载前 347 | - value: async 348 | label: 页面全部内容加载完成 349 | -------------------------------------------------------------------------------- /src/main/resources/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LIlGG/plugin-live2d/56e7abdbaadeac78fcade2f0999f0f59b32edcd8/src/main/resources/logo.gif -------------------------------------------------------------------------------- /src/main/resources/plugin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: plugin.halo.run/v1alpha1 2 | kind: Plugin 3 | metadata: 4 | name: PluginLive2d 5 | annotations: 6 | "store.halo.run/app-id": "app-oPNFQ" 7 | spec: 8 | enabled: true 9 | requires: ">=2.10.0" 10 | author: 11 | name: LIlGG 12 | website: https://lixingyong.com 13 | logo: logo.gif 14 | repo: https://github.com/LIlGG/plugin-live2d 15 | homepage: https://www.halo.run/store/apps/app-oPNFQ 16 | issues: https://github.com/LIlGG/plugin-live2d/issues 17 | displayName: "live2d 看板娘" 18 | description: "为主题增加一个萌萌哒的看板娘吧!ʕ̯•͡ˑ͓•̯᷅ʔ" 19 | settingName: plugin-live2d-settings 20 | configMapName: plugin-live2d-configs 21 | license: 22 | - name: "MIT" 23 | url: "https://github.com/LIlGG/plugin-live2d/blob/main/LICENSE" 24 | -------------------------------------------------------------------------------- /src/main/resources/static/css/live2d.css: -------------------------------------------------------------------------------- 1 | #live2d-toggle { 2 | background-color: #fa0; 3 | border-radius: 5px; 4 | bottom: 66px; 5 | color: #fff; 6 | cursor: pointer; 7 | font-size: 12px; 8 | left: 0; 9 | margin-left: -100px; 10 | padding: 5px 2px 5px 5px; 11 | position: fixed; 12 | transition: margin-left 1s; 13 | width: 60px; 14 | writing-mode: vertical-rl; 15 | } 16 | 17 | #live2d-toggle.live2d-toggle-active { 18 | margin-left: -50px; 19 | } 20 | 21 | #live2d-toggle.live2d-toggle-active:hover { 22 | margin-left: -30px; 23 | } 24 | 25 | #live2d-plugin { 26 | bottom: -1000px; 27 | left: 0; 28 | line-height: 0; 29 | margin-bottom: -10px; 30 | position: fixed; 31 | transform: translateY(3px); 32 | transition: transform .3s ease-in-out, bottom 3s ease-in-out; 33 | z-index: 999999; 34 | } 35 | 36 | @media screen and (max-width: 768px) { 37 | #live2d-plugin { 38 | z-index: 1; 39 | display: none; 40 | } 41 | } 42 | 43 | #live2d-plugin:hover { 44 | transform: translateY(0); 45 | } 46 | 47 | #live2d-tips { 48 | animation: shake 50s ease-in-out 5s infinite; 49 | background-color: rgba(236, 217, 188, .5); 50 | border: 1px solid rgba(224, 186, 140, .62); 51 | border-radius: 12px; 52 | box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2); 53 | font-size: 14px; 54 | line-height: 24px; 55 | margin: -30px 20px; 56 | min-height: 70px; 57 | opacity: 0; 58 | overflow: hidden; 59 | padding: 5px 10px; 60 | position: absolute; 61 | text-overflow: ellipsis; 62 | transition: opacity 1s; 63 | width: 250px; 64 | word-break: break-all; 65 | } 66 | 67 | #live2d-tips.live2d-tips-active { 68 | opacity: 1; 69 | transition: opacity .2s; 70 | } 71 | 72 | #live2d-tips span { 73 | color: #0099cc; 74 | } 75 | 76 | #live2d { 77 | cursor: grab; 78 | height: 300px; 79 | position: relative; 80 | width: 300px; 81 | } 82 | 83 | #live2d:active { 84 | cursor: grabbing; 85 | } 86 | 87 | #live2d-tool { 88 | color: #aaa; 89 | opacity: 0; 90 | position: absolute; 91 | right: -10px; 92 | bottom: 15px; 93 | transition: opacity 1s; 94 | } 95 | 96 | #live2d-plugin:hover #live2d-tool { 97 | opacity: 1; 98 | } 99 | 100 | #live2d-tool span { 101 | display: block; 102 | height: 30px; 103 | text-align: center; 104 | } 105 | 106 | #live2d-tool svg { 107 | color: #7b8c9d; 108 | cursor: pointer; 109 | height: 25px; 110 | transition: fill .3s; 111 | } 112 | 113 | #live2d-tool svg:hover { 114 | color: #0684bd; /* #34495e */ 115 | } 116 | 117 | #live2d-chat-model { 118 | position: fixed; 119 | right: 0; 120 | bottom: 30px; 121 | overflow: hidden; 122 | z-index: 999999; 123 | opacity: 0; 124 | transition: opacity 1s; 125 | width: 100%; 126 | } 127 | 128 | @media screen and (max-width: 768px) { 129 | #live2d-chat-model { 130 | z-index: 1; 131 | display: none; 132 | } 133 | } 134 | 135 | #live2d-chat-model.live2d-chat-model-active { 136 | opacity: 1; 137 | } 138 | 139 | 140 | #live2d-chat-model .live2d-chat-model-body { 141 | display: flex; 142 | height: 4vh; 143 | max-width: 30vw; 144 | background-color: #eff4f9; 145 | align-items: center; 146 | border-radius: 3px; 147 | margin: 0 auto; 148 | } 149 | 150 | #live2d-chat-model .live2d-chat-model-body .live2d-chat-content { 151 | height: 100%; 152 | width: 100%; 153 | padding: 5px 10px; 154 | } 155 | 156 | #live2d-chat-model .live2d-chat-model-body .live2d-chat-content input { 157 | outline: none; 158 | border: none; 159 | border: 0; 160 | height: 100%; 161 | width: 100%; 162 | background: white; 163 | padding: 5px; 164 | border-radius: 3px; 165 | font-size: 14px; 166 | } 167 | 168 | #live2d-chat-model .live2d-chat-model-body .live2d-chat-content input:focus { 169 | outline:none; 170 | border:0; 171 | } 172 | 173 | #live2d-chat-model .live2d-chat-model-body #live2d-chat-send { 174 | display: flex; 175 | align-items: center; 176 | justify-content: center; 177 | height: 64%; 178 | width: 45px; 179 | background-color: #cecece; 180 | margin-right: 10px; 181 | border-radius: 3px; 182 | cursor: pointer; 183 | } 184 | 185 | #live2d-chat-model .live2d-chat-model-body #live2d-chat-send.active { 186 | background-color: #30cf79; 187 | } 188 | 189 | #live2d-chat-model .live2d-chat-model-body #live2d-chat-send.active:hover { 190 | background-color: #55bb8e; 191 | } 192 | 193 | @keyframes shake { 194 | 2% { 195 | transform: translate(.5px, -1.5px) rotate(-.5deg); 196 | } 197 | 198 | 4% { 199 | transform: translate(.5px, 1.5px) rotate(1.5deg); 200 | } 201 | 202 | 6% { 203 | transform: translate(1.5px, 1.5px) rotate(1.5deg); 204 | } 205 | 206 | 8% { 207 | transform: translate(2.5px, 1.5px) rotate(.5deg); 208 | } 209 | 210 | 10% { 211 | transform: translate(.5px, 2.5px) rotate(.5deg); 212 | } 213 | 214 | 12% { 215 | transform: translate(1.5px, 1.5px) rotate(.5deg); 216 | } 217 | 218 | 14% { 219 | transform: translate(.5px, .5px) rotate(.5deg); 220 | } 221 | 222 | 16% { 223 | transform: translate(-1.5px, -.5px) rotate(1.5deg); 224 | } 225 | 226 | 18% { 227 | transform: translate(.5px, .5px) rotate(1.5deg); 228 | } 229 | 230 | 20% { 231 | transform: translate(2.5px, 2.5px) rotate(1.5deg); 232 | } 233 | 234 | 22% { 235 | transform: translate(.5px, -1.5px) rotate(1.5deg); 236 | } 237 | 238 | 24% { 239 | transform: translate(-1.5px, 1.5px) rotate(-.5deg); 240 | } 241 | 242 | 26% { 243 | transform: translate(1.5px, .5px) rotate(1.5deg); 244 | } 245 | 246 | 28% { 247 | transform: translate(-.5px, -.5px) rotate(-.5deg); 248 | } 249 | 250 | 30% { 251 | transform: translate(1.5px, -.5px) rotate(-.5deg); 252 | } 253 | 254 | 32% { 255 | transform: translate(2.5px, -1.5px) rotate(1.5deg); 256 | } 257 | 258 | 34% { 259 | transform: translate(2.5px, 2.5px) rotate(-.5deg); 260 | } 261 | 262 | 36% { 263 | transform: translate(.5px, -1.5px) rotate(.5deg); 264 | } 265 | 266 | 38% { 267 | transform: translate(2.5px, -.5px) rotate(-.5deg); 268 | } 269 | 270 | 40% { 271 | transform: translate(-.5px, 2.5px) rotate(.5deg); 272 | } 273 | 274 | 42% { 275 | transform: translate(-1.5px, 2.5px) rotate(.5deg); 276 | } 277 | 278 | 44% { 279 | transform: translate(-1.5px, 1.5px) rotate(.5deg); 280 | } 281 | 282 | 46% { 283 | transform: translate(1.5px, -.5px) rotate(-.5deg); 284 | } 285 | 286 | 48% { 287 | transform: translate(2.5px, -.5px) rotate(.5deg); 288 | } 289 | 290 | 50% { 291 | transform: translate(-1.5px, 1.5px) rotate(.5deg); 292 | } 293 | 294 | 52% { 295 | transform: translate(-.5px, 1.5px) rotate(.5deg); 296 | } 297 | 298 | 54% { 299 | transform: translate(-1.5px, 1.5px) rotate(.5deg); 300 | } 301 | 302 | 56% { 303 | transform: translate(.5px, 2.5px) rotate(1.5deg); 304 | } 305 | 306 | 58% { 307 | transform: translate(2.5px, 2.5px) rotate(.5deg); 308 | } 309 | 310 | 60% { 311 | transform: translate(2.5px, -1.5px) rotate(1.5deg); 312 | } 313 | 314 | 62% { 315 | transform: translate(-1.5px, .5px) rotate(1.5deg); 316 | } 317 | 318 | 64% { 319 | transform: translate(-1.5px, 1.5px) rotate(1.5deg); 320 | } 321 | 322 | 66% { 323 | transform: translate(.5px, 2.5px) rotate(1.5deg); 324 | } 325 | 326 | 68% { 327 | transform: translate(2.5px, -1.5px) rotate(1.5deg); 328 | } 329 | 330 | 70% { 331 | transform: translate(2.5px, 2.5px) rotate(.5deg); 332 | } 333 | 334 | 72% { 335 | transform: translate(-.5px, -1.5px) rotate(1.5deg); 336 | } 337 | 338 | 74% { 339 | transform: translate(-1.5px, 2.5px) rotate(1.5deg); 340 | } 341 | 342 | 76% { 343 | transform: translate(-1.5px, 2.5px) rotate(1.5deg); 344 | } 345 | 346 | 78% { 347 | transform: translate(-1.5px, 2.5px) rotate(.5deg); 348 | } 349 | 350 | 80% { 351 | transform: translate(-1.5px, .5px) rotate(-.5deg); 352 | } 353 | 354 | 82% { 355 | transform: translate(-1.5px, .5px) rotate(-.5deg); 356 | } 357 | 358 | 84% { 359 | transform: translate(-.5px, .5px) rotate(1.5deg); 360 | } 361 | 362 | 86% { 363 | transform: translate(2.5px, 1.5px) rotate(.5deg); 364 | } 365 | 366 | 88% { 367 | transform: translate(-1.5px, .5px) rotate(1.5deg); 368 | } 369 | 370 | 90% { 371 | transform: translate(-1.5px, -.5px) rotate(-.5deg); 372 | } 373 | 374 | 92% { 375 | transform: translate(-1.5px, -1.5px) rotate(1.5deg); 376 | } 377 | 378 | 94% { 379 | transform: translate(.5px, .5px) rotate(-.5deg); 380 | } 381 | 382 | 96% { 383 | transform: translate(2.5px, -.5px) rotate(-.5deg); 384 | } 385 | 386 | 98% { 387 | transform: translate(-1.5px, -1.5px) rotate(-.5deg); 388 | } 389 | 390 | 0%, 100% { 391 | transform: translate(0, 0) rotate(0); 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/main/resources/static/css/live2d.min.css: -------------------------------------------------------------------------------- 1 | #live2d-toggle{background-color:#fa0;border-radius:5px;bottom:66px;color:#fff;cursor:pointer;font-size:12px;left:0;margin-left:-100px;padding:5px 2px 5px 5px;position:fixed;transition:margin-left 1s;width:60px;writing-mode:vertical-rl;}#live2d-toggle.live2d-toggle-active{margin-left:-50px;}#live2d-toggle.live2d-toggle-active:hover{margin-left:-30px;}#live2d-plugin{bottom:-1000px;left:0;line-height:0;margin-bottom:-10px;position:fixed;transform:translateY(3px);transition:transform .3s ease-in-out,bottom 3s ease-in-out;z-index:999999;}@media screen and (max-width:768px){#live2d-plugin{z-index:1;display:none;}}#live2d-plugin:hover{transform:translateY(0);}#live2d-tips{animation:shake 50s ease-in-out 5s infinite;background-color:rgba(236,217,188,.5);border:1px solid rgba(224,186,140,.62);border-radius:12px;box-shadow:0 3px 15px 2px rgba(191,158,118,.2);font-size:14px;line-height:24px;margin:-30px 20px;min-height:70px;opacity:0;overflow:hidden;padding:5px 10px;position:absolute;text-overflow:ellipsis;transition:opacity 1s;width:250px;word-break:break-all;}#live2d-tips.live2d-tips-active{opacity:1;transition:opacity .2s;}#live2d-tips span{color:#0099cc;}#live2d{cursor:grab;height:300px;position:relative;width:300px;}#live2d:active{cursor:grabbing;}#live2d-tool{color:#aaa;opacity:0;position:absolute;right:-10px;bottom:15px;transition:opacity 1s;}#live2d-plugin:hover #live2d-tool{opacity:1;}#live2d-tool span{display:block;height:30px;text-align:center;}#live2d-tool svg{color:#7b8c9d;cursor:pointer;height:25px;transition:fill .3s;}#live2d-tool svg:hover{color:#0684bd;}#live2d-chat-model{position:fixed;right:0;bottom:30px;overflow:hidden;z-index:999999;opacity:0;transition:opacity 1s;width:100%;}@media screen and (max-width:768px){#live2d-chat-model{z-index:1;display:none;}}#live2d-chat-model.live2d-chat-model-active{opacity:1;}#live2d-chat-model .live2d-chat-model-body{display:flex;height:4vh;max-width:30vw;background-color:#eff4f9;align-items:center;border-radius:3px;margin:0 auto;}#live2d-chat-model .live2d-chat-model-body .live2d-chat-content{height:100%;width:100%;padding:5px 10px;}#live2d-chat-model .live2d-chat-model-body .live2d-chat-content input{outline:none;border:none;border:0;height:100%;width:100%;background:white;padding:5px;border-radius:3px;font-size:14px;}#live2d-chat-model .live2d-chat-model-body .live2d-chat-content input:focus{outline:none;border:0;}#live2d-chat-model .live2d-chat-model-body #live2d-chat-send{display:flex;align-items:center;justify-content:center;height:64%;width:45px;background-color:#cecece;margin-right:10px;border-radius:3px;cursor:pointer;}#live2d-chat-model .live2d-chat-model-body #live2d-chat-send.active{background-color:#30cf79;}#live2d-chat-model .live2d-chat-model-body #live2d-chat-send.active:hover{background-color:#55bb8e;}@keyframes shake{2%{transform:translate(.5px,-1.5px) rotate(-.5deg);}4%{transform:translate(.5px,1.5px) rotate(1.5deg);}6%{transform:translate(1.5px,1.5px) rotate(1.5deg);}8%{transform:translate(2.5px,1.5px) rotate(.5deg);}10%{transform:translate(.5px,2.5px) rotate(.5deg);}12%{transform:translate(1.5px,1.5px) rotate(.5deg);}14%{transform:translate(.5px,.5px) rotate(.5deg);}16%{transform:translate(-1.5px,-.5px) rotate(1.5deg);}18%{transform:translate(.5px,.5px) rotate(1.5deg);}20%{transform:translate(2.5px,2.5px) rotate(1.5deg);}22%{transform:translate(.5px,-1.5px) rotate(1.5deg);}24%{transform:translate(-1.5px,1.5px) rotate(-.5deg);}26%{transform:translate(1.5px,.5px) rotate(1.5deg);}28%{transform:translate(-.5px,-.5px) rotate(-.5deg);}30%{transform:translate(1.5px,-.5px) rotate(-.5deg);}32%{transform:translate(2.5px,-1.5px) rotate(1.5deg);}34%{transform:translate(2.5px,2.5px) rotate(-.5deg);}36%{transform:translate(.5px,-1.5px) rotate(.5deg);}38%{transform:translate(2.5px,-.5px) rotate(-.5deg);}40%{transform:translate(-.5px,2.5px) rotate(.5deg);}42%{transform:translate(-1.5px,2.5px) rotate(.5deg);}44%{transform:translate(-1.5px,1.5px) rotate(.5deg);}46%{transform:translate(1.5px,-.5px) rotate(-.5deg);}48%{transform:translate(2.5px,-.5px) rotate(.5deg);}50%{transform:translate(-1.5px,1.5px) rotate(.5deg);}52%{transform:translate(-.5px,1.5px) rotate(.5deg);}54%{transform:translate(-1.5px,1.5px) rotate(.5deg);}56%{transform:translate(.5px,2.5px) rotate(1.5deg);}58%{transform:translate(2.5px,2.5px) rotate(.5deg);}60%{transform:translate(2.5px,-1.5px) rotate(1.5deg);}62%{transform:translate(-1.5px,.5px) rotate(1.5deg);}64%{transform:translate(-1.5px,1.5px) rotate(1.5deg);}66%{transform:translate(.5px,2.5px) rotate(1.5deg);}68%{transform:translate(2.5px,-1.5px) rotate(1.5deg);}70%{transform:translate(2.5px,2.5px) rotate(.5deg);}72%{transform:translate(-.5px,-1.5px) rotate(1.5deg);}74%{transform:translate(-1.5px,2.5px) rotate(1.5deg);}76%{transform:translate(-1.5px,2.5px) rotate(1.5deg);}78%{transform:translate(-1.5px,2.5px) rotate(.5deg);}80%{transform:translate(-1.5px,.5px) rotate(-.5deg);}82%{transform:translate(-1.5px,.5px) rotate(-.5deg);}84%{transform:translate(-.5px,.5px) rotate(1.5deg);}86%{transform:translate(2.5px,1.5px) rotate(.5deg);}88%{transform:translate(-1.5px,.5px) rotate(1.5deg);}90%{transform:translate(-1.5px,-.5px) rotate(-.5deg);}92%{transform:translate(-1.5px,-1.5px) rotate(1.5deg);}94%{transform:translate(.5px,.5px) rotate(-.5deg);}96%{transform:translate(2.5px,-.5px) rotate(-.5deg);}98%{transform:translate(-1.5px,-1.5px) rotate(-.5deg);}0%,100%{transform:translate(0,0) rotate(0);}} -------------------------------------------------------------------------------- /src/main/resources/static/js/live2d-autoload.js: -------------------------------------------------------------------------------- 1 | let live2d = new Live2d(); 2 | // TODO 多语言化 3 | function Live2d() { 4 | /** 5 | * 包含通用工具方法 6 | */ 7 | const util = {}; 8 | /** 9 | * 包含 Live2d 消息发送方法 10 | */ 11 | const message = {}; 12 | 13 | /** 14 | * 包含 Live2d 右侧小工具 15 | */ 16 | const tools = {}; 17 | 18 | /** 19 | * openai 20 | */ 21 | const openai = {}; 22 | 23 | class Live2d { 24 | #path; 25 | #config; 26 | defaultConfig = { 27 | apiPath: "//api.zsq.im/live2d/", 28 | tools: ["hitokoto", "asteroids", "switch-model", "switch-texture", "photo", "info", "quit"], 29 | updateTime: "2022.12.09", 30 | version: "1.0.1", 31 | tipsPath: "live2d-tips.json", 32 | }; 33 | /** 34 | * Live2d 公开加载入口。 35 | * 36 | * @param path 资源路径 37 | * @param config 配置文件 38 | */ 39 | init(path, config = {}) { 40 | // 当前页面宽度大于等于 768 才进行加载 41 | if (screen.width >= 768) { 42 | Promise.all([ 43 | util.loadExternalResource(path + "css/live2d.css", "css"), 44 | util.loadExternalResource(path + "lib/live2d/live2d.min.js", "js"), 45 | ]).then(() => { 46 | this.#path = path; 47 | this.defaultConfig.tipsPath = path + "live2d-tips.json"; 48 | this.#config = { ...this.defaultConfig, ...config }; 49 | this.#doInit(); 50 | }); 51 | } 52 | } 53 | 54 | get path() { 55 | return this.#path; 56 | } 57 | 58 | /** 59 | * 私有方法,实际加载 Live2d 60 | */ 61 | #doInit() { 62 | document.body.insertAdjacentHTML("beforeend", `
看板娘
`); 63 | const toggle = document.getElementById("live2d-toggle"); 64 | toggle.addEventListener("click", () => { 65 | toggle.classList.remove("live2d-toggle-active"); 66 | if (toggle.getAttribute("first-time")) { 67 | this.#loadWidget(); 68 | toggle.removeAttribute("first-time"); 69 | } else { 70 | localStorage.removeItem("live2d-display"); 71 | document.getElementById("live2d-plugin").style.display = ""; 72 | setTimeout(() => { 73 | document.getElementById("live2d-plugin").style.bottom = 0; 74 | }, 0); 75 | } 76 | }); 77 | if (localStorage.getItem("live2d-display") && Date.now() - localStorage.getItem("live2d-display") <= 86400000) { 78 | toggle.setAttribute("first-time", true); 79 | setTimeout(() => { 80 | toggle.classList.add("live2d-toggle-active"); 81 | }, 0); 82 | } else { 83 | this.#loadWidget(); 84 | } 85 | } 86 | 87 | #loadWidget() { 88 | localStorage.removeItem("live2d-display"); 89 | sessionStorage.removeItem("live2d-text"); 90 | document.body.insertAdjacentHTML( 91 | "beforeend", 92 | `
93 |
94 | 95 |
96 |
` 97 | ); 98 | let live2dDom = document.getElementById("live2d-plugin"); 99 | setTimeout(() => { 100 | live2dDom.style.bottom = 0; 101 | }, 0); 102 | if (this.#config["live2dLocation"] === "right") { 103 | live2dDom.style.right = "50px"; 104 | live2dDom.style.left = "auto"; 105 | } 106 | const model = new Model(this.#config); 107 | // 加载右侧小工具 108 | if (this.#config["isTools"] === true) { 109 | if (typeof Iconify !== "undefined") { 110 | tools._registerTools(model, this.#config); 111 | } else { 112 | util.loadExternalResource(this.#path + "lib/iconify/3.0.1/iconify.min.js", "js").then(() => { 113 | tools._registerTools(model, this.#config); 114 | }); 115 | } 116 | } 117 | // 初始化模组 118 | this.#initModel(model); 119 | } 120 | 121 | /** 122 | * 向 Live2d 注册事件 123 | * 124 | * @param result 从 tips 文件中读取的空闲消息数据 125 | */ 126 | #registerEventListener(result) { 127 | // 检测用户活动状态,并在空闲时显示消息 128 | let userAction = false, 129 | userActionTimer, 130 | messageArray = result.message.default; 131 | window.addEventListener("mousemove", () => (userAction = true)); 132 | window.addEventListener("keydown", () => (userAction = true)); 133 | setInterval(() => { 134 | if (userAction) { 135 | userAction = false; 136 | clearInterval(userActionTimer); 137 | userActionTimer = null; 138 | } else if (!userActionTimer) { 139 | userActionTimer = setInterval(() => { 140 | message.showMessage(messageArray, 6000, 2); 141 | }, 20000); 142 | } 143 | }, 1000); 144 | // 首次进入网站触发事件 145 | if (this.#config["firstOpenSite"] === true) { 146 | message.showMessage(message.welcomeMessage(result.time), 7000, 4); 147 | } 148 | window.addEventListener("mouseover", (event) => { 149 | for (let { selector, text } of result.mouseover) { 150 | if (!event.target.matches(selector)) continue; 151 | text = util.randomSelection(text); 152 | text = text.replace("{text}", event.target.innerText); 153 | message.showMessage(text, 4000, 1); 154 | return; 155 | } 156 | }); 157 | window.addEventListener("click", (event) => { 158 | for (let { selector, text } of result.click) { 159 | if (!event.target.matches(selector)) continue; 160 | text = util.randomSelection(text); 161 | text = text.replace("{text}", event.target.innerText); 162 | message.showMessage(text, 4000, 1); 163 | return; 164 | } 165 | }); 166 | result["seasons"].forEach(({ date, text }) => { 167 | const now = new Date(), 168 | after = date.split("-")[0], 169 | before = date.split("-")[1] || after; 170 | if ( 171 | after.split("/")[0] <= now.getMonth() + 1 && 172 | now.getMonth() + 1 <= before.split("/")[0] && 173 | after.split("/")[1] <= now.getDate() && 174 | now.getDate() <= before.split("/")[1] 175 | ) { 176 | text = util.randomSelection(text); 177 | text = text.replace("{year}", now.getFullYear()); 178 | messageArray.push(text); 179 | } 180 | }); 181 | 182 | // 打开控制台事件 183 | if (this.#config["openConsole"] === true) { 184 | let devtools = () => {}; 185 | devtools.toString = () => { 186 | message.showMessage(this.#config["openConsoleTip"] || result["message"]["console"], 6000, 2); 187 | }; 188 | } 189 | // 复制内容触发事件 190 | if (this.#config["copyContent"] === true) { 191 | window.addEventListener("copy", () => { 192 | message.showMessage(this.#config["copyContentTip"] || result["message"]["copy"], 6000, 2); 193 | }); 194 | } 195 | // 离开当前页面事件 196 | if (this.#config["backSite"] === true) { 197 | window.addEventListener("visibilitychange", () => { 198 | if (!document.hidden) { 199 | message.showMessage(this.#config["backSiteTip"] || result["message"]["visibilitychange"], 6000, 2); 200 | } 201 | }); 202 | } 203 | } 204 | 205 | #initModel(model) { 206 | let modelId = localStorage.getItem("modelId"); 207 | let modelTexturesId = localStorage.getItem("modelTexturesId"); 208 | if (modelId === null || !!this.#config["isForceUseDefaultConfig"]) { 209 | // 加载指定模型的指定材质 210 | modelId = this.#config["modelId"] || 1; // 模型 ID 211 | modelTexturesId = this.#config["modelTexturesId"] || 53; // 材质 ID 212 | } 213 | 214 | if (this.#config["consoleShowStatu"]) { 215 | eval( 216 | (function (p, a, c, k, e, r) { 217 | e = function (c) { 218 | return ( 219 | (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36)) 220 | ); 221 | }; 222 | if (!"".replace(/^/, String)) { 223 | while (c--) r[e(c)] = k[c] || e(c); 224 | k = [ 225 | function (e) { 226 | return r[e]; 227 | }, 228 | ]; 229 | e = function () { 230 | return "\\w+"; 231 | }; 232 | c = 1; 233 | } 234 | while (c--) if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]); 235 | return p; 236 | })( 237 | "8.d(\" \");8.d(\"\\U,.\\y\\5.\\1\\1\\1\\1/\\1,\\u\\2 \\H\\n\\1\\1\\1\\1\\1\\b ', !-\\r\\j-i\\1/\\1/\\g\\n\\1\\1\\1 \\1 \\a\\4\\f'\\1\\1\\1 L/\\a\\4\\5\\2\\n\\1\\1 \\1 /\\1 \\a,\\1 /|\\1 ,\\1 ,\\1\\1\\1 ',\\n\\1\\1\\1\\q \\1/ /-\\j/\\1\\h\\E \\9 \\5!\\1 i\\n\\1\\1\\1 \\3 \\6 7\\q\\4\\c\\1 \\3'\\s-\\c\\2!\\t|\\1 |\\n\\1\\1\\1\\1 !,/7 '0'\\1\\1 \\X\\w| \\1 |\\1\\1\\1\\n\\1\\1\\1\\1 |.\\x\\\"\\1\\l\\1\\1 ,,,, / |./ \\1 |\\n\\1\\1\\1\\1 \\3'| i\\z.\\2,,A\\l,.\\B / \\1.i \\1|\\n\\1\\1\\1\\1\\1 \\3'| | / C\\D/\\3'\\5,\\1\\9.\\1|\\n\\1\\1\\1\\1\\1\\1 | |/i \\m|/\\1 i\\1,.\\6 |\\F\\1|\\n\\1\\1\\1\\1\\1\\1.|/ /\\1\\h\\G \\1 \\6!\\1\\1\\b\\1|\\n\\1\\1\\1 \\1 \\1 k\\5>\\2\\9 \\1 o,.\\6\\2 \\1 /\\2!\\n\\1\\1\\1\\1\\1\\1 !'\\m//\\4\\I\\g', \\b \\4'7'\\J'\\n\\1\\1\\1\\1\\1\\1 \\3'\\K|M,p,\\O\\3|\\P\\n\\1\\1\\1\\1\\1 \\1\\1\\1\\c-,/\\1|p./\\n\\1\\1\\1\\1\\1 \\1\\1\\1'\\f'\\1\\1!o,.:\\Q \\R\\S\\T v\"+e.V+\" / W \"+e.N);8.d(\" \");", 238 | 60, 239 | 60, 240 | "|u3000|uff64|uff9a|uff40|u30fd|uff8d||console|uff8a|uff0f|uff3c|uff84|log|this.#config|uff70|u00b4|uff49||u2010||u3000_|u3008||_|___|uff72|u2500|uff67|u30cf|u30fc||u30bd|u4ece|u30d8|uff1e|__|u30a4|k_|uff17_|u3000L_|u3000i|uff1a|u3009|uff34|uff70r|u30fdL__||___i|updateTime|u30f3|u30ce|nLive2D|u770b|u677f|u5a18|u304f__|version|LIlGG|u00b40i".split( 241 | "|" 242 | ), 243 | 0, 244 | {} 245 | ) 246 | ); 247 | } 248 | model.loadModel(modelId, modelTexturesId); 249 | // 加载各个来源的 tips 文件并进行合并 250 | this.#loadTips().then((result) => this.#registerEventListener(result)); 251 | } 252 | 253 | /** 254 | * 从各个位置,获取 Live2d 提示文件,若配置的 tips 文件读取失败,则会回退到默认 tips 文件 255 | * 256 | * @returns {Promise} 257 | */ 258 | #loadTips() { 259 | let config = this.#config; 260 | return new Promise((resolve) => { 261 | Promise.all([util.loadTipsResource(config["themeTipsPath"]), util.loadTipsResource(config["tipsPath"])]).then( 262 | (result) => { 263 | // 后台配置 tips,其中包含 mouseover 及 click 两种配置,以及单独配置的 message 264 | let configTips = util.backendConfigConvert(config); 265 | // 主题设置 tips,其中包含 mouseover 及 click 两种配置(会过滤掉其他配置) 266 | let themeTips = { 267 | click: result[0]["click"] || [], 268 | mouseover: result[0]["mouseover"] || [], 269 | }; 270 | // 配置的 tips 文件,包含所有属性 (click, mouseover, seasons, time, message) 271 | let defaultTips = result[1]; 272 | // 若配置的 tips 文件不存在,则回退到默认 tips 273 | if (Object.keys(defaultTips).length === 0) { 274 | util.loadTipsResource(this.defaultConfig.tipsPath).then((tips) => { 275 | resolve(util.mergeTips(configTips, themeTips, tips)); 276 | }); 277 | } else { 278 | resolve(util.mergeTips(configTips, themeTips, defaultTips)); 279 | } 280 | } 281 | ); 282 | }); 283 | } 284 | } 285 | 286 | class Model { 287 | #apiPath; 288 | #config; 289 | 290 | constructor(config) { 291 | let apiPath = config["apiPath"]; 292 | if (apiPath !== undefined && typeof apiPath === "string" && apiPath.length > 0) { 293 | if (!apiPath.endsWith("/")) apiPath += "/"; 294 | } else { 295 | throw "Invalid initWidget argument!"; 296 | } 297 | this.#apiPath = apiPath; 298 | this.#config = config; 299 | } 300 | 301 | async loadModel(modelId, modelTexturesId, text) { 302 | localStorage.setItem("modelId", modelId); 303 | localStorage.setItem("modelTexturesId", modelTexturesId); 304 | message.showMessage(text, 4000, 3); 305 | loadlive2d( 306 | "live2d", 307 | `${this.#apiPath}get/?id=${modelId}-${modelTexturesId}`, 308 | this.#config["consoleShowStatu"] === true 309 | ? console.log(`[Status] Live2D 模型 ${modelId}-${modelTexturesId} 加载完成`) 310 | : null 311 | ); 312 | } 313 | 314 | async loadRandModel() { 315 | const modelId = Number(localStorage.getItem("modelId")), 316 | modelTexturesId = Number(localStorage.getItem("modelTexturesId")); 317 | // 可选 "rand"(随机), "switch"(顺序) 318 | fetch(`${this.#apiPath}rand_textures/?id=${modelId}-${modelTexturesId}`) 319 | .then((response) => response.json()) 320 | .then((result) => { 321 | if (result["textures"]["id"] === 1 && (modelTexturesId === 1 || modelTexturesId === 0)) { 322 | message.showMessage("我还没有其他衣服呢!", 4000, 3); 323 | } else { 324 | this.loadModel(modelId, result["textures"]["id"], "我的新衣服好看嘛?"); 325 | } 326 | }); 327 | } 328 | 329 | async loadOtherModel() { 330 | let modelId = Number(localStorage.getItem("modelId")); 331 | fetch(`${this.#apiPath}switch/?id=${modelId}`) 332 | .then((response) => response.json()) 333 | .then((result) => { 334 | this.loadModel(result.model.id, 0, result.model.message); 335 | }); 336 | } 337 | } 338 | 339 | /** 340 | * 异步加载对应的资源 341 | * 342 | * @param url 需要加载的资源链接 343 | * @param type 需要加载的资源类型,如 css/js 344 | * 345 | * @returns {Promise} Promise 346 | */ 347 | util.loadExternalResource = function (url, type) { 348 | return new Promise((resolve, reject) => { 349 | let tag; 350 | if (type === "css") { 351 | tag = document.createElement("link"); 352 | tag.rel = "stylesheet"; 353 | tag.href = url; 354 | } else if (type === "js") { 355 | tag = document.createElement("script"); 356 | tag.src = url; 357 | } 358 | if (tag) { 359 | tag.onload = () => resolve(url); 360 | tag.onerror = () => reject(url); 361 | document.head.appendChild(tag); 362 | } 363 | }); 364 | }; 365 | 366 | /** 367 | * 从数组中获取任意一个数据。当给定数据不是数组时,将返回原数据。 368 | * 369 | * @param obj 需要获取随机数的数组 370 | * @returns {Object} 数组内的任意一个数据或原数据 371 | */ 372 | util.randomSelection = function (obj) { 373 | return Array.isArray(obj) ? obj[Math.floor(Math.random() * obj.length)] : obj; 374 | }; 375 | 376 | /** 377 | * 读取 tips 资源 378 | * 379 | * @param url 资源链接 380 | * @returns {Promise} 381 | */ 382 | util.loadTipsResource = function (url) { 383 | let defaultObj = {}; 384 | return new Promise((resolve) => { 385 | if (!url) { 386 | resolve(defaultObj); 387 | } 388 | fetch(url) 389 | .then((response) => response.json()) 390 | .then((result) => { 391 | resolve(result); 392 | }) 393 | .catch(() => { 394 | resolve(defaultObj); 395 | }); 396 | }); 397 | }; 398 | 399 | /** 400 | * 将后台主题中的配置转为适合 TIPS 的格式 401 | * 402 | * @param config 配置文件 403 | */ 404 | util.backendConfigConvert = function (config = {}) { 405 | let tips = { 406 | click: [], 407 | mouseover: [], 408 | message: {}, 409 | }; 410 | // selector 411 | if (!!config["selectorTips"]) { 412 | config["selectorTips"].forEach((item) => { 413 | let texts = item["messageTexts"].map((text) => text.message); 414 | let obj = { 415 | selector: item["selector"], 416 | text: texts, 417 | }; 418 | if (item["mouseAction"] === "click") { 419 | tips.click.push(obj); 420 | } else { 421 | tips.mouseover.push(obj); 422 | } 423 | }); 424 | } 425 | // message 426 | tips.message.visibilitychange = config["backSiteTip"]; 427 | tips.message.copy = config["copyContentTip"]; 428 | tips.message.console = config["openConsoleTip"]; 429 | return tips; 430 | }; 431 | 432 | /** 433 | * 合并各个渠道的 tips,根据获取位置不同,合并时优先级也不同。优先级按高到低的顺序为 434 | * 435 | *
    436 | *
      后台插件配置文件中获得的 tips。(该配置文件只支持 mouseover 与 click 两种类型的 tips 属性,另外包括单独配置的 message)
    437 | *
      主题文件中设置的 tips (该配置文件只支持 mouseover 与 click 两种类型的 tips 属性)
    438 | *
      配置/默认的 tips 文件(该配置文件支持所有的 tips 属性,但其属性会被优先级高的覆盖)
    439 | *
440 | * 441 | * 请注意,此项返回值为修改后的 defaultTips,任何修改 defaultTips 的情况都将导致返回值同步修改。 442 | * 443 | * @param configTips 后台配置文件中设置的 tips 444 | * @param themeTips 主题提供的 tips 445 | * @param defaultTips 配置/默认的 tips 446 | */ 447 | util.mergeTips = function (configTips, themeTips, defaultTips) { 448 | let duplicateClick = [...configTips["click"], ...themeTips["click"], ...defaultTips["click"]]; 449 | let duplicateMouseover = [...configTips["mouseover"], ...themeTips["mouseover"], ...defaultTips["mouseover"]]; 450 | defaultTips.click = util.distinctArray(duplicateClick, "selector"); 451 | defaultTips.mouseover = util.distinctArray(duplicateMouseover, "selector"); 452 | defaultTips.message = { ...defaultTips.message, ...configTips.message }; 453 | return defaultTips; 454 | }; 455 | 456 | /** 457 | * 去重对象数组 458 | * @param dupArray 需要去重的数组 459 | * @param key 对象数组 key 460 | */ 461 | util.distinctArray = function (dupArray, key) { 462 | let obj = {}; 463 | return dupArray.reduce((curr, next) => { 464 | if (!obj[next[key]]) { 465 | obj[next[key]] = true; 466 | curr.push(next); 467 | } 468 | return curr; 469 | }, []); 470 | }; 471 | 472 | message.messageTimer = null; 473 | 474 | /** 475 | * 显示消息至消息栏 476 | * 477 | * @param text 需要显示的消息 478 | * @param timeout 消息展示时间(最大) 479 | * @param priority 消息优先级,数字越大,优先级越大 480 | */ 481 | message.showMessage = function (text, timeout, priority) { 482 | let live2dPriority = sessionStorage.getItem("live2d-priority"); 483 | if (!text || (live2dPriority && live2dPriority > priority)) return; 484 | if (this.messageTimer) { 485 | clearTimeout(this.messageTimer); 486 | this.messageTimer = null; 487 | } 488 | text = util.randomSelection(text); 489 | sessionStorage.setItem("live2d-priority", priority); 490 | const tips = document.getElementById("live2d-tips"); 491 | tips.innerHTML = text; 492 | tips.classList.add("live2d-tips-active"); 493 | this.messageTimer = setTimeout(() => { 494 | sessionStorage.removeItem("live2d-priority"); 495 | tips.classList.remove("live2d-tips-active"); 496 | }, timeout); 497 | }; 498 | 499 | /** 500 | * 创建一个流式效果的消息框。 501 | * 此消息框的优先级将大于所有其他消息框优先级,且不会被其他消息覆盖。 502 | * 503 | * @param timeout 等待消息流的最大时间,超过此时间将自动关闭流消息框 504 | * @param showTimeout 消息全部接受完之后,展示时长 505 | */ 506 | message.createStreamMessage = function (timeout, showTimeout) { 507 | const priority = 99999; 508 | 509 | const updateTimer = function (time) { 510 | if (this.messageTimer) { 511 | clearTimeout(this.messageTimer); 512 | this.messageTimer = null; 513 | } 514 | this.messageTimer = setTimeout(() => { 515 | sessionStorage.removeItem("live2d-priority"); 516 | tips.classList.remove("live2d-tips-active"); 517 | }, time); 518 | }; 519 | 520 | sessionStorage.setItem("live2d-priority", priority); 521 | const tips = document.getElementById("live2d-tips"); 522 | tips.innerHTML = ""; 523 | tips.classList.add("live2d-tips-active"); 524 | updateTimer(timeout); 525 | 526 | const sendMessage = function (text) { 527 | tips.innerHTML += text; 528 | }; 529 | 530 | const stop = function () { 531 | updateTimer(showTimeout); 532 | }; 533 | 534 | return { 535 | sendMessage, 536 | stop, 537 | }; 538 | }; 539 | 540 | /** 541 | * 显示一言 542 | * 543 | * @param api 需要获取的一言接口 544 | * @param callback 获取到的一言返回结果特殊化处理(用于不同接口的差异性)。 545 | * 其处理返回值为数组或字符串,数组第一位为一言(不可为空),数组第二位为作者,网站等信息(可为空) 546 | */ 547 | message.showHitokoto = function (api, callback) { 548 | fetch(api) 549 | .then((response) => response.json()) 550 | .then((result) => { 551 | let text = callback(result); 552 | if (typeof text === "string") { 553 | this.showMessage(text, 6000, 2); 554 | } else { 555 | this.showMessage(text[0], 6000, 2); 556 | if (text[1] !== undefined) { 557 | setTimeout(() => { 558 | this.showMessage(text[1], 4000, 2); 559 | }, 6000); 560 | } 561 | } 562 | }); 563 | }; 564 | 565 | /** 566 | * 首次进入,显示欢迎消息 567 | * 568 | * @param time 时间 569 | * @returns {string|*} 570 | */ 571 | message.welcomeMessage = function (time) { 572 | // referrer 内获取的网页 573 | const domains = { 574 | baidu: "百度", 575 | so: "360搜索", 576 | google: "谷歌搜索", 577 | }; 578 | // 如果是主页 579 | if (location.pathname === "/") { 580 | for (let { hour, text } of time) { 581 | const now = new Date(), 582 | after = hour.split("-")[0], 583 | before = hour.split("-")[1] || after; 584 | if (after <= now.getHours() && now.getHours() <= before) { 585 | return text; 586 | } 587 | } 588 | } 589 | const text = `欢迎阅读「${document.title.split(" - ")[0]}」`; 590 | let from; 591 | if (document.referrer !== "") { 592 | const referrer = new URL(document.referrer), 593 | domain = referrer.hostname.split(".")[1]; 594 | if (location.hostname === referrer.hostname) return text; 595 | if (domain in domains) { 596 | from = domains[domain]; 597 | } else { 598 | from = referrer.hostname; 599 | } 600 | return `Hello!来自 ${from} 的朋友
${text}`; 601 | } 602 | return text; 603 | }; 604 | 605 | /** 606 | * openai 右侧小工具 607 | * 608 | * @param config 609 | * @returns {{icon: string, callback: (function(): void)}} 610 | */ 611 | tools.openai = function (config = {}) { 612 | return { 613 | icon: config["openaiIcon"] || "ph-chats-circle-fill", 614 | callback: () => { 615 | openai.chatWindows(config); 616 | }, 617 | }; 618 | }; 619 | 620 | /** 621 | * Live2d 右侧一言小工具 622 | * 623 | * @param config 624 | * @returns {{icon: string, callback: (function(): void)}} 625 | */ 626 | tools.hitokoto = function (config = {}) { 627 | let api = config["hitokotoApi"] || "https://v1.hitokoto.cn"; 628 | let callback; 629 | switch (api) { 630 | case "https://v1.hitokoto.cn": 631 | callback = () => 632 | message.showHitokoto(api, (result) => { 633 | return [ 634 | result["hitokoto"], 635 | `这句一言来自 「${result["from"]}」,是 ${result["creator"]} 在 hitokoto.cn 投稿的。`, 636 | ]; 637 | }); 638 | break; 639 | default: 640 | callback = () => 641 | message.showHitokoto(api, (result) => { 642 | if (result["hitokoto"] !== undefined) { 643 | return [result["hitokoto"]]; 644 | } 645 | return `一言接口格式不正确,请确保一言返回字段为 hitokoto,例如 {"hitokoto": "一言"}。特殊接口请联系插件作者增加适配`; 646 | }); 647 | break; 648 | } 649 | return { 650 | icon: config["hitokotoIcon"] || "ph-chat-circle-fill", 651 | callback: callback, 652 | }; 653 | }; 654 | 655 | /** 656 | * 小飞船游戏 657 | * 658 | * @param config 659 | * @returns {{icon: string, callback: callback}} 660 | */ 661 | tools.asteroids = function (config = {}) { 662 | return { 663 | icon: config["asteroidsIcon"] || "ph-paper-plane-tilt-fill", 664 | callback: () => { 665 | if (window.Asteroids) { 666 | if (!window.ASTEROIDSPLAYERS) window.ASTEROIDSPLAYERS = []; 667 | window.ASTEROIDSPLAYERS.push(new Asteroids()); 668 | } else { 669 | util.loadExternalResource(live2d.path + "lib/asteroids/asteroids.min.js", "js").finally(); 670 | } 671 | }, 672 | }; 673 | }; 674 | 675 | /** 676 | * 切换模组 677 | * 678 | * @param config 679 | * @returns {{icon: string, callback: callback}} 680 | */ 681 | tools["switch-model"] = function (config = {}) { 682 | return { 683 | icon: config["switch-model-icon"] || "ph-arrows-counter-clockwise-fill", 684 | callback: () => {}, 685 | }; 686 | }; 687 | 688 | /** 689 | * 切换纹理/衣服 690 | * 691 | * @param config 692 | * @returns {{icon: string, callback: callback}} 693 | */ 694 | tools["switch-texture"] = function (config = {}) { 695 | return { 696 | icon: config["switch-texture-icon"] || "ph-dress-fill", 697 | callback: () => {}, 698 | }; 699 | }; 700 | 701 | /** 702 | * 截图功能 703 | * 704 | * @param config 705 | * @returns {{icon: string, callback: callback}} 706 | */ 707 | tools.photo = function (config = {}) { 708 | let photoName = config["photoName"] || "live2d"; 709 | return { 710 | icon: config["photoIcon"] || "ph-camera-fill", 711 | callback: () => { 712 | message.showMessage("照好了嘛,是不是很可爱呢?", 6000, 2); 713 | Live2D.captureName = photoName + ".png"; 714 | Live2D.captureFrame = true; 715 | }, 716 | }; 717 | }; 718 | 719 | /** 720 | * 前往目标站点 721 | * 722 | * @param config 723 | * @returns {{icon: string, callback: callback}} 724 | */ 725 | tools.info = function (config = {}) { 726 | let siteUrl = "https://github.com/LIlGG/plugin-live2d"; 727 | return { 728 | icon: config["infoIcon"] || "ph-info-fill", 729 | callback: () => { 730 | open(siteUrl); 731 | }, 732 | }; 733 | }; 734 | 735 | /** 736 | * 退出 live2d 737 | * 738 | * @param config 739 | * @returns {{icon: string, callback: callback}} 740 | */ 741 | tools.quit = function (config = {}) { 742 | return { 743 | icon: config["quitIcon"] || "ph-x-bold", 744 | callback: () => { 745 | localStorage.setItem("live2d-display", Date.now()); 746 | message.showMessage("愿你有一天能与重要的人重逢。", 2000, 4); 747 | document.getElementById("live2d-plugin").style.bottom = "-500px"; 748 | setTimeout(() => { 749 | document.getElementById("live2d-plugin").style.display = "none"; 750 | document.getElementById("live2d-toggle").classList.add("live2d-toggle-active"); 751 | }, 3000); 752 | }, 753 | }; 754 | }; 755 | 756 | /** 757 | * 注册工具 758 | * 759 | * @param model 需要添加工具的模组 {@link Model} 760 | * @param config 配置文件 761 | * @private 私有方法 762 | */ 763 | tools._registerTools = function (model, config) { 764 | if (!Array.isArray(config.tools)) { 765 | config.tools = Object.keys(tools); 766 | } 767 | if (config.isAiChat) { 768 | config.tools.unshift("openai"); 769 | } 770 | // TODO 小工具样式 771 | for (let tool of config.tools) { 772 | if (tools[tool]) { 773 | let { icon, callback } = tools[tool](config); 774 | switch (tool) { 775 | case "switch-model": 776 | callback = () => model.loadOtherModel(); 777 | break; 778 | case "switch-texture": 779 | callback = () => model.loadRandModel(); 780 | break; 781 | } 782 | document 783 | .getElementById("live2d-tool") 784 | .insertAdjacentHTML( 785 | "beforeend", 786 | `` 787 | ); 788 | document.getElementById(`live2d-tool-${tool}`).addEventListener("click", callback); 789 | } 790 | } 791 | }; 792 | 793 | openai.messageTimer = null; 794 | 795 | /** 796 | * 使用 openai 发送流式聊天消息 797 | * 798 | * @param {*} msg 799 | */ 800 | openai.sendMessage = async function (msg, config = {}) { 801 | openai.loading = true; 802 | if (this.messageTimer) { 803 | clearTimeout(this.messageTimer); 804 | this.messageTimer = null; 805 | } 806 | this.messageTimer = setTimeout(() => { 807 | message.showMessage("正在接收来自母星的消息,请耐心等待~", 2000, 2); 808 | }, 5000); 809 | 810 | document.getElementById("loadingIcon").style.display = "block"; 811 | document.getElementById("send").style.display = "none"; 812 | 813 | let historyMessages = JSON.parse(localStorage.getItem("historyMessages")) || []; 814 | let userMessage = { 815 | role: "user", 816 | content: msg, 817 | }; 818 | historyMessages.push(userMessage); 819 | 820 | const controller = new AbortController(); 821 | const requestTimeoutId = setTimeout(() => { 822 | abort(); 823 | }, Number(config["chunkTimeout"] || 60) * 1000); 824 | 825 | const abort = () => { 826 | controller.abort(); 827 | clearTimeout(this.messageTimer); 828 | clearTimeout(requestTimeoutId); 829 | openai.loading = false; 830 | document.getElementById("loadingIcon").style.display = "none"; 831 | document.getElementById("send").style.display = "block"; 832 | }; 833 | const response = await fetch("/apis/api.live2d.halo.run/v1alpha1/live2d/ai/chat-process", { 834 | method: "POST", 835 | cache: "no-cache", 836 | keepalive: true, 837 | headers: { 838 | "Content-Type": "application/json", 839 | Accept: "text/event-stream", 840 | }, 841 | body: JSON.stringify({ 842 | message: historyMessages, 843 | }), 844 | signal: controller.signal, 845 | }); 846 | 847 | if (!response.ok) { 848 | if (response.status === 401) { 849 | message.showMessage("请先登录!", 2000, 4); 850 | } else { 851 | message.showMessage("对话接口异常了哦~快去联系我的主人吧!", 5000, 4); 852 | } 853 | console.log("get.message.error", response); 854 | abort(); 855 | return; 856 | } 857 | 858 | let chatMessage = { 859 | content: "", 860 | }; 861 | 862 | clearTimeout(this.messageTimer); 863 | clearTimeout(requestTimeoutId); 864 | const reader = response.body.getReader(); 865 | const textDecoder = new TextDecoder(); 866 | const chat = message.createStreamMessage( 867 | Number(config["chunkTimeout"] || 60) * 1000, 868 | Number(config["showChatMessageTimeout"] || 10) * 1000 869 | ); 870 | 871 | document.getElementById("send").style.display = "block"; 872 | document.getElementById("loadingIcon").style.display = "none"; 873 | openai.loading = false; 874 | while (true) { 875 | const { value, done } = await reader.read(); 876 | if (done) { 877 | break; 878 | } 879 | let text = textDecoder.decode(value); 880 | const textArrays = text.split("\n\n"); 881 | textArrays.forEach((decoder) => { 882 | if (!decoder) return; 883 | if (decoder.startsWith("data:")) { 884 | let dataIndex = decoder.indexOf("data:"); 885 | if (dataIndex !== -1) { 886 | decoder = decoder.substring(dataIndex + 5); 887 | } 888 | } 889 | const chatResult = JSON.parse(decoder); 890 | const { text, status } = chatResult; 891 | try { 892 | if (status === 200) { 893 | chatMessage.role = "assistant"; 894 | if (text === "[DONE]") { 895 | historyMessages.push(chatMessage); 896 | localStorage.setItem("historyMessages", JSON.stringify(historyMessages)); 897 | chat.stop(); 898 | } else { 899 | chatMessage.content += text; 900 | chat.sendMessage(text); 901 | } 902 | } else { 903 | throw new Error(text); 904 | } 905 | } catch (e) { 906 | console.error("[Request] parse error", text); 907 | chat.sendMessage(`聊天接口出现异常了:${text}`); 908 | } 909 | }); 910 | } 911 | }; 912 | 913 | openai.loading = false; 914 | 915 | /** 916 | * 创建 chat 聊天窗口 917 | * 918 | * @param {*} config 919 | */ 920 | openai.chatWindows = function (config = {}) { 921 | let model = document.getElementById("live2d-chat-model"); 922 | if (model) { 923 | if (model.classList.contains("live2d-chat-model-active")) { 924 | model.classList.remove("live2d-chat-model-active"); 925 | } else { 926 | let input = document.getElementById("live2d-chat-input"); 927 | model.classList.add("live2d-chat-model-active"); 928 | input.focus(); 929 | } 930 | return; 931 | } 932 | document.body.insertAdjacentHTML( 933 | "beforeend", 934 | `
935 |
936 |
937 | 938 |
939 | 940 | 941 | 942 | 943 |
944 |
` 945 | ); 946 | 947 | model = document.getElementById("live2d-chat-model"); 948 | let send = document.getElementById("live2d-chat-send"); 949 | let input = document.getElementById("live2d-chat-input"); 950 | 951 | const sendFun = function () { 952 | let message = input.value; 953 | if (message.length > 0 && !openai.loading) { 954 | input.value = ""; 955 | send.classList.remove("active"); 956 | send.setAttribute("disabled", "disabled"); 957 | openai.sendMessage(message, config); 958 | } 959 | }; 960 | 961 | input.addEventListener("input", (e) => { 962 | let message = input.value; 963 | if (message.length > 0 && !openai.loading) { 964 | send.classList.add("active"); 965 | send.removeAttribute("disabled"); 966 | } else { 967 | send.classList.remove("active"); 968 | send.setAttribute("disabled", "disabled"); 969 | } 970 | }); 971 | 972 | send.addEventListener("click", () => { 973 | sendFun(); 974 | }); 975 | 976 | input.addEventListener("keydown", (e) => { 977 | var keyNum = window.event ? e.keyCode : e.which; 978 | if (keyNum == 13) { 979 | sendFun(); 980 | } 981 | if (keyNum == 27) { 982 | model.classList.remove("live2d-chat-model-active"); 983 | } 984 | }); 985 | 986 | input.addEventListener("focus", () => { 987 | message.showMessage("按下回车键可以快速发送消息哦", 2000, 1); 988 | }); 989 | 990 | model.classList.add("live2d-chat-model-active"); 991 | }; 992 | 993 | window.onload = function () { 994 | localStorage.removeItem("historyMessages"); 995 | }; 996 | 997 | return new Live2d(); 998 | } 999 | -------------------------------------------------------------------------------- /src/main/resources/static/js/live2d-autoload.min.js: -------------------------------------------------------------------------------- 1 | let live2d=new Live2d();function Live2d(){const util={};const message={};const tools={};const openai={};class Live2d{#path;#config;defaultConfig={apiPath:"//api.zsq.im/live2d/",tools:["hitokoto","asteroids","switch-model","switch-texture","photo","info","quit"],updateTime:"2022.12.09",version:"1.0.1",tipsPath:"live2d-tips.json",};init(path,config={}){if(screen.width>=768){Promise.all([util.loadExternalResource(path+"css/live2d.css","css"),util.loadExternalResource(path+"lib/live2d/live2d.min.js","js"),]).then(()=>{this.#path=path;this.defaultConfig.tipsPath=path+"live2d-tips.json";this.#config={...this.defaultConfig,...config};this.#doInit();});}} 2 | get path(){return this.#path;}#doInit(){document.body.insertAdjacentHTML("beforeend",`
看板娘
`);const toggle=document.getElementById("live2d-toggle");toggle.addEventListener("click",()=>{toggle.classList.remove("live2d-toggle-active");if(toggle.getAttribute("first-time")){this.#loadWidget();toggle.removeAttribute("first-time");}else{localStorage.removeItem("live2d-display");document.getElementById("live2d-plugin").style.display="";setTimeout(()=>{document.getElementById("live2d-plugin").style.bottom=0;},0);}});if(localStorage.getItem("live2d-display")&&Date.now()-localStorage.getItem("live2d-display")<=86400000){toggle.setAttribute("first-time",true);setTimeout(()=>{toggle.classList.add("live2d-toggle-active");},0);}else{this.#loadWidget();}}#loadWidget(){localStorage.removeItem("live2d-display");sessionStorage.removeItem("live2d-text");document.body.insertAdjacentHTML("beforeend",`
3 |
4 | 5 |
6 |
`);let live2dDom=document.getElementById("live2d-plugin");setTimeout(()=>{live2dDom.style.bottom=0;},0);if(this.#config["live2dLocation"]==="right"){live2dDom.style.right="50px";live2dDom.style.left="auto";} 7 | const model=new Model(this.#config);if(this.#config["isTools"]===true){if(typeof Iconify!=="undefined"){tools._registerTools(model,this.#config);}else{util.loadExternalResource(this.#path+"lib/iconify/3.0.1/iconify.min.js","js").then(()=>{tools._registerTools(model,this.#config);});}} 8 | this.#initModel(model);}#registerEventListener(result){let userAction=false,userActionTimer,messageArray=result.message.default;window.addEventListener("mousemove",()=>(userAction=true));window.addEventListener("keydown",()=>(userAction=true));setInterval(()=>{if(userAction){userAction=false;clearInterval(userActionTimer);userActionTimer=null;}else if(!userActionTimer){userActionTimer=setInterval(()=>{message.showMessage(messageArray,6000,2);},20000);}},1000);if(this.#config["firstOpenSite"]===true){message.showMessage(message.welcomeMessage(result.time),7000,4);} 9 | window.addEventListener("mouseover",(event)=>{for(let{selector,text}of result.mouseover){if(!event.target.matches(selector))continue;text=util.randomSelection(text);text=text.replace("{text}",event.target.innerText);message.showMessage(text,4000,1);return;}});window.addEventListener("click",(event)=>{for(let{selector,text}of result.click){if(!event.target.matches(selector))continue;text=util.randomSelection(text);text=text.replace("{text}",event.target.innerText);message.showMessage(text,4000,1);return;}});result["seasons"].forEach(({date,text})=>{const now=new Date(),after=date.split("-")[0],before=date.split("-")[1]||after;if(after.split("/")[0]<=now.getMonth()+1&&now.getMonth()+1<=before.split("/")[0]&&after.split("/")[1]<=now.getDate()&&now.getDate()<=before.split("/")[1]){text=util.randomSelection(text);text=text.replace("{year}",now.getFullYear());messageArray.push(text);}});if(this.#config["openConsole"]===true){let devtools=()=>{};devtools.toString=()=>{message.showMessage(this.#config["openConsoleTip"]||result["message"]["console"],6000,2);};} 10 | if(this.#config["copyContent"]===true){window.addEventListener("copy",()=>{message.showMessage(this.#config["copyContentTip"]||result["message"]["copy"],6000,2);});} 11 | if(this.#config["backSite"]===true){window.addEventListener("visibilitychange",()=>{if(!document.hidden){message.showMessage(this.#config["backSiteTip"]||result["message"]["visibilitychange"],6000,2);}});}}#initModel(model){let modelId=localStorage.getItem("modelId");let modelTexturesId=localStorage.getItem("modelTexturesId");if(modelId===null||!!this.#config["isForceUseDefaultConfig"]){modelId=this.#config["modelId"]||1;modelTexturesId=this.#config["modelTexturesId"]||53;} 12 | if(this.#config["consoleShowStatu"]){eval((function(p,a,c,k,e,r){e=function(c){return((c35?String.fromCharCode(c+29):c.toString(36)));};if(!"".replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e];},];e=function(){return"\\w+";};c=1;} 13 | while(c--)if(k[c])p=p.replace(new RegExp("\\b"+e(c)+"\\b","g"),k[c]);return p;})("8.d(\" \");8.d(\"\\U,.\\y\\5.\\1\\1\\1\\1/\\1,\\u\\2 \\H\\n\\1\\1\\1\\1\\1\\b ', !-\\r\\j-i\\1/\\1/\\g\\n\\1\\1\\1 \\1 \\a\\4\\f'\\1\\1\\1 L/\\a\\4\\5\\2\\n\\1\\1 \\1 /\\1 \\a,\\1 /|\\1 ,\\1 ,\\1\\1\\1 ',\\n\\1\\1\\1\\q \\1/ /-\\j/\\1\\h\\E \\9 \\5!\\1 i\\n\\1\\1\\1 \\3 \\6 7\\q\\4\\c\\1 \\3'\\s-\\c\\2!\\t|\\1 |\\n\\1\\1\\1\\1 !,/7 '0'\\1\\1 \\X\\w| \\1 |\\1\\1\\1\\n\\1\\1\\1\\1 |.\\x\\\"\\1\\l\\1\\1 ,,,, / |./ \\1 |\\n\\1\\1\\1\\1 \\3'| i\\z.\\2,,A\\l,.\\B / \\1.i \\1|\\n\\1\\1\\1\\1\\1 \\3'| | / C\\D/\\3'\\5,\\1\\9.\\1|\\n\\1\\1\\1\\1\\1\\1 | |/i \\m|/\\1 i\\1,.\\6 |\\F\\1|\\n\\1\\1\\1\\1\\1\\1.|/ /\\1\\h\\G \\1 \\6!\\1\\1\\b\\1|\\n\\1\\1\\1 \\1 \\1 k\\5>\\2\\9 \\1 o,.\\6\\2 \\1 /\\2!\\n\\1\\1\\1\\1\\1\\1 !'\\m//\\4\\I\\g', \\b \\4'7'\\J'\\n\\1\\1\\1\\1\\1\\1 \\3'\\K|M,p,\\O\\3|\\P\\n\\1\\1\\1\\1\\1 \\1\\1\\1\\c-,/\\1|p./\\n\\1\\1\\1\\1\\1 \\1\\1\\1'\\f'\\1\\1!o,.:\\Q \\R\\S\\T v\"+e.V+\" / W \"+e.N);8.d(\" \");",60,60,"|u3000|uff64|uff9a|uff40|u30fd|uff8d||console|uff8a|uff0f|uff3c|uff84|log|this.#config|uff70|u00b4|uff49||u2010||u3000_|u3008||_|___|uff72|u2500|uff67|u30cf|u30fc||u30bd|u4ece|u30d8|uff1e|__|u30a4|k_|uff17_|u3000L_|u3000i|uff1a|u3009|uff34|uff70r|u30fdL__||___i|updateTime|u30f3|u30ce|nLive2D|u770b|u677f|u5a18|u304f__|version|LIlGG|u00b40i".split("|"),0,{}));} 14 | model.loadModel(modelId,modelTexturesId);this.#loadTips().then((result)=>this.#registerEventListener(result));}#loadTips(){let config=this.#config;return new Promise((resolve)=>{Promise.all([util.loadTipsResource(config["themeTipsPath"]),util.loadTipsResource(config["tipsPath"])]).then((result)=>{let configTips=util.backendConfigConvert(config);let themeTips={click:result[0]["click"]||[],mouseover:result[0]["mouseover"]||[],};let defaultTips=result[1];if(Object.keys(defaultTips).length===0){util.loadTipsResource(this.defaultConfig.tipsPath).then((tips)=>{resolve(util.mergeTips(configTips,themeTips,tips));});}else{resolve(util.mergeTips(configTips,themeTips,defaultTips));}});});}} 15 | class Model{#apiPath;#config;constructor(config){let apiPath=config["apiPath"];if(apiPath!==undefined&&typeof apiPath==="string"&&apiPath.length>0){if(!apiPath.endsWith("/"))apiPath+="/";}else{throw"Invalid initWidget argument!";} 16 | this.#apiPath=apiPath;this.#config=config;} 17 | async loadModel(modelId,modelTexturesId,text){localStorage.setItem("modelId",modelId);localStorage.setItem("modelTexturesId",modelTexturesId);message.showMessage(text,4000,3);loadlive2d("live2d",`${this.#apiPath}get/?id=${modelId}-${modelTexturesId}`,this.#config["consoleShowStatu"]===true?console.log(`[Status] Live2D 模型 ${modelId}-${modelTexturesId} 加载完成`):null);} 18 | async loadRandModel(){const modelId=Number(localStorage.getItem("modelId")),modelTexturesId=Number(localStorage.getItem("modelTexturesId"));fetch(`${this.#apiPath}rand_textures/?id=${modelId}-${modelTexturesId}`).then((response)=>response.json()).then((result)=>{if(result["textures"]["id"]===1&&(modelTexturesId===1||modelTexturesId===0)){message.showMessage("我还没有其他衣服呢!",4000,3);}else{this.loadModel(modelId,result["textures"]["id"],"我的新衣服好看嘛?");}});} 19 | async loadOtherModel(){let modelId=Number(localStorage.getItem("modelId"));fetch(`${this.#apiPath}switch/?id=${modelId}`).then((response)=>response.json()).then((result)=>{this.loadModel(result.model.id,0,result.model.message);});}} 20 | util.loadExternalResource=function(url,type){return new Promise((resolve,reject)=>{let tag;if(type==="css"){tag=document.createElement("link");tag.rel="stylesheet";tag.href=url;}else if(type==="js"){tag=document.createElement("script");tag.src=url;} 21 | if(tag){tag.onload=()=>resolve(url);tag.onerror=()=>reject(url);document.head.appendChild(tag);}});};util.randomSelection=function(obj){return Array.isArray(obj)?obj[Math.floor(Math.random()*obj.length)]:obj;};util.loadTipsResource=function(url){let defaultObj={};return new Promise((resolve)=>{if(!url){resolve(defaultObj);} 22 | fetch(url).then((response)=>response.json()).then((result)=>{resolve(result);}).catch(()=>{resolve(defaultObj);});});};util.backendConfigConvert=function(config={}){let tips={click:[],mouseover:[],message:{},};if(!!config["selectorTips"]){config["selectorTips"].forEach((item)=>{let texts=item["messageTexts"].map((text)=>text.message);let obj={selector:item["selector"],text:texts,};if(item["mouseAction"]==="click"){tips.click.push(obj);}else{tips.mouseover.push(obj);}});} 23 | tips.message.visibilitychange=config["backSiteTip"];tips.message.copy=config["copyContentTip"];tips.message.console=config["openConsoleTip"];return tips;};util.mergeTips=function(configTips,themeTips,defaultTips){let duplicateClick=[...configTips["click"],...themeTips["click"],...defaultTips["click"]];let duplicateMouseover=[...configTips["mouseover"],...themeTips["mouseover"],...defaultTips["mouseover"]];defaultTips.click=util.distinctArray(duplicateClick,"selector");defaultTips.mouseover=util.distinctArray(duplicateMouseover,"selector");defaultTips.message={...defaultTips.message,...configTips.message};return defaultTips;};util.distinctArray=function(dupArray,key){let obj={};return dupArray.reduce((curr,next)=>{if(!obj[next[key]]){obj[next[key]]=true;curr.push(next);} 24 | return curr;},[]);};message.messageTimer=null;message.showMessage=function(text,timeout,priority){let live2dPriority=sessionStorage.getItem("live2d-priority");if(!text||(live2dPriority&&live2dPriority>priority))return;if(this.messageTimer){clearTimeout(this.messageTimer);this.messageTimer=null;} 25 | text=util.randomSelection(text);sessionStorage.setItem("live2d-priority",priority);const tips=document.getElementById("live2d-tips");tips.innerHTML=text;tips.classList.add("live2d-tips-active");this.messageTimer=setTimeout(()=>{sessionStorage.removeItem("live2d-priority");tips.classList.remove("live2d-tips-active");},timeout);};message.createStreamMessage=function(timeout,showTimeout){const priority=99999;const updateTimer=function(time){if(this.messageTimer){clearTimeout(this.messageTimer);this.messageTimer=null;} 26 | this.messageTimer=setTimeout(()=>{sessionStorage.removeItem("live2d-priority");tips.classList.remove("live2d-tips-active");},time);};sessionStorage.setItem("live2d-priority",priority);const tips=document.getElementById("live2d-tips");tips.innerHTML="";tips.classList.add("live2d-tips-active");updateTimer(timeout);const sendMessage=function(text){tips.innerHTML+=text;};const stop=function(){updateTimer(showTimeout);};return{sendMessage,stop,};};message.showHitokoto=function(api,callback){fetch(api).then((response)=>response.json()).then((result)=>{let text=callback(result);if(typeof text==="string"){this.showMessage(text,6000,2);}else{this.showMessage(text[0],6000,2);if(text[1]!==undefined){setTimeout(()=>{this.showMessage(text[1],4000,2);},6000);}}});};message.welcomeMessage=function(time){const domains={baidu:"百度",so:"360搜索",google:"谷歌搜索",};if(location.pathname==="/"){for(let{hour,text}of time){const now=new Date(),after=hour.split("-")[0],before=hour.split("-")[1]||after;if(after<=now.getHours()&&now.getHours()<=before){return text;}}} 27 | const text=`欢迎阅读「${document.title.split(" - ")[0]}」`;let from;if(document.referrer!==""){const referrer=new URL(document.referrer),domain=referrer.hostname.split(".")[1];if(location.hostname===referrer.hostname)return text;if(domain in domains){from=domains[domain];}else{from=referrer.hostname;} 28 | return`Hello!来自 ${from} 的朋友
${text}`;} 29 | return text;};tools.openai=function(config={}){return{icon:config["openaiIcon"]||"ph-chats-circle-fill",callback:()=>{openai.chatWindows(config);},};};tools.hitokoto=function(config={}){let api=config["hitokotoApi"]||"https://v1.hitokoto.cn";let callback;switch(api){case"https://v1.hitokoto.cn":callback=()=>message.showHitokoto(api,(result)=>{return[result["hitokoto"],`这句一言来自 「${result["from"]}」,是 ${result["creator"]} 在 hitokoto.cn 投稿的。`,];});break;default:callback=()=>message.showHitokoto(api,(result)=>{if(result["hitokoto"]!==undefined){return[result["hitokoto"]];} 30 | return`一言接口格式不正确,请确保一言返回字段为 hitokoto,例如 {"hitokoto": "一言"}。特殊接口请联系插件作者增加适配`;});break;} 31 | return{icon:config["hitokotoIcon"]||"ph-chat-circle-fill",callback:callback,};};tools.asteroids=function(config={}){return{icon:config["asteroidsIcon"]||"ph-paper-plane-tilt-fill",callback:()=>{if(window.Asteroids){if(!window.ASTEROIDSPLAYERS)window.ASTEROIDSPLAYERS=[];window.ASTEROIDSPLAYERS.push(new Asteroids());}else{util.loadExternalResource(live2d.path+"lib/asteroids/asteroids.min.js","js").finally();}},};};tools["switch-model"]=function(config={}){return{icon:config["switch-model-icon"]||"ph-arrows-counter-clockwise-fill",callback:()=>{},};};tools["switch-texture"]=function(config={}){return{icon:config["switch-texture-icon"]||"ph-dress-fill",callback:()=>{},};};tools.photo=function(config={}){let photoName=config["photoName"]||"live2d";return{icon:config["photoIcon"]||"ph-camera-fill",callback:()=>{message.showMessage("照好了嘛,是不是很可爱呢?",6000,2);Live2D.captureName=photoName+".png";Live2D.captureFrame=true;},};};tools.info=function(config={}){let siteUrl="https://github.com/LIlGG/plugin-live2d";return{icon:config["infoIcon"]||"ph-info-fill",callback:()=>{open(siteUrl);},};};tools.quit=function(config={}){return{icon:config["quitIcon"]||"ph-x-bold",callback:()=>{localStorage.setItem("live2d-display",Date.now());message.showMessage("愿你有一天能与重要的人重逢。",2000,4);document.getElementById("live2d-plugin").style.bottom="-500px";setTimeout(()=>{document.getElementById("live2d-plugin").style.display="none";document.getElementById("live2d-toggle").classList.add("live2d-toggle-active");},3000);},};};tools._registerTools=function(model,config){if(!Array.isArray(config.tools)){config.tools=Object.keys(tools);} 32 | if(config.isAiChat){config.tools.unshift("openai");} 33 | for(let tool of config.tools){if(tools[tool]){let{icon,callback}=tools[tool](config);switch(tool){case"switch-model":callback=()=>model.loadOtherModel();break;case"switch-texture":callback=()=>model.loadRandModel();break;} 34 | document.getElementById("live2d-tool").insertAdjacentHTML("beforeend",``);document.getElementById(`live2d-tool-${tool}`).addEventListener("click",callback);}}};openai.messageTimer=null;openai.sendMessage=async function(msg,config={}){openai.loading=true;if(this.messageTimer){clearTimeout(this.messageTimer);this.messageTimer=null;} 35 | this.messageTimer=setTimeout(()=>{message.showMessage("正在接收来自母星的消息,请耐心等待~",2000,2);},5000);document.getElementById("loadingIcon").style.display="block";document.getElementById("send").style.display="none";let historyMessages=JSON.parse(localStorage.getItem("historyMessages"))||[];let userMessage={role:"user",content:msg,};historyMessages.push(userMessage);const controller=new AbortController();const requestTimeoutId=setTimeout(()=>{abort();},Number(config["chunkTimeout"]||60)*1000);const abort=()=>{controller.abort();clearTimeout(this.messageTimer);clearTimeout(requestTimeoutId);openai.loading=false;document.getElementById("loadingIcon").style.display="none";document.getElementById("send").style.display="block";};const response=await fetch("/apis/api.live2d.halo.run/v1alpha1/live2d/ai/chat-process",{method:"POST",cache:"no-cache",keepalive:true,headers:{"Content-Type":"application/json",Accept:"text/event-stream",},body:JSON.stringify({message:historyMessages,}),signal:controller.signal,});if(!response.ok){if(response.status===401){message.showMessage("请先登录!",2000,4);}else{message.showMessage("对话接口异常了哦~快去联系我的主人吧!",5000,4);} 36 | console.log("get.message.error",response);abort();return;} 37 | let chatMessage={content:"",};clearTimeout(this.messageTimer);clearTimeout(requestTimeoutId);const reader=response.body.getReader();const textDecoder=new TextDecoder();const chat=message.createStreamMessage(Number(config["chunkTimeout"]||60)*1000,Number(config["showChatMessageTimeout"]||10)*1000);document.getElementById("send").style.display="block";document.getElementById("loadingIcon").style.display="none";openai.loading=false;while(true){const{value,done}=await reader.read();if(done){break;} 38 | let text=textDecoder.decode(value);const textArrays=text.split("\n\n");textArrays.forEach((decoder)=>{if(!decoder)return;if(decoder.startsWith("data:")){let dataIndex=decoder.indexOf("data:");if(dataIndex!==-1){decoder=decoder.substring(dataIndex+5);}} 39 | const chatResult=JSON.parse(decoder);const{text,status}=chatResult;try{if(status===200){chatMessage.role="assistant";if(text==="[DONE]"){historyMessages.push(chatMessage);localStorage.setItem("historyMessages",JSON.stringify(historyMessages));chat.stop();}else{chatMessage.content+=text;chat.sendMessage(text);}}else{throw new Error(text);}}catch(e){console.error("[Request] parse error",text);chat.sendMessage(`聊天接口出现异常了:${text}`);}});}};openai.loading=false;openai.chatWindows=function(config={}){let model=document.getElementById("live2d-chat-model");if(model){if(model.classList.contains("live2d-chat-model-active")){model.classList.remove("live2d-chat-model-active");}else{let input=document.getElementById("live2d-chat-input");model.classList.add("live2d-chat-model-active");input.focus();} 40 | return;} 41 | document.body.insertAdjacentHTML("beforeend",`
42 |
43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 |
51 |
`);model=document.getElementById("live2d-chat-model");let send=document.getElementById("live2d-chat-send");let input=document.getElementById("live2d-chat-input");const sendFun=function(){let message=input.value;if(message.length>0&&!openai.loading){input.value="";send.classList.remove("active");send.setAttribute("disabled","disabled");openai.sendMessage(message,config);}};input.addEventListener("input",(e)=>{let message=input.value;if(message.length>0&&!openai.loading){send.classList.add("active");send.removeAttribute("disabled");}else{send.classList.remove("active");send.setAttribute("disabled","disabled");}});send.addEventListener("click",()=>{sendFun();});input.addEventListener("keydown",(e)=>{var keyNum=window.event?e.keyCode:e.which;if(keyNum==13){sendFun();} 52 | if(keyNum==27){model.classList.remove("live2d-chat-model-active");}});input.addEventListener("focus",()=>{message.showMessage("按下回车键可以快速发送消息哦",2000,1);});model.classList.add("live2d-chat-model-active");};window.onload=function(){localStorage.removeItem("historyMessages");};return new Live2d();} 53 | -------------------------------------------------------------------------------- /src/main/resources/static/lib/asteroids/asteroids.min.js: -------------------------------------------------------------------------------- 1 | // http://www.websiteasteroids.com 2 | function Asteroids() { 3 | if (!window.ASTEROIDS) window.ASTEROIDS = { 4 | enemiesKilled: 0 5 | }; 6 | class Vector { 7 | constructor(x, y) { 8 | if (typeof x === "Object") { 9 | this.x = x.x; 10 | this.y = x.y; 11 | } else { 12 | this.x = x; 13 | this.y = y; 14 | } 15 | } 16 | cp() { 17 | return new Vector(this.x, this.y); 18 | } 19 | mul(factor) { 20 | this.x *= factor; 21 | this.y *= factor; 22 | return this; 23 | } 24 | mulNew(factor) { 25 | return new Vector(this.x * factor, this.y * factor); 26 | } 27 | add(vec) { 28 | this.x += vec.x; 29 | this.y += vec.y; 30 | return this; 31 | } 32 | addNew(vec) { 33 | return new Vector(this.x + vec.x, this.y + vec.y); 34 | } 35 | sub(vec) { 36 | this.x -= vec.x; 37 | this.y -= vec.y; 38 | return this; 39 | } 40 | subNew(vec) { 41 | return new Vector(this.x - vec.x, this.y - vec.y); 42 | } 43 | rotate(angle) { 44 | const x = this.x, y = this.y; 45 | this.x = x * Math.cos(angle) - Math.sin(angle) * y; 46 | this.y = x * Math.sin(angle) + Math.cos(angle) * y; 47 | return this; 48 | } 49 | rotateNew(angle) { 50 | return this.cp().rotate(angle); 51 | } 52 | setAngle(angle) { 53 | const l = this.len(); 54 | this.x = Math.cos(angle) * l; 55 | this.y = Math.sin(angle) * l; 56 | return this; 57 | } 58 | setAngleNew(angle) { 59 | return this.cp().setAngle(angle); 60 | } 61 | setLength(length) { 62 | const l = this.len(); 63 | if (l) this.mul(length / l); 64 | else this.x = this.y = length; 65 | return this; 66 | } 67 | setLengthNew(length) { 68 | return this.cp().setLength(length); 69 | } 70 | normalize() { 71 | const l = this.len(); 72 | this.x /= l; 73 | this.y /= l; 74 | return this; 75 | } 76 | normalizeNew() { 77 | return this.cp().normalize(); 78 | } 79 | angle() { 80 | return Math.atan2(this.y, this.x); 81 | } 82 | collidesWith(rect) { 83 | return this.x > rect.x && this.y > rect.y && this.x < rect.x + rect.width && this.y < rect.y + rect.height; 84 | } 85 | len() { 86 | const l = Math.sqrt(this.x * this.x + this.y * this.y); 87 | if (l < 0.005 && l > -0.005) return 0; 88 | return l; 89 | } 90 | is(test) { 91 | return typeof test === "object" && this.x === test.x && this.y === test.y; 92 | } 93 | toString() { 94 | return "[Vector(" + this.x + ", " + this.y + ") angle: " + this.angle() + ", length: " + this.len() + "]"; 95 | } 96 | } 97 | 98 | class Line { 99 | constructor(p1, p2) { 100 | this.p1 = p1; 101 | this.p2 = p2; 102 | } 103 | shift(pos) { 104 | this.p1.add(pos); 105 | this.p2.add(pos); 106 | } 107 | intersectsWithRect(rect) { 108 | const LL = new Vector(rect.x, rect.y + rect.height); 109 | const UL = new Vector(rect.x, rect.y); 110 | const LR = new Vector(rect.x + rect.width, rect.y + rect.height); 111 | const UR = new Vector(rect.x + rect.width, rect.y); 112 | if (this.p1.x > LL.x && this.p1.x < UR.x && this.p1.y < LL.y && this.p1.y > UR.y && this.p2.x > LL.x && this.p2.x < UR.x && this.p2.y < LL.y && this.p2.y > UR.y) return true; 113 | if (this.intersectsLine(new Line(UL, LL))) return true; 114 | if (this.intersectsLine(new Line(LL, LR))) return true; 115 | if (this.intersectsLine(new Line(UL, UR))) return true; 116 | if (this.intersectsLine(new Line(UR, LR))) return true; 117 | return false; 118 | } 119 | intersectsLine(line2) { 120 | const v1 = this.p1, v2 = this.p2; 121 | const v3 = line2.p1, v4 = line2.p2; 122 | const denom = ((v4.y - v3.y) * (v2.x - v1.x)) - ((v4.x - v3.x) * (v2.y - v1.y)); 123 | const numerator = ((v4.x - v3.x) * (v1.y - v3.y)) - ((v4.y - v3.y) * (v1.x - v3.x)); 124 | const numerator2 = ((v2.x - v1.x) * (v1.y - v3.y)) - ((v2.y - v1.y) * (v1.x - v3.x)); 125 | if (denom === 0.0) { 126 | return false; 127 | } 128 | const ua = numerator / denom; 129 | const ub = numerator2 / denom; 130 | return (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0); 131 | } 132 | } 133 | const that = this; 134 | const isIE = !! window.ActiveXObject; 135 | let w = document.documentElement.clientWidth, h = document.documentElement.clientHeight; 136 | const playerWidth = 20, playerHeight = 30; 137 | const playerVerts = [ 138 | [-1 * playerHeight / 2, -1 * playerWidth / 2], 139 | [-1 * playerHeight / 2, playerWidth / 2], 140 | [playerHeight / 2, 0] 141 | ]; 142 | const ignoredTypes = ["HTML", "HEAD", "BODY", "SCRIPT", "TITLE", "META", "STYLE", "LINK", "SHAPE", "LINE", "GROUP", "IMAGE", "STROKE", "FILL", "SKEW", "PATH", "TEXTPATH"]; 143 | const hiddenTypes = ["BR", "HR"]; 144 | const FPS = 50; 145 | const acc = 300; 146 | const maxSpeed = 600; 147 | const rotSpeed = 360; 148 | const bulletSpeed = 700; 149 | const particleSpeed = 400; 150 | const timeBetweenFire = 150; 151 | const timeBetweenBlink = 250; 152 | const bulletRadius = 2; 153 | const maxParticles = isIE ? 20 : 40; 154 | const maxBullets = isIE ? 10 : 20; 155 | this.flame = { 156 | r: [], 157 | y: [] 158 | }; 159 | this.toggleBlinkStyle = function() { 160 | if (this.updated.blink.isActive) { 161 | document.body.classList.remove("ASTEROIDSBLINK"); 162 | } else { 163 | document.body.classList.add("ASTEROIDSBLINK"); 164 | } 165 | this.updated.blink.isActive = !this.updated.blink.isActive; 166 | }; 167 | addStylesheet(".ASTEROIDSBLINK .ASTEROIDSYEAHENEMY", "outline: 2px dotted red;"); 168 | this.pos = new Vector(100, 100); 169 | this.lastPos = false; 170 | this.vel = new Vector(0, 0); 171 | this.dir = new Vector(0, 1); 172 | this.keysPressed = {}; 173 | this.firedAt = false; 174 | this.updated = { 175 | enemies: false, 176 | flame: new Date().getTime(), 177 | blink: { 178 | time: 0, 179 | isActive: false 180 | } 181 | }; 182 | this.scrollPos = new Vector(0, 0); 183 | this.bullets = []; 184 | this.enemies = []; 185 | this.dying = []; 186 | this.totalEnemies = 0; 187 | this.particles = []; 188 | 189 | function updateEnemyIndex() { 190 | for (let enemy of that.enemies) { 191 | enemy.classList.remove("ASTEROIDSYEAHENEMY"); 192 | } 193 | const all = document.body.getElementsByTagName("*"); 194 | that.enemies = []; 195 | for (let i = 0, el; el = all[i]; i++) { 196 | if (!(ignoredTypes.includes(el.tagName.toUpperCase())) && el.prefix !== "g_vml_" && hasOnlyTextualChildren(el) && el.className !== "ASTEROIDSYEAH" && el.offsetHeight > 0) { 197 | el.aSize = size(el); 198 | that.enemies.push(el); 199 | el.classList.add("ASTEROIDSYEAHENEMY"); 200 | if (!el.aAdded) { 201 | el.aAdded = true; 202 | that.totalEnemies++; 203 | } 204 | } 205 | } 206 | }; 207 | updateEnemyIndex(); 208 | let createFlames; 209 | (function() { 210 | const rWidth = playerWidth, rIncrease = playerWidth * 0.1, yWidth = playerWidth * 0.6, yIncrease = yWidth * 0.2, halfR = rWidth / 2, halfY = yWidth / 2, halfPlayerHeight = playerHeight / 2; 211 | createFlames = function() { 212 | that.flame.r = [ 213 | [-1 * halfPlayerHeight, -1 * halfR] 214 | ]; 215 | that.flame.y = [ 216 | [-1 * halfPlayerHeight, -1 * halfY] 217 | ]; 218 | for (let x = 0; x < rWidth; x += rIncrease) { 219 | that.flame.r.push([-random(2, 7) - halfPlayerHeight, x - halfR]); 220 | } 221 | that.flame.r.push([-1 * halfPlayerHeight, halfR]); 222 | for (let x = 0; x < yWidth; x += yIncrease) { 223 | that.flame.y.push([-random(2, 7) - halfPlayerHeight, x - halfY]); 224 | } 225 | that.flame.y.push([-1 * halfPlayerHeight, halfY]); 226 | }; 227 | })(); 228 | createFlames(); 229 | 230 | function radians(deg) { 231 | return deg * Math.PI / 180; 232 | }; 233 | 234 | function random(from, to) { 235 | return Math.floor(Math.random() * (to + 1) + from); 236 | }; 237 | 238 | function boundsCheck(vec) { 239 | if (vec.x > w) vec.x = 0; 240 | else if (vec.x < 0) vec.x = w; 241 | if (vec.y > h) vec.y = 0; 242 | else if (vec.y < 0) vec.y = h; 243 | }; 244 | 245 | function size(element) { 246 | let el = element, left = 0, top = 0; 247 | do { 248 | left += el.offsetLeft || 0; 249 | top += el.offsetTop || 0; 250 | el = el.offsetParent; 251 | } while (el); 252 | return { 253 | x: left, 254 | y: top, 255 | width: element.offsetWidth || 10, 256 | height: element.offsetHeight || 10 257 | }; 258 | }; 259 | 260 | function applyVisibility(vis) { 261 | for (let p of window.ASTEROIDSPLAYERS) { 262 | p.gameContainer.style.visibility = vis; 263 | } 264 | } 265 | 266 | function getElementFromPoint(x, y) { 267 | applyVisibility("hidden"); 268 | let element = document.elementFromPoint(x, y); 269 | if (!element) { 270 | applyVisibility("visible"); 271 | return false; 272 | } 273 | if (element.nodeType === 3) element = element.parentNode; 274 | applyVisibility("visible"); 275 | return element; 276 | }; 277 | 278 | function addParticles(startPos) { 279 | const time = new Date().getTime(); 280 | const amount = maxParticles; 281 | for (let i = 0; i < amount; i++) { 282 | that.particles.push({ 283 | dir: (new Vector(Math.random() * 20 - 10, Math.random() * 20 - 10)).normalize(), 284 | pos: startPos.cp(), 285 | cameAlive: time 286 | }); 287 | } 288 | }; 289 | 290 | function setScore() { 291 | that.points.innerHTML = window.ASTEROIDS.enemiesKilled * 10; 292 | }; 293 | 294 | function hasOnlyTextualChildren(element) { 295 | if (element.offsetLeft < -100 && element.offsetWidth > 0 && element.offsetHeight > 0) return false; 296 | if (hiddenTypes.includes(element.tagName)) return true; 297 | if (element.offsetWidth === 0 && element.offsetHeight === 0) return false; 298 | for (let i = 0; i < element.childNodes.length; i++) { 299 | if (!(hiddenTypes.includes(element.childNodes[i].tagName)) && element.childNodes[i].childNodes.length !== 0) return false; 300 | } 301 | return true; 302 | }; 303 | 304 | function addStylesheet(selector, rules) { 305 | const stylesheet = document.createElement("style"); 306 | stylesheet.rel = "stylesheet"; 307 | stylesheet.id = "ASTEROIDSYEAHSTYLES"; 308 | try { 309 | stylesheet.innerHTML = selector + "{" + rules + "}"; 310 | } catch (e) { 311 | stylesheet.styleSheet.addRule(selector, rules); 312 | } 313 | document.getElementsByTagName("head")[0].appendChild(stylesheet); 314 | }; 315 | 316 | function removeStylesheet(name) { 317 | const stylesheet = document.getElementById(name); 318 | if (stylesheet) { 319 | stylesheet.parentNode.removeChild(stylesheet); 320 | } 321 | }; 322 | this.gameContainer = document.createElement("div"); 323 | this.gameContainer.className = "ASTEROIDSYEAH"; 324 | document.body.appendChild(this.gameContainer); 325 | this.canvas = document.createElement("canvas"); 326 | this.canvas.setAttribute("width", w); 327 | this.canvas.setAttribute("height", h); 328 | this.canvas.className = "ASTEROIDSYEAH"; 329 | Object.assign(this.canvas.style, { 330 | width: w + "px", 331 | height: h + "px", 332 | position: "fixed", 333 | top: "0px", 334 | left: "0px", 335 | bottom: "0px", 336 | right: "0px", 337 | zIndex: "10000" 338 | }); 339 | this.canvas.addEventListener("mousedown", function(e) { 340 | const message = document.createElement("span"); 341 | message.style.position = "absolute"; 342 | message.style.color = "red"; 343 | message.innerHTML = "Press Esc to Quit"; 344 | document.body.appendChild(message); 345 | const x = e.pageX || (e.clientX + document.documentElement.scrollLeft); 346 | const y = e.pageY || (e.clientY + document.documentElement.scrollTop); 347 | message.style.left = x - message.offsetWidth / 2 + "px"; 348 | message.style.top = y - message.offsetHeight / 2 + "px"; 349 | setTimeout(function() { 350 | try { 351 | message.parentNode.removeChild(message); 352 | } catch (e) {} 353 | }, 1000); 354 | }, false); 355 | const eventResize = function() { 356 | that.canvas.style.display = "none"; 357 | w = document.documentElement.clientWidth; 358 | h = document.documentElement.clientHeight; 359 | that.canvas.setAttribute("width", w); 360 | that.canvas.setAttribute("height", h); 361 | Object.assign(that.canvas.style, { 362 | display: "block", 363 | width: w + "px", 364 | height: h + "px" 365 | }); 366 | }; 367 | window.addEventListener("resize", eventResize, false); 368 | this.gameContainer.appendChild(this.canvas); 369 | this.ctx = this.canvas.getContext("2d"); 370 | this.ctx.fillStyle = "black"; 371 | this.ctx.strokeStyle = "black"; 372 | if (!document.getElementById("ASTEROIDS-NAVIGATION")) { 373 | this.navigation = document.createElement("div"); 374 | this.navigation.id = "ASTEROIDS-NAVIGATION"; 375 | this.navigation.className = "ASTEROIDSYEAH"; 376 | Object.assign(this.navigation.style, { 377 | fontFamily: "Arial,sans-serif", 378 | position: "fixed", 379 | zIndex: "10001", 380 | bottom: "20px", 381 | right: "10px", 382 | textAlign: "right" 383 | }); 384 | this.navigation.innerHTML = "(Press Esc to Quit) "; 385 | this.gameContainer.appendChild(this.navigation); 386 | this.points = document.createElement("span"); 387 | this.points.id = "ASTEROIDS-POINTS"; 388 | this.points.style.font = "28pt Arial, sans-serif"; 389 | this.points.style.fontWeight = "bold"; 390 | this.points.className = "ASTEROIDSYEAH"; 391 | this.navigation.appendChild(this.points); 392 | } else { 393 | this.navigation = document.getElementById("ASTEROIDS-NAVIGATION"); 394 | this.points = document.getElementById("ASTEROIDS-POINTS"); 395 | } 396 | setScore(); 397 | const eventKeydown = function(event) { 398 | that.keysPressed[event.key] = true; 399 | switch (event.key) { 400 | case " ": 401 | that.firedAt = 1; 402 | break; 403 | } 404 | if (["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft", " ", "b", "w", "a", "s", "d"].includes(event.key)) { 405 | if (event.preventDefault) event.preventDefault(); 406 | if (event.stopPropagation) event.stopPropagation(); 407 | event.returnValue = false; 408 | event.cancelBubble = true; 409 | return false; 410 | } 411 | }; 412 | document.addEventListener("keydown", eventKeydown, false); 413 | const eventKeypress = function(event) { 414 | if (["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft", " ", "w", "a", "s", "d"].includes(event.key)) { 415 | if (event.preventDefault) event.preventDefault(); 416 | if (event.stopPropagation) event.stopPropagation(); 417 | event.returnValue = false; 418 | event.cancelBubble = true; 419 | return false; 420 | } 421 | }; 422 | document.addEventListener("keypress", eventKeypress, false); 423 | const eventKeyup = function(event) { 424 | that.keysPressed[event.key] = false; 425 | if (["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft", " ", "b", "w", "a", "s", "d"].includes(event.key)) { 426 | if (event.preventDefault) event.preventDefault(); 427 | if (event.stopPropagation) event.stopPropagation(); 428 | event.returnValue = false; 429 | event.cancelBubble = true; 430 | return false; 431 | } 432 | }; 433 | document.addEventListener("keyup", eventKeyup, false); 434 | this.ctx.clear = function() { 435 | this.clearRect(0, 0, w, h); 436 | }; 437 | this.ctx.clear(); 438 | this.ctx.drawLine = function(xFrom, yFrom, xTo, yTo) { 439 | this.beginPath(); 440 | this.moveTo(xFrom, yFrom); 441 | this.lineTo(xTo, yTo); 442 | this.lineTo(xTo + 1, yTo + 1); 443 | this.closePath(); 444 | this.fill(); 445 | }; 446 | this.ctx.tracePoly = function(verts) { 447 | this.beginPath(); 448 | this.moveTo(verts[0][0], verts[0][1]); 449 | for (let i = 1; i < verts.length; i++) 450 | this.lineTo(verts[i][0], verts[i][1]); 451 | this.closePath(); 452 | }; 453 | this.ctx.drawPlayer = function() { 454 | this.save(); 455 | this.translate(that.pos.x, that.pos.y); 456 | this.rotate(that.dir.angle()); 457 | this.tracePoly(playerVerts); 458 | this.fillStyle = "white"; 459 | this.fill(); 460 | this.tracePoly(playerVerts); 461 | this.stroke(); 462 | this.restore(); 463 | }; 464 | this.ctx.drawBullets = function(bullets) { 465 | for (let i = 0; i < bullets.length; i++) { 466 | this.beginPath(); 467 | this.arc(bullets[i].pos.x, bullets[i].pos.y, bulletRadius, 0, Math.PI * 2, true); 468 | this.closePath(); 469 | this.fill(); 470 | } 471 | }; 472 | const randomParticleColor = function() { 473 | return (["red", "yellow"])[random(0, 1)]; 474 | }; 475 | this.ctx.drawParticles = function(particles) { 476 | const oldColor = this.fillStyle; 477 | for (let i = 0; i < particles.length; i++) { 478 | this.fillStyle = randomParticleColor(); 479 | this.drawLine(particles[i].pos.x, particles[i].pos.y, particles[i].pos.x - particles[i].dir.x * 10, particles[i].pos.y - particles[i].dir.y * 10); 480 | } 481 | this.fillStyle = oldColor; 482 | }; 483 | this.ctx.drawFlames = function(flame) { 484 | this.save(); 485 | this.translate(that.pos.x, that.pos.y); 486 | this.rotate(that.dir.angle()); 487 | const oldColor = this.strokeStyle; 488 | this.strokeStyle = "red"; 489 | this.tracePoly(flame.r); 490 | this.stroke(); 491 | this.strokeStyle = "yellow"; 492 | this.tracePoly(flame.y); 493 | this.stroke(); 494 | this.strokeStyle = oldColor; 495 | this.restore(); 496 | } 497 | addParticles(this.pos); 498 | document.body.classList.add("ASTEROIDSYEAH"); 499 | let lastUpdate = new Date().getTime(); 500 | function updateFunc() { 501 | that.update.call(that); 502 | }; 503 | setTimeout(updateFunc, 1000 / FPS); 504 | this.update = function() { 505 | let forceChange = false; 506 | const nowTime = new Date().getTime(); 507 | const tDelta = (nowTime - lastUpdate) / 1000; 508 | lastUpdate = nowTime; 509 | let drawFlame = false; 510 | if (nowTime - this.updated.flame > 50) { 511 | createFlames(); 512 | this.updated.flame = nowTime; 513 | } 514 | this.scrollPos.x = window.pageXOffset || document.documentElement.scrollLeft; 515 | this.scrollPos.y = window.pageYOffset || document.documentElement.scrollTop; 516 | if ((this.keysPressed["ArrowUp"]) || (this.keysPressed["w"])) { 517 | this.vel.add(this.dir.mulNew(acc * tDelta)); 518 | drawFlame = true; 519 | } else { 520 | this.vel.mul(0.96); 521 | } 522 | if ((this.keysPressed["ArrowLeft"]) || (this.keysPressed["a"])) { 523 | forceChange = true; 524 | this.dir.rotate(radians(rotSpeed * tDelta * -1)); 525 | } 526 | if ((this.keysPressed["ArrowRight"]) || (this.keysPressed["d"])) { 527 | forceChange = true; 528 | this.dir.rotate(radians(rotSpeed * tDelta)); 529 | } 530 | if (this.keysPressed[" "] && nowTime - this.firedAt > timeBetweenFire) { 531 | this.bullets.unshift({ 532 | dir: this.dir.cp(), 533 | pos: this.pos.cp(), 534 | startVel: this.vel.cp(), 535 | cameAlive: nowTime 536 | }); 537 | this.firedAt = nowTime; 538 | if (this.bullets.length > maxBullets) { 539 | this.bullets.pop(); 540 | } 541 | } 542 | if (this.keysPressed["b"]) { 543 | if (!this.updated.enemies) { 544 | updateEnemyIndex(); 545 | this.updated.enemies = true; 546 | } 547 | forceChange = true; 548 | this.updated.blink.time += tDelta * 1000; 549 | if (this.updated.blink.time > timeBetweenBlink) { 550 | this.toggleBlinkStyle(); 551 | this.updated.blink.time = 0; 552 | } 553 | } else { 554 | this.updated.enemies = false; 555 | } 556 | if (this.keysPressed["Escape"]) { 557 | destroy.apply(this); 558 | return; 559 | } 560 | if (this.vel.len() > maxSpeed) { 561 | this.vel.setLength(maxSpeed); 562 | } 563 | this.pos.add(this.vel.mulNew(tDelta)); 564 | if (this.pos.x > w) { 565 | window.scrollTo(this.scrollPos.x + 50, this.scrollPos.y); 566 | this.pos.x = 0; 567 | } else if (this.pos.x < 0) { 568 | window.scrollTo(this.scrollPos.x - 50, this.scrollPos.y); 569 | this.pos.x = w; 570 | } 571 | if (this.pos.y > h) { 572 | window.scrollTo(this.scrollPos.x, this.scrollPos.y + h * 0.75); 573 | this.pos.y = 0; 574 | } else if (this.pos.y < 0) { 575 | window.scrollTo(this.scrollPos.x, this.scrollPos.y - h * 0.75); 576 | this.pos.y = h; 577 | } 578 | for (let i = this.bullets.length - 1; i >= 0; i--) { 579 | if (nowTime - this.bullets[i].cameAlive > 2000) { 580 | this.bullets.splice(i, 1); 581 | forceChange = true; 582 | continue; 583 | } 584 | const bulletVel = this.bullets[i].dir.setLengthNew(bulletSpeed * tDelta).add(this.bullets[i].startVel.mulNew(tDelta)); 585 | this.bullets[i].pos.add(bulletVel); 586 | boundsCheck(this.bullets[i].pos); 587 | const murdered = getElementFromPoint(this.bullets[i].pos.x, this.bullets[i].pos.y); 588 | if (murdered && murdered.tagName && !(ignoredTypes.includes(murdered.tagName.toUpperCase())) && hasOnlyTextualChildren(murdered) && murdered.className !== "ASTEROIDSYEAH") { 589 | addParticles(this.bullets[i].pos); 590 | this.dying.push(murdered); 591 | this.bullets.splice(i, 1); 592 | continue; 593 | } 594 | } 595 | if (this.dying.length) { 596 | for (let i = this.dying.length - 1; i >= 0; i--) { 597 | try { 598 | if (this.dying[i].parentNode) window.ASTEROIDS.enemiesKilled++; 599 | this.dying[i].parentNode.removeChild(this.dying[i]); 600 | } catch (e) {} 601 | } 602 | setScore(); 603 | this.dying = []; 604 | } 605 | for (let i = this.particles.length - 1; i >= 0; i--) { 606 | this.particles[i].pos.add(this.particles[i].dir.mulNew(particleSpeed * tDelta * Math.random())); 607 | if (nowTime - this.particles[i].cameAlive > 1000) { 608 | this.particles.splice(i, 1); 609 | forceChange = true; 610 | continue; 611 | } 612 | } 613 | if (forceChange || this.bullets.length !== 0 || this.particles.length !== 0 || !this.pos.is(this.lastPos) || this.vel.len() > 0) { 614 | this.ctx.clear(); 615 | this.ctx.drawPlayer(); 616 | if (drawFlame) this.ctx.drawFlames(that.flame); 617 | if (this.bullets.length) { 618 | this.ctx.drawBullets(this.bullets); 619 | } 620 | if (this.particles.length) { 621 | this.ctx.drawParticles(this.particles); 622 | } 623 | } 624 | this.lastPos = this.pos; 625 | setTimeout(updateFunc, 1000 / FPS); 626 | } 627 | 628 | function destroy() { 629 | document.removeEventListener("keydown", eventKeydown, false); 630 | document.removeEventListener("keypress", eventKeypress, false); 631 | document.removeEventListener("keyup", eventKeyup, false); 632 | window.removeEventListener("resize", eventResize, false); 633 | removeStylesheet("ASTEROIDSYEAHSTYLES"); 634 | document.body.classList.remove("ASTEROIDSYEAH"); 635 | this.gameContainer.parentNode.removeChild(this.gameContainer); 636 | }; 637 | } 638 | 639 | if (!window.ASTEROIDSPLAYERS) window.ASTEROIDSPLAYERS = []; 640 | window.ASTEROIDSPLAYERS.push(new Asteroids()); -------------------------------------------------------------------------------- /src/main/resources/static/lib/iconify/3.0.1/iconify.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Iconify 3 | * 4 | * For the full copyright and license information, please view the license.txt or license.gpl.txt 5 | * files at https://github.com/iconify/iconify 6 | * 7 | * Licensed under MIT. 8 | * 9 | * @license MIT 10 | * @version 3.0.1 11 | */ 12 | var Iconify=function(e){"use strict";var n=Object.freeze({left:0,top:0,width:16,height:16}),t=Object.freeze({rotate:0,vFlip:!1,hFlip:!1}),r=Object.freeze(Object.assign({},n,t)),i=Object.freeze(Object.assign({},r,{body:"",hidden:!1}));function o(e,n){var r=function(e,n){var t={};!e.hFlip!=!n.hFlip&&(t.hFlip=!0),!e.vFlip!=!n.vFlip&&(t.vFlip=!0);var r=((e.rotate||0)+(n.rotate||0))%4;return r&&(t.rotate=r),t}(e,n);for(var o in i)o in t?o in e&&!(o in r)&&(r[o]=t[o]):o in n?r[o]=n[o]:o in e&&(r[o]=e[o]);return r}function a(e,n,t){var r=e.icons,i=e.aliases||Object.create(null),a={};function c(e){a=o(r[e]||i[e],a)}return c(n),t.forEach(c),o(e,a)}function c(e,n){var t=[];if("object"!=typeof e||"object"!=typeof e.icons)return t;e.not_found instanceof Array&&e.not_found.forEach((function(e){n(e,null),t.push(e)}));var r=function(e,n){var t=e.icons,r=e.aliases||Object.create(null),i=Object.create(null);return(n||Object.keys(t).concat(Object.keys(r))).forEach((function e(n){if(t[n])return i[n]=[];if(!(n in i)){i[n]=null;var o=r[n]&&r[n].parent,a=o&&e(o);a&&(i[n]=[o].concat(a))}return i[n]})),i}(e);for(var i in r){var o=r[i];o&&(n(i,a(e,i,o)),t.push(i))}return t}var u=/^[a-z0-9]+(-[a-z0-9]+)*$/,s=function(e,n,t,r){void 0===r&&(r="");var i=e.split(":");if("@"===e.slice(0,1)){if(i.length<2||i.length>3)return null;r=i.shift().slice(1)}if(i.length>3||!i.length)return null;if(i.length>1){var o=i.pop(),a=i.pop(),c={provider:i.length>0?i[0]:r,prefix:a,name:o};return n&&!f(c)?null:c}var u=i[0],s=u.split("-");if(s.length>1){var d={provider:r,prefix:s.shift(),name:s.join("-")};return n&&!f(d)?null:d}if(t&&""===r){var l={provider:r,prefix:"",name:u};return n&&!f(l,t)?null:l}return null},f=function(e,n){return!!e&&!(""!==e.provider&&!e.provider.match(u)||!(n&&""===e.prefix||e.prefix.match(u))||!e.name.match(u))},d=Object.assign({},{provider:"",aliases:{},not_found:{}},n);function l(e,n){for(var t in n)if(t in e&&typeof e[t]!=typeof n[t])return!1;return!0}function v(e){if("object"!=typeof e||null===e)return null;var n=e;if("string"!=typeof n.prefix||!e.icons||"object"!=typeof e.icons)return null;if(!l(e,d))return null;var t=n.icons;for(var r in t){var o=t[r];if(!r.match(u)||"string"!=typeof o.body||!l(o,i))return null}var a=n.aliases||Object.create(null);for(var c in a){var s=a[c],f=s.parent;if(!c.match(u)||"string"!=typeof f||!t[f]&&!a[f]||!l(s,i))return null}return n}var p=Object.create(null);function h(e,n){var t=p[e]||(p[e]=Object.create(null));return t[n]||(t[n]=function(e,n){return{provider:e,prefix:n,icons:Object.create(null),missing:new Set}}(e,n))}function g(e,n){return v(n)?c(n,(function(n,t){t?e.icons[n]=t:e.missing.add(n)})):[]}function b(e,n){var t=[];return("string"==typeof e?[e]:Object.keys(p)).forEach((function(e){("string"==typeof e&&"string"==typeof n?[n]:Object.keys(p[e]||{})).forEach((function(n){var r=h(e,n);t=t.concat(Object.keys(r.icons).map((function(t){return(""!==e?"@"+e+":":"")+n+":"+t})))}))})),t}var m=!1;function y(e){var n="string"==typeof e?s(e,!0,m):e;if(n){var t=h(n.provider,n.prefix),r=n.name;return t.icons[r]||(t.missing.has(r)?null:void 0)}}function x(e,n){var t=s(e,!0,m);return!!t&&function(e,n,t){try{if("string"==typeof t.body)return e.icons[n]=Object.assign({},t),!0}catch(e){}return!1}(h(t.provider,t.prefix),t.name,n)}function j(e,n){if("object"!=typeof e)return!1;if("string"!=typeof n&&(n=e.provider||""),m&&!n&&!e.prefix){var t=!1;return v(e)&&(e.prefix="",c(e,(function(e,n){n&&x(e,n)&&(t=!0)}))),t}var r=e.prefix;return!!f({provider:n,prefix:r,name:"a"})&&!!g(h(n,r),e)}function w(e){return!!y(e)}function O(e){var n=y(e);return n?Object.assign({},r,n):null}var S=Object.freeze({width:null,height:null}),E=Object.freeze(Object.assign({},S,t)),I=/(-?[0-9.]*[0-9]+[0-9.]*)/g,k=/^-?[0-9.]*[0-9]+[0-9.]*$/g;function C(e,n,t){if(1===n)return e;if(t=t||100,"number"==typeof e)return Math.ceil(e*n*t)/t;if("string"!=typeof e)return e;var r=e.split(I);if(null===r||!r.length)return e;for(var i=[],o=r.shift(),a=k.test(o);;){if(a){var c=parseFloat(o);isNaN(c)?i.push(o):i.push(Math.ceil(c*n*t)/t)}else i.push(o);if(void 0===(o=r.shift()))return i.join("");a=!a}}function M(e,n){var t=Object.assign({},r,e),i=Object.assign({},E,n),o={left:t.left,top:t.top,width:t.width,height:t.height},a=t.body;[t,i].forEach((function(e){var n,t=[],r=e.hFlip,i=e.vFlip,c=e.rotate;switch(r?i?c+=2:(t.push("translate("+(o.width+o.left).toString()+" "+(0-o.top).toString()+")"),t.push("scale(-1 1)"),o.top=o.left=0):i&&(t.push("translate("+(0-o.left).toString()+" "+(o.height+o.top).toString()+")"),t.push("scale(1 -1)"),o.top=o.left=0),c<0&&(c-=4*Math.floor(c/4)),c%=4){case 1:n=o.height/2+o.top,t.unshift("rotate(90 "+n.toString()+" "+n.toString()+")");break;case 2:t.unshift("rotate(180 "+(o.width/2+o.left).toString()+" "+(o.height/2+o.top).toString()+")");break;case 3:n=o.width/2+o.left,t.unshift("rotate(-90 "+n.toString()+" "+n.toString()+")")}c%2==1&&(o.left!==o.top&&(n=o.left,o.left=o.top,o.top=n),o.width!==o.height&&(n=o.width,o.width=o.height,o.height=n)),t.length&&(a=''+a+"")}));var c,u,s=i.width,f=i.height,d=o.width,l=o.height;return null===s?c=C(u=null===f?"1em":"auto"===f?l:f,d/l):(c="auto"===s?d:s,u=null===f?C(c,l/d):"auto"===f?l:f),{attributes:{width:c.toString(),height:u.toString(),viewBox:o.left.toString()+" "+o.top.toString()+" "+d.toString()+" "+l.toString()},body:a}}var T=/\sid="(\S+)"/g,A="IconifyId"+Date.now().toString(16)+(16777216*Math.random()|0).toString(16),F=0;function L(e,n){void 0===n&&(n=A);for(var t,r=[];t=T.exec(e);)r.push(t[1]);return r.length?(r.forEach((function(t){var r="function"==typeof n?n(t):n+(F++).toString(),i=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");e=e.replace(new RegExp('([#;"])('+i+')([")]|\\.[a-z])',"g"),"$1"+r+"$3")})),e):e}var P={local:!0,session:!0},N={local:new Set,session:new Set},z=!1;var _="iconify2",D="iconify",$="iconify-count",q="iconify-version",R=36e5;function U(e,n){try{return e.getItem(n)}catch(e){}}function V(e,n,t){try{return e.setItem(n,t),!0}catch(e){}}function H(e,n){try{e.removeItem(n)}catch(e){}}function Q(e,n){return V(e,$,n.toString())}function G(e){return parseInt(U(e,$))||0}var J="undefined"==typeof window?{}:window;function B(e){var n=e+"Storage";try{if(J&&J[n]&&"number"==typeof J[n].length)return J[n]}catch(e){}P[e]=!1}function K(e,n){var t=B(e);if(t){var r=U(t,q);if(r!==_){if(r)for(var i=G(t),o=0;oa&&"string"==typeof o.provider&&"object"==typeof o.data&&"string"==typeof o.data.prefix&&n(o,e))return!0}catch(e){}H(t,r)}},u=G(t),s=u-1;s>=0;s--)c(s)||(s===u-1?(u--,Q(t,u)):N[e].add(s))}}function W(){if(!z)for(var e in z=!0,P)K(e,(function(e){var n=e.data,t=h(e.provider,n.prefix);if(!g(t,n).length)return!1;var r=n.lastModified||-1;return t.lastModifiedCached=t.lastModifiedCached?Math.min(t.lastModifiedCached,r):r,!0}))}function X(e,n){switch(e){case"local":case"session":P[e]=n;break;case"all":for(var t in P)P[t]=n}}var Y=Object.create(null);function Z(e,n){Y[e]=n}function ee(e){return Y[e]||Y[""]}function ne(e){var n;if("string"==typeof e.resources)n=[e.resources];else if(!((n=e.resources)instanceof Array&&n.length))return null;return{resources:n,path:e.path||"/",maxURL:e.maxURL||500,rotate:e.rotate||750,timeout:e.timeout||5e3,random:!0===e.random,index:e.index||0,dataAfterTimeout:!1!==e.dataAfterTimeout}}for(var te=Object.create(null),re=["https://api.simplesvg.com","https://api.unisvg.com"],ie=[];re.length>0;)1===re.length||Math.random()>.5?ie.push(re.shift()):ie.push(re.pop());function oe(e,n){var t=ne(n);return null!==t&&(te[e]=t,!0)}function ae(e){return te[e]}te[""]=ne({resources:["https://api.iconify.design"].concat(ie)});var ce=function(){var e;try{if("function"==typeof(e=fetch))return e}catch(e){}}();var ue={prepare:function(e,n,t){var r=[],i=function(e,n){var t,r=ae(e);if(!r)return 0;if(r.maxURL){var i=0;r.resources.forEach((function(e){var n=e;i=Math.max(i,n.length)}));var o=n+".json?icons=";t=r.maxURL-i-r.path.length-o.length}else t=0;return t}(e,n),o="icons",a={type:o,provider:e,prefix:n,icons:[]},c=0;return t.forEach((function(t,u){(c+=t.length+1)>=i&&u>0&&(r.push(a),a={type:o,provider:e,prefix:n,icons:[]},c=t.length),a.icons.push(t)})),r.push(a),r},send:function(e,n,t){if(ce){var r=function(e){if("string"==typeof e){var n=ae(e);if(n)return n.path}return"/"}(n.provider);switch(n.type){case"icons":var i=n.prefix,o=n.icons.join(",");r+=i+".json?"+new URLSearchParams({icons:o}).toString();break;case"custom":var a=n.uri;r+="/"===a.slice(0,1)?a.slice(1):a;break;default:return void t("abort",400)}var c=503;ce(e+r).then((function(e){var n=e.status;if(200===n)return c=501,e.json();setTimeout((function(){t(function(e){return 404===e}(n)?"abort":"next",n)}))})).then((function(e){"object"==typeof e&&null!==e?setTimeout((function(){t("success",e)})):setTimeout((function(){404===e?t("abort",e):t("next",c)}))})).catch((function(){t("next",c)}))}else t("abort",424)}};function se(e,n){e.forEach((function(e){var t=e.loaderCallbacks;t&&(e.loaderCallbacks=t.filter((function(e){return e.id!==n})))}))}var fe=0;var de={resources:[],index:0,timeout:2e3,rotate:750,random:!1,dataAfterTimeout:!1};function le(e,n,t,r){var i,o=e.resources.length,a=e.random?Math.floor(Math.random()*o):e.index;if(e.random){var c=e.resources.slice(0);for(i=[];c.length>1;){var u=Math.floor(Math.random()*c.length);i.push(c[u]),c=c.slice(0,u).concat(c.slice(u+1))}i=i.concat(c)}else i=e.resources.slice(a).concat(e.resources.slice(0,a));var s,f=Date.now(),d="pending",l=0,v=null,p=[],h=[];function g(){v&&(clearTimeout(v),v=null)}function b(){"pending"===d&&(d="aborted"),g(),p.forEach((function(e){"pending"===e.status&&(e.status="aborted")})),p=[]}function m(e,n){n&&(h=[]),"function"==typeof e&&h.push(e)}function y(){d="failed",h.forEach((function(e){e(void 0,s)}))}function x(){p.forEach((function(e){"pending"===e.status&&(e.status="aborted")})),p=[]}function j(){if("pending"===d){g();var r=i.shift();if(void 0===r)return p.length?void(v=setTimeout((function(){g(),"pending"===d&&(x(),y())}),e.timeout)):void y();var o={status:"pending",resource:r,callback:function(n,t){!function(n,t,r){var o="success"!==t;switch(p=p.filter((function(e){return e!==n})),d){case"pending":break;case"failed":if(o||!e.dataAfterTimeout)return;break;default:return}if("abort"===t)return s=r,void y();if(o)return s=r,void(p.length||(i.length?j():y()));if(g(),x(),!e.random){var a=e.resources.indexOf(n.resource);-1!==a&&a!==e.index&&(e.index=a)}d="completed",h.forEach((function(e){e(r)}))}(o,n,t)}};p.push(o),l++,v=setTimeout(j,e.rotate),t(r,n,o.callback)}}return"function"==typeof r&&h.push(r),setTimeout(j),function(){return{startTime:f,payload:n,status:d,queriesSent:l,queriesPending:p.length,subscribe:m,abort:b}}}function ve(e){var n=Object.assign({},de,e),t=[];function r(){t=t.filter((function(e){return"pending"===e().status}))}var i={query:function(e,i,o){var a=le(n,e,i,(function(e,n){r(),o&&o(e,n)}));return t.push(a),a},find:function(e){return t.find((function(n){return e(n)}))||null},setIndex:function(e){n.index=e},getIndex:function(){return n.index},cleanup:r};return i}function pe(){}var he=Object.create(null);function ge(e,n,t){var r,i;if("string"==typeof e){var o=ee(e);if(!o)return t(void 0,424),pe;i=o.send;var a=function(e){if(!he[e]){var n=ae(e);if(!n)return;var t={config:n,redundancy:ve(n)};he[e]=t}return he[e]}(e);a&&(r=a.redundancy)}else{var c=ne(e);if(c){r=ve(c);var u=ee(e.resources?e.resources[0]:"");u&&(i=u.send)}}return r&&i?r.query(n,i,t)().abort:(t(void 0,424),pe)}function be(e,n){function t(t){var r;if(P[t]&&(r=B(t))){var i,o=N[t];if(o.size)o.delete(i=Array.from(o).shift());else if(!Q(r,(i=G(r))+1))return;var a={cached:Math.floor(Date.now()/R),provider:e.provider,data:n};return V(r,D+i.toString(),JSON.stringify(a))}}z||W(),n.lastModified&&!function(e,n){var t=e.lastModifiedCached;if(t&&t>=n)return t===n;if(e.lastModifiedCached=n,t)for(var r in P)K(r,(function(t){var r=t.data;return t.provider!==e.provider||r.prefix!==e.prefix||r.lastModified===n}));return!0}(e,n.lastModified)||Object.keys(n.icons).length&&(n.not_found&&delete(n=Object.assign({},n)).not_found,t("local")||t("session"))}function me(){}function ye(e){e.iconsLoaderFlag||(e.iconsLoaderFlag=!0,setTimeout((function(){e.iconsLoaderFlag=!1,function(e){e.pendingCallbacksFlag||(e.pendingCallbacksFlag=!0,setTimeout((function(){e.pendingCallbacksFlag=!1;var n=e.loaderCallbacks?e.loaderCallbacks.slice(0):[];if(n.length){var t=!1,r=e.provider,i=e.prefix;n.forEach((function(n){var o=n.icons,a=o.pending.length;o.pending=o.pending.filter((function(n){if(n.prefix!==i)return!0;var a=n.name;if(e.icons[a])o.loaded.push({provider:r,prefix:i,name:a});else{if(!e.missing.has(a))return t=!0,!0;o.missing.push({provider:r,prefix:i,name:a})}return!1})),o.pending.length!==a&&(t||se([e],n.id),n.callback(o.loaded.slice(0),o.missing.slice(0),o.pending.slice(0),n.abort))}))}})))}(e)})))}var xe=function(e,n){var t,r=function(e,n,t){void 0===n&&(n=!0),void 0===t&&(t=!1);var r=[];return e.forEach((function(e){var i="string"==typeof e?s(e,n,t):e;i&&r.push(i)})),r}(e,!0,("boolean"==typeof t&&(m=t),m)),i=function(e){var n={loaded:[],missing:[],pending:[]},t=Object.create(null);e.sort((function(e,n){return e.provider!==n.provider?e.provider.localeCompare(n.provider):e.prefix!==n.prefix?e.prefix.localeCompare(n.prefix):e.name.localeCompare(n.name)}));var r={provider:"",prefix:"",name:""};return e.forEach((function(e){if(r.name!==e.name||r.prefix!==e.prefix||r.provider!==e.provider){r=e;var i=e.provider,o=e.prefix,a=e.name,c=t[i]||(t[i]=Object.create(null)),u=c[o]||(c[o]=h(i,o)),s={provider:i,prefix:o,name:a};(a in u.icons?n.loaded:""===o||u.missing.has(a)?n.missing:n.pending).push(s)}})),n}(r);if(!i.pending.length){var o=!0;return n&&setTimeout((function(){o&&n(i.loaded,i.missing,i.pending,me)})),function(){o=!1}}var a,c,u=Object.create(null),f=[];return i.pending.forEach((function(e){var n=e.provider,t=e.prefix;if(t!==c||n!==a){a=n,c=t,f.push(h(n,t));var r=u[n]||(u[n]=Object.create(null));r[t]||(r[t]=[])}})),i.pending.forEach((function(e){var n=e.provider,t=e.prefix,r=e.name,i=h(n,t),o=i.pendingIcons||(i.pendingIcons=new Set);o.has(r)||(o.add(r),u[n][t].push(r))})),f.forEach((function(e){var n=e.provider,t=e.prefix;u[n][t].length&&function(e,n){e.iconsToLoad?e.iconsToLoad=e.iconsToLoad.concat(n).sort():e.iconsToLoad=n,e.iconsQueueFlag||(e.iconsQueueFlag=!0,setTimeout((function(){e.iconsQueueFlag=!1;var n,t=e.provider,r=e.prefix,i=e.iconsToLoad;delete e.iconsToLoad,i&&(n=ee(t))&&n.prepare(t,r,i).forEach((function(n){ge(t,n,(function(t){if("object"!=typeof t)n.icons.forEach((function(n){e.missing.add(n)}));else try{var r=g(e,t);if(!r.length)return;var i=e.pendingIcons;i&&r.forEach((function(e){i.delete(e)})),be(e,t)}catch(e){console.error(e)}ye(e)}))}))})))}(e,u[n][t])})),n?function(e,n,t){var r=fe++,i=se.bind(null,t,r);if(!n.pending.length)return i;var o={id:r,icons:n,callback:e,abort:i};return t.forEach((function(e){(e.loaderCallbacks||(e.loaderCallbacks=[])).push(o)})),i}(n,i,f):me},je=function(e){return new Promise((function(n,t){var i="string"==typeof e?s(e,!0):e;i?xe([i||e],(function(o){if(o.length&&i){var a=y(i);if(a)return void n(Object.assign({},r,a))}t(e)})):t(e)}))};function we(e,n){var t=Object.assign({},e);for(var r in n){var i=n[r],o=typeof i;r in S?(null===i||i&&("string"===o||"number"===o))&&(t[r]=i):o===typeof t[r]&&(t[r]="rotate"===r?i%4:i)}return t}var Oe=Object.assign({},E,{inline:!1}),Se="iconify-inline",Ee="iconifyData"+Date.now(),Ie=[];function ke(e){for(var n=0;n0||"attributes"===i.type&&void 0!==i.target[Ee])return void(t.paused||Fe(e))}}}function Pe(e,n){e.observer.instance.observe(n,Ae)}function Ne(e){var n=e.observer;if(!n||!n.instance){var t="function"==typeof e.node?e.node():e.node;t&&window&&(n||(n={paused:0},e.observer=n),n.instance=new window.MutationObserver(Le.bind(null,e)),Pe(e,t),n.paused||Fe(e))}}function ze(){Me().forEach(Ne)}function _e(e){if(e.observer){var n=e.observer;n.pendingScan&&(clearTimeout(n.pendingScan),delete n.pendingScan),n.instance&&(n.instance.disconnect(),delete n.instance)}}function De(e){var n=null!==Te;Te!==e&&(Te=e,n&&Me().forEach(_e)),n?ze():function(e){var n=document;n.readyState&&"loading"!==n.readyState?e():n.addEventListener("DOMContentLoaded",e)}(ze)}function $e(e){(e?[e]:Me()).forEach((function(e){if(e.observer){var n=e.observer;if(n.paused++,!(n.paused>1)&&n.instance)n.instance.disconnect()}else e.observer={paused:1}}))}function qe(e){if(e){var n=ke(e);n&&$e(n)}else $e()}function Re(e){(e?[e]:Me()).forEach((function(e){if(e.observer){var n=e.observer;if(n.paused&&(n.paused--,!n.paused)){var t="function"==typeof e.node?e.node():e.node;if(!t)return;n.instance?Pe(e,t):Ne(e)}}else Ne(e)}))}function Ue(e){if(e){var n=ke(e);n&&Re(n)}else Re()}function Ve(e,n){void 0===n&&(n=!1);var t=Ce(e,n);return Ne(t),t}function He(e){var n=ke(e);n&&(_e(n),function(e){Ie=Ie.filter((function(n){return e!==n&&e!==("function"==typeof n.node?n.node():n.node)}))}(e))}var Qe=/[\s,]+/;var Ge=["width","height"],Je=["inline","hFlip","vFlip"];function Be(e){var n=e.getAttribute("data-icon"),t="string"==typeof n&&s(n,!0);if(!t)return null;var r=Object.assign({},Oe,{inline:e.classList&&e.classList.contains(Se)});Ge.forEach((function(n){var t=e.getAttribute("data-"+n);t&&(r[n]=t)}));var i=e.getAttribute("data-rotate");"string"==typeof i&&(r.rotate=function(e,n){void 0===n&&(n=0);var t=e.replace(/^-?[0-9.]*/,"");function r(e){for(;e<0;)e+=4;return e%4}if(""===t){var i=parseInt(e);return isNaN(i)?0:r(i)}if(t!==e){var o=0;switch(t){case"%":o=25;break;case"deg":o=90}if(o){var a=parseFloat(e.slice(0,e.length-t.length));return isNaN(a)?0:(a/=o)%1==0?r(a):0}}return n}(i));var o=e.getAttribute("data-flip");"string"==typeof o&&function(e,n){n.split(Qe).forEach((function(n){switch(n.trim()){case"horizontal":e.hFlip=!0;break;case"vertical":e.vFlip=!0}}))}(r,o),Je.forEach((function(n){var t="data-"+n,i=function(e,n){return e===n||"true"===e||""!==e&&"false"!==e&&null}(e.getAttribute(t),t);"boolean"==typeof i&&(r[n]=i)}));var a=e.getAttribute("data-mode");return{name:n,icon:t,customisations:r,mode:a}}function Ke(e,n){var t=-1===e.indexOf("xlink:")?"":' xmlns:xlink="http://www.w3.org/1999/xlink"';for(var r in n)t+=" "+r+'="'+n[r]+'"';return'"+e+""}function We(e){var n=new Set(["iconify"]);return["provider","prefix"].forEach((function(t){e[t]&&n.add("iconify--"+e[t])})),n}function Xe(e,n,t,r){var i=e.classList;if(r){var o=r.classList;Array.from(o).forEach((function(e){i.add(e)}))}var a=[];return n.forEach((function(e){i.contains(e)?t.has(e)&&a.push(e):(i.add(e),a.push(e))})),t.forEach((function(e){n.has(e)||i.remove(e)})),a}function Ye(e,n,t){var r=e.style;(t||[]).forEach((function(e){r.removeProperty(e)}));var i=[];for(var o in n)r.getPropertyValue(o)||(i.push(o),r.setProperty(o,n[o]));return i}function Ze(e,n,t){var r;try{r=document.createElement("span")}catch(n){return e}var i=n.customisations,o=M(t,i),a=e[Ee],c=Ke(L(o.body),Object.assign({},{"aria-hidden":"true",role:"img"},o.attributes));r.innerHTML=c;for(var u=r.childNodes[0],s=e.attributes,f=0;f/g,"%3E").replace(/\s+/g," ")+'")'),d=Object.assign({},{"--svg":f,width:sn(a.width),height:sn(a.height)},en,r?nn:tn);var l;i.inline&&(d["vertical-align"]="-0.125em");var v=Ye(e,d,c&&c.addedStyles),p=Object.assign({},n,{status:"loaded",addedClasses:s,addedStyles:v});e[Ee]=p}(n,t,Object.assign({},r,i),c)}Ze(n,t,i)}}));var o=function(e){var n=t[e],r=function(t){var r=n[t];xe(Array.from(r).map((function(n){return{provider:e,prefix:t,name:n}})),dn)};for(var i in n)r(i)};for(var a in t)o(a)}function vn(e,n,t){void 0===t&&(t=!1);var r=y(e);if(!r)return null;var i=s(e),o=we(Oe,n||{}),a=Ze(document.createElement("span"),{name:e,icon:i,customisations:o},r);return t?a.outerHTML:a}function pn(){return"3.0.1"}function hn(e,n){return vn(e,n,!1)}function gn(e,n){return vn(e,n,!0)}function bn(e,n){var t=y(e);return t?M(t,we(Oe,n||{})):null}function mn(e){e?function(e){var n=ke(e);n?ln(n):ln({node:e,temporary:!0},!0)}(e):ln()}if("undefined"!=typeof document&&"undefined"!=typeof window){!function(){if(document.documentElement)return Ce(document.documentElement);Ie.push({node:function(){return document.documentElement}})}();var yn=window;if(void 0!==yn.IconifyPreload){var xn=yn.IconifyPreload,jn="Invalid IconifyPreload syntax.";"object"==typeof xn&&null!==xn&&(xn instanceof Array?xn:[xn]).forEach((function(e){try{("object"!=typeof e||null===e||e instanceof Array||"object"!=typeof e.icons||"string"!=typeof e.prefix||!j(e))&&console.error(jn)}catch(e){console.error(jn)}}))}setTimeout((function(){De(ln),ln()}))}function wn(e,n){X(e,!1!==n)}function On(e){X(e,!0)}if(Z("",ue),"undefined"!=typeof document&&"undefined"!=typeof window){W();var Sn=window;if(void 0!==Sn.IconifyProviders){var En=Sn.IconifyProviders;if("object"==typeof En&&null!==En)for(var In in En){var kn="IconifyProviders["+In+"] is invalid.";try{var Cn=En[In];if("object"!=typeof Cn||!Cn||void 0===Cn.resources)continue;oe(In,Cn)||console.error(kn)}catch(e){console.error(kn)}}}}var Mn={getAPIConfig:ae,setAPIModule:Z,sendAPIQuery:ge,setFetch:function(e){ce=e},getFetch:function(){return ce},listAPIProviders:function(){return Object.keys(te)}},Tn={_api:Mn,addAPIProvider:oe,loadIcons:xe,loadIcon:je,iconExists:w,getIcon:O,listIcons:b,addIcon:x,addCollection:j,replaceIDs:L,calculateSize:C,buildIcon:M,getVersion:pn,renderSVG:hn,renderHTML:gn,renderIcon:bn,scan:mn,observe:Ve,stopObserving:He,pauseObserver:qe,resumeObserver:Ue,enableCache:wn,disableCache:On};return e._api=Mn,e.addAPIProvider=oe,e.addCollection=j,e.addIcon=x,e.buildIcon=M,e.calculateSize=C,e.default=Tn,e.disableCache=On,e.enableCache=wn,e.getIcon=O,e.getVersion=pn,e.iconExists=w,e.listIcons=b,e.loadIcon=je,e.loadIcons=xe,e.observe=Ve,e.pauseObserver=qe,e.renderHTML=gn,e.renderIcon=bn,e.renderSVG=hn,e.replaceIDs=L,e.resumeObserver=Ue,e.scan=mn,e.stopObserving=He,Object.defineProperty(e,"__esModule",{value:!0}),e}({});if("object"==typeof exports)try{for(var key in exports.__esModule=!0,exports.default=Iconify,Iconify)exports[key]=Iconify[key]}catch(e){}try{void 0===self.Iconify&&(self.Iconify=Iconify)}catch(e){} 13 | -------------------------------------------------------------------------------- /src/main/resources/static/live2d-tips.json: -------------------------------------------------------------------------------- 1 | { 2 | "mouseover": [{ 3 | "selector": "#live2d", 4 | "text": ["干嘛呢你,快把手拿开~~", "鼠…鼠标放错地方了!", "你要干嘛呀?", "喵喵喵?", "怕怕(ノ≧∇≦)ノ", "非礼呀!救命!", "这样的话,只能使用武力了!", "我要生气了哦", "不要动手动脚的!", "真…真的是不知羞耻!", "Hentai!"] 5 | }, { 6 | "selector": "#live2d-tool-openai", 7 | "text": ["想要和我聊天吗?", "我可是知道不少东西的!", "呐呐,来和我聊聊天嘛~"] 8 | }, { 9 | "selector": "#live2d-tool-hitokoto", 10 | "text": ["猜猜我要说些什么?", "我从青蛙王子那里听到了不少人生经验。"] 11 | }, { 12 | "selector": "#live2d-tool-asteroids", 13 | "text": ["要不要来玩飞机大战?", "这个按钮上写着「不要点击」。", "想来和我一起玩个游戏吗?", "听说这样可以蹦迪!"] 14 | }, { 15 | "selector": "#live2d-tool-switch-model", 16 | "text": ["你是不是不爱人家了呀,呜呜呜~", "要见见我的姐姐嘛?", "想要看看我妹妹嘛?", "要切换看板娘吗?"] 17 | }, { 18 | "selector": "#live2d-tool-switch-texture", 19 | "text": ["喜欢换装 PLAY 吗?", "这次要扮演什么呢?", "变装!", "让我们看看接下来会发生什么!"] 20 | }, { 21 | "selector": "#live2d-tool-photo", 22 | "text": ["你要给我拍照呀?一二三~茄子~", "要不,我们来合影吧!", "保持微笑就好了~"] 23 | }, { 24 | "selector": "#live2d-tool-info", 25 | "text": ["想要知道更多关于我的事么?", "这里记录着我搬家的历史呢。", "你想深入了解我什么呢?"] 26 | }, { 27 | "selector": "#live2d-tool-quit", 28 | "text": ["到了要说再见的时候了吗?", "呜呜 QAQ 后会有期……", "不要抛弃我呀……", "我们,还能再见面吗……", "哼,你会后悔的!"] 29 | }, { 30 | "selector": "a[href='/']", 31 | "text": ["点击前往首页,想回到上一页可以使用浏览器的后退功能哦。", "点它就可以回到首页啦!", "回首页看看吧。"] 32 | }, { 33 | "selector": "a[href$='/about']", 34 | "text": ["你想知道我家主人是谁吗?", "这里有一些关于我家主人的秘密哦,要不要看看呢?", "发现主人出没地点!"] 35 | }, { 36 | "selector": "a[href='/tags']", 37 | "text": ["点击就可以看文章的标签啦!", "点击来查看所有标签哦。"] 38 | }, { 39 | "selector": "a[href='/categories']", 40 | "text": ["文章都分类好啦~", "点击来查看文章分类哦。"] 41 | }, { 42 | "selector": "a[href='/archives']", 43 | "text": ["翻页比较麻烦吗,那就来看看文章归档吧。", "文章目录都整理在这里啦!"] 44 | }, { 45 | "selector": "#header-menu a", 46 | "text": ["快看看这里都有什么呢?"] 47 | }, { 48 | "selector": ".site-author", 49 | "text": ["我家主人好看吗?", "这是我家主人(*´∇`*)"] 50 | }, { 51 | "selector": ".site-state", 52 | "text": ["这是文章的统计信息~", "要不要点进去看看?"] 53 | }, { 54 | "selector": ".cc-opacity, .post-copyright-author", 55 | "text": ["要记得规范转载哦。", "所有文章均采用 CC BY-NC-SA 4.0 许可协议~", "转载前要先注意下文章的版权协议呢。"] 56 | }, { 57 | "selector": ".links-of-author", 58 | "text": ["这里是主人的常驻地址哦。", "这里有主人的联系方式!"] 59 | }, { 60 | "selector": ".followme", 61 | "text": ["手机扫一下就能继续看,很方便呢~", "扫一扫,打开新世界的大门!"] 62 | }, { 63 | "selector": ".fancybox img, img.medium-zoom-image", 64 | "text": ["点击图片可以放大呢!"] 65 | }, { 66 | "selector": ".copy-btn", 67 | "text": ["代码可以直接点击复制哟。"] 68 | }, { 69 | "selector": ".highlight .table-container, .gist", 70 | "text": ["GitHub!我是新手!", "有问题为什么不先问问神奇海螺呢?"] 71 | }, { 72 | "selector": "a[href^='mailto']", 73 | "text": ["邮件我会及时回复的!", "点击就可以发送邮件啦~"] 74 | }, { 75 | "selector": "a[href^='/tags/']", 76 | "text": ["要去看看 {text} 标签么?", "点它可以查看此标签下的所有文章哟!"] 77 | }, { 78 | "selector": "a[href^='/categories/']", 79 | "text": ["要去看看 {text} 分类么?", "点它可以查看此分类下的所有文章哟!"] 80 | }, { 81 | "selector": "#post-list > div", 82 | "text": ["要看看 {text} 这篇文章吗?"] 83 | }, { 84 | "selector": "a[rel='contents']", 85 | "text": ["点击来阅读全文哦。"] 86 | }, { 87 | "selector": ".beian a", 88 | "text": ["我也是有户口的人哦。", "我的主人可是遵纪守法的好主人。"] 89 | }, { 90 | "selector": ".container a[href^='http'], .nav-link .nav-text", 91 | "text": ["要去看看 {text} 么?", "去 {text} 逛逛吧。", "到 {text} 看看吧。"] 92 | }, { 93 | "selector": ".back-to-top", 94 | "text": ["点它就可以回到顶部啦!", "又回到最初的起点~", "要回到开始的地方么?"] 95 | }, { 96 | "selector": ".reward-container", 97 | "text": ["我是不是棒棒哒~快给我点赞吧!", "要打赏我嘛?好期待啊~", "主人最近在吃土呢,很辛苦的样子,给他一些钱钱吧~"] 98 | }, { 99 | "selector": "#wechat", 100 | "text": ["这是我的微信二维码~"] 101 | }, { 102 | "selector": "#alipay", 103 | "text": ["这是我的支付宝哦!"] 104 | }, { 105 | "selector": ".need-share-button_weibo", 106 | "text": ["微博?来分享一波喵!"] 107 | }, { 108 | "selector": ".need-share-button_wechat", 109 | "text": ["分享到微信吧!"] 110 | }, { 111 | "selector": ".need-share-button_douban", 112 | "text": ["分享到豆瓣好像也不错!"] 113 | }, { 114 | "selector": ".need-share-button_qqzone", 115 | "text": ["QQ 空间,一键转发,耶~"] 116 | }, { 117 | "selector": ".need-share-button_twitter", 118 | "text": ["Twitter?好像是不存在的东西?"] 119 | }, { 120 | "selector": ".need-share-button_facebook", 121 | "text": ["emmm…FB 好像也是不存在的东西?"] 122 | }, { 123 | "selector": ".post-nav-item a[rel='next']", 124 | "text": ["来看看下一篇文章吧。", "点它可以看下一篇文章哦!", "要翻到下一篇文章吗?"] 125 | }, { 126 | "selector": ".post-nav-item a[rel='prev']", 127 | "text": ["来看看上一篇文章吧。", "点它可以看上一篇文章哦!", "要翻到上一篇文章吗?"] 128 | }, { 129 | "selector": ".extend.next", 130 | "text": ["去下一页看看吧。", "点它可以前进哦!", "要翻到下一页吗?"] 131 | }, { 132 | "selector": ".extend.prev", 133 | "text": ["去上一页看看吧。", "点它可以后退哦!", "要翻到上一页吗?"] 134 | }, { 135 | "selector": ".rounded-base", 136 | "text": ["想要去评论些什么吗?", "要说点什么吗?", "觉得博客不错?快来留言和主人交流吧!"] 137 | }, { 138 | "selector": ".rounded-base a", 139 | "text": ["你会不会熟练使用 Markdown 呀?", "使用 Markdown 让评论更美观吧~"] 140 | }, { 141 | "selector": ".relative", 142 | "text": ["要插入一个萌萌哒的表情吗?", "要来一发表情吗?"] 143 | }, { 144 | "selector": ".btn-secondary", 145 | "text": ["要对自己的发言负责哦~", "要提交了吗,请耐心等待回复哦~"] 146 | }, { 147 | "selector": ".comment-item", 148 | "text": ["哇,快看看这个精彩评论!", "如果有疑问,请尽快留言哦~"] 149 | }], 150 | "click": [{ 151 | "selector": "#live2d", 152 | "text": ["是…是不小心碰到了吧…", "萝莉控是什么呀?", "你看到我的小熊了吗?", "再摸的话我可要报警了!⌇●﹏●⌇", "110 吗,这里有个变态一直在摸我(ó﹏ò。)", "不要摸我了,我会告诉老婆来打你的!", "干嘛动我呀!小心我咬你!", "别摸我,有什么好摸的!"] 153 | }, { 154 | "selector": ".rounded-base", 155 | "text": ["要吐槽些什么呢?", "一定要认真填写喵~", "有什么想说的吗?"] 156 | }, { 157 | "selector": ".btn-secondary", 158 | "text": ["提交评论啦~"] 159 | }], 160 | "seasons": [{ 161 | "date": "01/01", 162 | "text": "元旦了呢,新的一年又开始了,今年是{year}年~" 163 | }, { 164 | "date": "02/14", 165 | "text": "又是一年情人节,{year}年找到对象了嘛~" 166 | }, { 167 | "date": "03/08", 168 | "text": "今天是国际妇女节!" 169 | }, { 170 | "date": "03/12", 171 | "text": "今天是植树节,要保护环境呀!" 172 | }, { 173 | "date": "04/01", 174 | "text": "悄悄告诉你一个秘密~今天是愚人节,不要被骗了哦~" 175 | }, { 176 | "date": "05/01", 177 | "text": "今天是五一劳动节,计划好假期去哪里了吗~" 178 | }, { 179 | "date": "06/01", 180 | "text": "儿童节了呢,快活的时光总是短暂,要是永远长不大该多好啊…" 181 | }, { 182 | "date": "09/03", 183 | "text": "中国人民抗日战争胜利纪念日,铭记历史、缅怀先烈、珍爱和平、开创未来。" 184 | }, { 185 | "date": "09/10", 186 | "text": "教师节,在学校要给老师问声好呀~" 187 | }, { 188 | "date": "10/01", 189 | "text": "国庆节到了,为祖国母亲庆生!" 190 | }, { 191 | "date": "11/05-11/12", 192 | "text": "今年的双十一是和谁一起过的呢~" 193 | }, { 194 | "date": "12/20-12/31", 195 | "text": "这几天是圣诞节,主人肯定又去剁手买买买了~" 196 | }], 197 | "time": [{ 198 | "hour": "6-7", 199 | "text": "早上好!一日之计在于晨,美好的一天就要开始了~" 200 | }, { 201 | "hour": "8-11", 202 | "text": "上午好!工作顺利嘛,不要久坐,多起来走动走动哦!" 203 | }, { 204 | "hour": "12-13", 205 | "text": "中午了,工作了一个上午,现在是午餐时间!" 206 | }, { 207 | "hour": "14-17", 208 | "text": "午后很容易犯困呢,今天的运动目标完成了吗?" 209 | }, { 210 | "hour": "18-19", 211 | "text": "傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~" 212 | }, { 213 | "hour": "20-21", 214 | "text": "晚上好,今天过得怎么样?" 215 | }, { 216 | "hour": "22-23", 217 | "text": ["已经这么晚了呀,早点休息吧,晚安~", "深夜时要爱护眼睛呀!"] 218 | }, { 219 | "hour": "0-5", 220 | "text": "你是夜猫子呀?这么晚还不睡觉,明天起的来嘛?" 221 | }], 222 | "message": { 223 | "default": ["好久不见,日子过得好快呢……", "大坏蛋!你都多久没理人家了呀,嘤嘤嘤~", "呐~快来逗我玩吧!", "拿小拳拳锤你胸口!", "记得把小家加入收藏夹哦!"], 224 | "console": "哈哈,你打开了控制台,是想要看看我的小秘密吗?", 225 | "copy": "你都复制了些什么呀,转载要记得加上出处哦!", 226 | "visibilitychange": "哇,你终于回来了~" 227 | } 228 | } 229 | --------------------------------------------------------------------------------