├── .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 |
4 |
5 |
6 |
7 |
8 |
9 | > 难道不想为您的网站添加一只萌萌的看板娘吗?? (ノ≧∇≦)ノ |
10 |
11 | ## 简介
12 | 仅仅是一只可爱的看板娘哦!
13 |
14 | 
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 | 
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 | 
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 | 
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 | ``
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",``);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'"}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 |
--------------------------------------------------------------------------------