ChatGPT with Date

显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。

当前为 2024-08-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT with Date
  3. // @name:en ChatGPT with Date
  4. // @name:zh-CN ChatGPT with Date
  5. // @namespace https://github.com/jiang-taibai/chatgpt-with-date
  6. // @version 2.0.3
  7. // @description 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。
  8. // @description:zh-cn 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。
  9. // @description:en Tampermonkey plugin for displaying ChatGPT historical and real-time conversation time.
  10. // @author CoderJiang
  11. // @license MIT
  12. // @match *://chat.openai.com/*
  13. // @match *://chatgpt.com/*
  14. // @match *://jiang-taibai.github.io/chatgpt-with-date-config-page*
  15. // @icon https://cdn.coderjiang.com/project/chatgpt-with-date/logo.svg
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM_listValues
  21. // @grant GM_deleteValue
  22. // @grant GM_addElement
  23. // @grant GM_addStyle
  24. // @grant GM_openInTab
  25. // @grant unsafeWindow
  26. // @run-at document-end
  27. // ==/UserScript==
  28.  
  29. (function () {
  30. 'use strict';
  31.  
  32. const IsConfigPage = window.location.hostname === 'jiang-taibai.github.io'
  33.  
  34. class SystemConfig {
  35. static Common = {
  36. ApplicationName: 'ChatGPT with Date',
  37. }
  38. static Main = {
  39. WindowRegisterKey: 'ChatGPTWithDate',
  40. }
  41. static Logger = {
  42. TimeFormatTemplate: "{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}.{ms}",
  43. }
  44. static TimeRender = {
  45. Interval: 1000,
  46. TimeClassName: 'chatgpt-time-container',
  47. BatchSize: 100,
  48. BatchTimeout: 200,
  49. RenderRetryCount: 3,
  50. BasicStyle: `
  51. .chatgpt-time-container.user {
  52. display:flex;
  53. justify-content: flex-end;
  54. }
  55. .chatgpt-time-container.assistant {
  56. display:flex;
  57. justify-content: flex-start;
  58. }
  59. `,
  60. TimeTagTemplates: [
  61. // 默认:2023-10-15 12:01:00
  62. `<span style="padding-right: 1rem; color: #ababab; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  63. // 美国:Oct 15, 2023 12:01 PM
  64. `<span style="padding-right: 1rem; color: #ababab; font-size: 0.9em;">{MM#shortname@en} {dd}, {yyyy} {HH#12}:{mm} {HH#tag}</span>`,
  65. // 英国:01/01/2024 12:01
  66. `<span style="padding-right: 1rem; color: #ababab; font-size: 0.9em;">{dd}/{MM}/{yyyy} {HH}:{mm}</span>`,
  67. // 日本:2023年10月15日 12:01
  68. `<span style="padding-right: 1rem; color: #ababab; font-size: 0.9em;">{yyyy}年{MM}月{dd}日 {HH}:{mm}</span>`,
  69. // 显示毫秒数:2023-10-15 12:01:00.000
  70. `<span style="padding-right: 1rem; color: #ababab; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}.{ms}</span>`,
  71. // 复杂模板
  72. `<span style="padding-right: 1rem; margin-bottom: .5rem; background: #2B2B2b; border-radius: 8px; padding: 1px 10px; color: #717171; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  73. `<span style="padding-right: 1rem; margin-bottom: .5rem; background: #d7d7d7; border-radius: 8px; padding: 1px 10px; color: #2b2b2b; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  74. `<span style="padding-right: 1rem; margin-bottom: .5rem; color: #E0E0E0; font-size: 0.9em;"><span style="background: #333; padding: 1px 4px 1px 10px; display: inline-block; border-radius: 8px 0 0 8px;">{yyyy}-{MM}-{dd}</span><span style="background: #606060; padding: 1px 10px 1px 4px; display: inline-block; border-radius: 0 8px 8px 0;">{HH}:{mm}:{ss}</span></span>`,
  75. `<span style="padding-right: 1rem; margin-bottom: .5rem; color: #E0E0E0; font-size: 0.9em;"><span style="background: #848484; padding: 1px 4px 1px 10px; display: inline-block; border-radius: 8px 0 0 8px;">{yyyy}-{MM}-{dd}</span><span style="background: #a6a6a6; padding: 1px 10px 1px 4px; display: inline-block; border-radius: 0 8px 8px 0;">{HH}:{mm}:{ss}</span></span>`,
  76. ],
  77. BasicStyleKey: 'time-render',
  78. AdditionalStyleKey: 'time-render-advanced',
  79. AdditionalScriptKey: 'time-render-advanced',
  80. }
  81. static ConfigPanel = {
  82. AppID: 'CWD-Configuration-Panel',
  83. Icon: {
  84. Close: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M834.858667 191.914667l-6.101334-5.546667-5.162666-3.84-5.674667-3.370667a67.712 67.712 0 0 0-80.469333 11.306667L512 416.042667 287.274667 191.36l-2.688-2.56c-27.946667-24.234667-68.266667-24.192-92.672 0.170667l-4.821334 5.12-4.565333 6.144-3.413333 5.674666a67.797333 67.797333 0 0 0 11.306666 80.469334l225.706667 225.664-227.2 227.285333c-24.32 28.16-24.32 68.394667 0 92.885333l5.12 4.778667 6.144 4.608 5.674667 3.370667a67.712 67.712 0 0 0 80.469333-11.306667l225.621333-225.706667 227.072 227.2c28.586667 24.789333 68.906667 23.936 94.293334-1.493333l4.565333-5.034667 4.096-5.632a67.882667 67.882667 0 0 0-8.618667-85.248L607.786667 512l224.554666-224.597333 4.138667-4.394667c23.04-26.752 22.4-66.986667-1.621333-91.136z"></path></svg>',
  85. Restore: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M597.333333 85.333333a128 128 0 0 1 127.786667 120.490667L725.333333 213.333333v21.333334h170.666667a42.666667 42.666667 0 0 1 4.992 85.034666L896 320h-42.666667V810.666667a128 128 0 0 1-120.490666 127.786666L725.333333 938.666667H298.666667a128 128 0 0 1-127.786667-120.490667L170.666667 810.666667V320H128a42.666667 42.666667 0 0 1-4.992-85.034667L128 234.666667h170.666667V213.333333a128 128 0 0 1 120.490666-127.786666L426.666667 85.333333h170.666666z m170.666667 234.666667H256V810.666667a42.666667 42.666667 0 0 0 37.674667 42.368L298.666667 853.333333h426.666666a42.666667 42.666667 0 0 0 42.368-37.674666L768 810.666667V320zM426.666667 426.666667a42.666667 42.666667 0 0 1 42.368 37.674666L469.333333 469.333333v213.333334a42.666667 42.666667 0 0 1-85.034666 4.992L384 682.666667v-213.333334a42.666667 42.666667 0 0 1 42.666667-42.666666z m170.666666 0a42.666667 42.666667 0 0 1 42.368 37.674666L640 469.333333v213.333334a42.666667 42.666667 0 0 1-85.034667 4.992L554.666667 682.666667v-213.333334a42.666667 42.666667 0 0 1 42.666666-42.666666z m0-256h-170.666666a42.666667 42.666667 0 0 0-42.368 37.674666L384 213.333333v21.333334h256V213.333333a42.666667 42.666667 0 0 0-37.674667-42.368L597.333333 170.666667z"></path></svg>',
  86. Language: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M757.205333 473.173333c5.333333 0 10.453333 2.090667 14.250667 5.717334a19.029333 19.029333 0 0 1 5.888 13.738666v58.154667h141.184c11.093333 0 20.138667 8.704 20.138667 19.413333v232.704a19.797333 19.797333 0 0 1-20.138667 19.413334h-141.184v96.981333a19.754667 19.754667 0 0 1-20.138667 19.370667H716.8a20.565333 20.565333 0 0 1-14.250667-5.674667 19.029333 19.029333 0 0 1-5.888-13.696v-96.981333h-141.141333a20.565333 20.565333 0 0 1-14.250667-5.674667 19.029333 19.029333 0 0 1-5.930666-13.738667v-232.704c0-5.12 2.133333-10.112 5.930666-13.738666a20.565333 20.565333 0 0 1 14.250667-5.674667h141.141333v-58.154667c0-5.162667 2.133333-10.112 5.888-13.738666a20.565333 20.565333 0 0 1 14.250667-5.674667h40.362667zM192.597333 628.394667c22.272 0 40.32 17.365333 40.32 38.826666v38.741334c0 40.618667 32.512 74.368 74.624 77.397333l6.058667 0.213333h80.64c21.930667 0.469333 39.424 17.706667 39.424 38.784 0 21.077333-17.493333 38.314667-39.424 38.784H313.6c-89.088 0-161.28-69.461333-161.28-155.178666v-38.741334c0-21.461333 18.005333-38.826667 40.277333-38.826666z m504.106667 0h-80.64v116.394666h80.64v-116.394666z m161.28 0h-80.64v116.394666h80.64v-116.394666zM320.170667 85.333333c8.234667 0 15.658667 4.778667 18.773333 12.202667H338.773333l161.322667 387.84c2.517333 5.973333 1.706667 12.8-2.005333 18.090667a20.394667 20.394667 0 0 1-16.725334 8.533333h-43.52a20.181333 20.181333 0 0 1-18.688-12.202667L375.850667 395.648H210.901333l-43.264 104.149333A20.181333 20.181333 0 0 1 148.906667 512H105.514667a20.394667 20.394667 0 0 1-16.725334-8.533333 18.773333 18.773333 0 0 1-2.005333-18.090667l161.28-387.84A20.181333 20.181333 0 0 1 266.88 85.333333h53.290667zM716.8 162.901333c42.794667 0 83.84 16.341333 114.090667 45.44a152.234667 152.234667 0 0 1 47.232 109.738667v38.741333c-0.469333 21.077333-18.389333 37.930667-40.32 37.930667s-39.808-16.853333-40.32-37.930667v-38.741333c0-20.608-8.490667-40.32-23.637334-54.869333a82.304 82.304 0 0 0-57.045333-22.741334h-80.64c-21.888-0.469333-39.424-17.706667-39.424-38.784 0-21.077333 17.493333-38.314667 39.424-38.784h80.64z m-423.424 34.304L243.2 318.037333h100.48L293.418667 197.205333z"></path></svg>',
  87. Documentation: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M768.085333 85.333333a128 128 0 0 1 127.701334 120.490667L896 213.333333v597.333334a128 128 0 0 1-120.405333 127.786666l-7.509334 0.213334H256.256a128 128 0 0 1-127.744-120.490667L128.298667 810.666667v-80A43.050667 43.050667 0 0 1 128 725.674667l0.298667-4.949334V213.333333A128 128 0 0 1 248.746667 85.546667L256.256 85.333333h511.829333zM810.666667 768.341333H213.589333V810.666667a42.666667 42.666667 0 0 0 37.717334 42.368l4.949333 0.298666h511.829333a42.666667 42.666667 0 0 0 42.368-37.674666L810.666667 810.666667v-42.325334zM768.085333 170.666667h-180.650666v233.728a42.666667 42.666667 0 0 1-61.738667 38.144l-4.096-2.346667-82.218667-53.376-85.077333 53.674667a42.666667 42.666667 0 0 1-64.341333-26.453334l-0.810667-4.693333-0.256-4.949333V170.666667h-32.64a42.666667 42.666667 0 0 0-42.368 37.674666L213.632 213.333333l-0.042667 469.674667H810.666667L810.709333 213.333333a42.666667 42.666667 0 0 0-37.674666-42.368L768.085333 170.666667z m-265.941333 20.352h-128v136.021333l42.88-26.965333a42.666667 42.666667 0 0 1 36.181333-4.394667l4.992 2.048 4.778667 2.688 39.168 25.386667V191.018667z"></path></svg>',
  88. Minimize: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M202.922667 562.176H407.466667l2.986666 0.128 3.84 0.554667 3.285334 0.810666 4.565333 1.706667 5.205333 2.986667 3.541334 2.688 3.456 3.413333c1.536 1.706667 2.858667 3.413333 4.053333 5.333333l1.92 3.328 1.834667 4.181334 1.28 4.138666 0.938666 4.352 0.426667 3.456 0.128 3.328v217.088c0 22.314667-16.768 40.405333-37.461333 40.405334-19.2 0-35.072-15.573333-37.205334-35.669334l-0.256-4.736V698.026667l-178.432 186.581333a35.541333 35.541333 0 0 1-52.949333-0.896 42.837333 42.837333 0 0 1-2.346667-53.418667l3.157334-3.754666L314.88 642.986667H202.922667c-20.693333 0-37.461333-18.090667-37.461334-40.405334 0-20.736 14.464-37.802667 33.109334-40.106666l4.352-0.298667z m602.538666-1.621333c20.693333 0 37.461333 18.090667 37.461334 40.405333 0 20.736-14.464 37.802667-33.109334 40.106667l-4.352 0.298666H690.346667l175.530666 183.552c14.848 15.530667 15.232 41.130667 0.853334 57.173334a35.498667 35.498667 0 0 1-49.408 4.181333l-3.584-3.285333-179.968-188.202667v119.978667c0 22.314667-16.768 40.448-37.461334 40.448-19.2 0-35.072-15.616-37.248-35.712l-0.213333-4.693334v-213.845333c0-22.314667 16.768-40.405333 37.461333-40.405333h209.152zM188.16 136.234667l3.541333 3.328 179.797334 190.464V209.237333c0-22.314667 16.768-40.448 37.461333-40.448 19.2 0 35.072 15.616 37.248 35.712l0.213333 4.693334v202.154666c0 6.058667-1.237333 11.818667-3.456 17.024a39.765333 39.765333 0 0 1-1.365333 6.826667l-0.853333 5.205333c-3.541333 16.384-16.298667 28.928-32.085334 30.890667l-4.352 0.298667H199.808c-20.693333 0-37.461333-18.133333-37.461333-40.448 0-20.736 14.464-37.802667 33.109333-40.106667l4.352-0.298667h122.112L139.221333 197.248a42.709333 42.709333 0 0 1-0.512-57.173333 35.456 35.456 0 0 1 49.450667-3.84z m698.368 5.333333c12.714667 15.402667 12.501333 38.4 0.213333 53.461333l-3.328 3.626667-190.250666 182.314667h123.349333c20.693333 0 37.461333 18.133333 37.461333 40.448 0 20.736-14.464 37.802667-33.109333 40.106666l-4.352 0.298667h-220.202667c-20.693333 0-37.461333-18.090667-37.461333-40.405333V204.330667c0-22.314667 16.768-40.405333 37.461333-40.405334 19.2 0 35.029333 15.573333 37.205334 35.669334l0.256 4.736v125.44l199.893333-191.573334a35.584 35.584 0 0 1 52.906667 3.413334z"></path></svg>',
  89. Maximize: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M411.648 547.328a36.565333 36.565333 0 0 1 50.688 3.413333c13.866667 14.805333 14.933333 38.101333 3.2 54.186667l-3.2 3.925333-191.701333 204.970667H396.8l4.48 0.298667c19.114667 2.389333 33.92 19.754667 33.92 40.789333 0 21.077333-14.805333 38.4-33.92 40.832L396.8 896H166.4l-4.48-0.256c-17.621333-2.218667-31.616-17.152-33.664-36.010667L128 853.76v-250.026667l0.256-4.778666c2.218667-20.437333 18.432-36.266667 38.144-36.266667s35.925333 15.829333 38.144 36.266667l0.256 4.778666v164.394667l203.264-217.386667 3.584-3.413333z m215.552 340.48l-4.48-0.256c-19.114667-2.346667-33.92-19.712-33.92-40.789333 0-21.077333 14.805333-38.4 33.92-40.789334l4.48-0.298666h155.008l-218.026667-194.346667-3.456-3.541333a43.221333 43.221333 0 0 1-1.408-54.272 36.693333 36.693333 0 0 1 50.176-8.32l3.882667 3.029333 205.824 183.466667V600.32l0.256-4.778667c2.218667-20.437333 18.432-36.266667 38.144-36.266666s35.925333 15.829333 38.144 36.266666l0.256 4.778667v246.442667c0 21.077333-14.805333 38.4-33.92 40.789333l-4.48 0.298667h-230.4zM396.8 128c21.205333 0 38.4 18.389333 38.4 41.088 0 21.034667-14.805333 38.4-33.92 40.789333l-4.48 0.256H264.789333L459.776 384c16.298667 14.506667 18.517333 40.405333 4.906667 57.856a36.693333 36.693333 0 0 1-50.176 8.277333l-3.882667-3.029333-205.824-183.466667V420.266667c0 22.656-17.194667 41.045333-38.4 41.045333-19.712 0-35.925333-15.829333-38.144-36.266667L128 420.266667V169.088c0-21.077333 14.805333-38.4 33.92-40.832L166.4 128h230.4z m460.8 0c19.712 0 35.925333 15.872 38.144 36.266667l0.256 4.821333v246.4c0 22.698667-17.194667 41.088-38.4 41.088-19.712 0-35.925333-15.872-38.144-36.266667l-0.256-4.821333V259.114667l-205.482667 187.648a36.693333 36.693333 0 0 1-54.144-4.608 43.264 43.264 0 0 1 0.853334-54.314667l3.413333-3.584 190.72-174.08H627.2c-21.205333 0-38.4-18.432-38.4-41.088 0-21.077333 14.805333-38.4 33.92-40.832L627.2 128h230.4z"></path></svg>',
  90. },
  91. StyleKey: 'config-panel',
  92. ApplicationRegisterKey: 'configPanel',
  93. I18N: {
  94. default: 'zh',
  95. rollback: 'zh',
  96. supported: ['zh', 'en'],
  97. zh: {
  98. 'restore-info': '恢复出厂设置',
  99. 'restore-warn': '确定恢复出厂设置?你的所有自定义配置将被清除!',
  100. 'toggle-language-info': 'Switch to English',
  101. 'documentation-info': '查看教程',
  102. 'documentation-international-access': '国际访问',
  103. 'documentation-china-access': '中国访问',
  104. 'template': '模板',
  105. 'preview': '预览',
  106. 'code': '代码',
  107. 'position': '位置',
  108. 'advance': '高级',
  109. 'reset': '重置',
  110. 'apply': '应用',
  111. 'save': '保存并关闭',
  112. 'apply-failed': '应用失败',
  113. 'input-html': '请输入 HTML 代码',
  114. 'input-css': '请输入 CSS 代码',
  115. 'input-js': '请输入 JavaScript 代码',
  116. 'position-after-role-left': '角色之后(靠左)',
  117. 'position-after-role-right': '角色之后(靠右)',
  118. 'position-below-role': '角色之下',
  119. 'gpt-prompt-info': '不会写代码?复制提示词让 ChatGPT 帮你写!',
  120. 'copy-success-info': '复制成功,发给 ChatGPT 吧!',
  121. 'js-invalid-info': 'JS 代码无效',
  122. 'gpt-prompt': 'IyAxLiDku7vliqHnroDku4sKCuS9oOmcgOimgeWGmSBIVE1M44CBQ1NT44CBSmF2YVNjcmlwdCDku6PnoIHvvIzlrp7njrDmiJHnmoTpnIDmsYLvvIzlkI7pnaLmiJHlsIbor6bnu4bku4vnu43kvaDlupTor6XmgI7kuYjlhpnku6PnoIHjgIIKCiMgMi4gSFRNTCDopoHmsYIKCuS9oOmcgOimgeWGmeS4gOS4quaXpeacn+aXtumXtOeahOaooeadvyBIVE1MIOWtl+espuS4su+8jOS9oOWPr+S7peS9v+eUqOWNoOS9jeespuadpeihqOekuuaXtumXtOWFg+e0oO+8jOS+i+Wmgu+8mgoKYGBgaHRtbAo8ZGl2IGNsYXNzPSJ0ZXh0LXRhZy1ib3giPgogICAgPHNwYW4gY2xhc3M9ImRhdGUiPnt5eXl5fS17TU19LXtkZH08L3NwYW4+CiAgICA8c3BhbiBjbGFzcz0idGltZSI+e0hIfTp7bW19Ontzc308L3NwYW4+CjwvZGl2PgpgYGAKCuWQjumdouS8muS7i+e7jeS9oOaAjuS5iOeUqCBKYXZhU2NyaXB0IOadpeWunueOsOaYvuekuueJueWumueahOaXtumXtOOAggoKIyAzLiBDU1Mg6KaB5rGCCgooMSkg5LiN5YWB6K645L2/55So5qCH562+6YCJ5oup5Zmo77yM5Y+q6IO95L2/55So57G76YCJ5oup5Zmo5oiWIElEIOmAieaLqeWZqAooMikg5bC96YeP5L2/55So5ZCO5Luj6YCJ5oup5Zmo77yM5LiN5rGh5p+T5YWo5bGA5qC35byPCigzKSDlsL3ph4/kuI3opoHkvb/nlKggYCFpbXBvcnRhbnRgCgojIDQuIEphdmFTY3JpcHQg6KaB5rGCCgojIyA0LjEg5o+Q5L6b55qEIEFQSSDmjqXlj6MKCkFQSSDlrprkuYnlnKggd2luZG93IOS4iu+8jOWmguacieW/heimgeS9oOmcgOimgeWcqCBKUyDohJrmnKzlhoXph43lhpnlh73mlbDjgIIKCi0gd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSk6IOagueaNriBEYXRlIOWvueixoeWwhuaooeadvyBIVE1MIOWtl+espuS4suS4reeahOWGheWuueabv+aNouS4uiBkYXRlCiAg5a+56LGh5oyH5a6a55qE5pe26Ze0CiAgICAtIGRhdGU6IEphdmFTY3JpcHQgRGF0ZSDlrp7kvosKICAgIC0gdGVtcGxhdGU6IEhUTUwg5a2X56ym5Liy77yM5Y2z5L2g5YaZ55qEIEhUTUwg5Luj56CBCiAgICAtIOi/lOWbnuWAvDog5qC85byP5YyW5pe26Ze05ZCO55qEIEhUTUwg5Luj56CBCi0gd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5iZWZvcmVDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0hUTUwpOiDlsIYgdGVtcGxhdGUg5o+S5YWl5Yiw6aG16Z2i5LmL5YmN6LCD55SoCiAgICAtIG1lc3NhZ2VJZDog5raI5oGv55qEIElE77yM5bm26Z2eIEhUTUwg5YWD57Sg55qEIElECiAgICAtIHRpbWVUYWdIVE1MOiDlrZfnrKbkuLLnsbvlnovvvIwnPGRpdiBjbGFzcz0iY2hhdGdwdC10aW1lIj4nICsgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLAogICAgICB0ZW1wbGF0ZSkgKyAnPC9kaXY+JwogICAgLSDov5Tlm57lgLw6IOaXoAotIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYWZ0ZXJDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0NvbnRhaW5lck5vZGUpOiDlsIYgdGVtcGxhdGUg5o+S5YWl5Yiw6aG16Z2i5LmL5ZCO6LCD55SoCiAgICAtIG1lc3NhZ2VJZDog5raI5oGv5pe26Ze05a+55bqU5raI5oGv55qEIElE77yM5bm26Z2eIEhUTUwg5YWD57Sg55qEIElECiAgICAtIHRpbWVUYWdOb2RlOiDmraTml7bnmoToioLngrnmmK8gJzxkaXYgY2xhc3M9ImNoYXRncHQtdGltZSI+JyArIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuZm9ybWF0RGF0ZVRpbWVCeURhdGUoZGF0ZSwKICAgICAgdGVtcGxhdGUpICsgJzwvZGl2Picg55qEIERPTSDoioLngrkKICAgIC0g6L+U5Zue5YC8OiDml6AKCiMjIDQuMiBBUEkg5omn6KGM6YC76L6RCgrns7vnu5/kvJrmjInnhafku6XkuIvpobrluo/miafooYwgQVBJ77yaCgooMSkgdGVtcGxhdGUgPSDkvaDovpPlhaXnmoQgSFRNTCDku6PnoIEKKDIpIHRlbXBsYXRlID0gd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSkKKDMpIHRpbWVUYWdIVE1MID0gJzxkaXYgY2xhc3M9ImNoYXRncHQtdGltZSI+JyArIHRlbXBsYXRlICsgJzwvZGl2PicKKDQpIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYmVmb3JlQ3JlYXRlVGltZVRhZyhtZXNzYWdlSWQsIHRpbWVUYWdIVE1MKQooNSkg5bCGIHRpbWVUYWdIVE1MIOaPkuWFpeWIsOafkOS9jee9rgooNikgdGltZVRhZ05vZGUgPSDliJrliJrmj5LlhaXnmoQgdGltZVRhZ0hUTUwg6IqC54K5Cig3KSB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmFmdGVyQ3JlYXRlVGltZVRhZyhtZXNzYWdlSWQsIHRpbWVUYWdOb2RlKQoKIyMgNC4zIOS7o+eggeinhOiMgwoKKDEpIOivt+S9v+eUqCBFUzYg6K+t5rOVCigyKSDor7fkvb/nlKjkuKXmoLzmqKHlvI8gYCd1c2Ugc3RyaWN0J2AKKDMpIOivt+S9v+eUqCBgY29uc3RgIOWSjCBgbGV0YCDlo7DmmI7lj5jph48KKDQpIOivt+S9v+eUqCBJSUZFIOmBv+WFjeWFqOWxgOWPmOmHj+axoeafkwooNSkg6K+35L2/55SoIGA9PT1gIOWSjCBgIT09YCDpgb/lhY3nsbvlnovovazmjaLpl67popgKKDYpIOazqOmHiuS4gOW+i+WGmeS4reaWh+azqOmHigoKIyA1LiDmoYjkvosKCuS7peS4i+aYr+S4gOS4quahiOS+i++8jOWunueOsOWFieagh+enu+WKqOWIsOaXtumXtOagh+etvuS4iuaXtu+8jOaXpeacn+aYvuekuuS4uuWHoOWkqeWJjeeahOaViOaenOOAggoKSFRNTCDku6PnoIHvvJoKCmBgYGh0bWwKPHNwYW4gY2xhc3M9InRpbWUtdGFnIj57eXl5eX0te01NfS17ZGR9IHtISH06e21tfTp7c3N9PC9zcGFuPgpgYGAKCkNTUyDku6PnoIHvvJoKCmBgYGNzcwoudGltZS10YWcgewogICAgcGFkZGluZy1yaWdodDogMXJlbTsgCiAgICBjb2xvcjogI2FiYWJhYjsgCiAgICBmb250LXNpemU6IDAuOWVtOwp9CmBgYAoKSmF2YVNjcmlwdCDku6PnoIHvvJoKCmBgYGphdmFzY3JpcHQKKCgpID0+IHsKICAgICd1c2Ugc3RyaWN0JzsKICAgIAogICAgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZSA9IChkYXRlLCB0ZW1wbGF0ZSkgPT4gewogICAgICAgIGNvbnN0IGZvcm1hdFZhbHVlID0gKHZhbHVlLCBmb3JtYXQpID0+IHZhbHVlLnRvU3RyaW5nKCkucGFkU3RhcnQoZm9ybWF0ID09PSAneXl5eScgPyA0IDogMiwgJzAnKTsKICAgICAgICBjb25zdCBkYXRlVmFsdWVzID0gewogICAgICAgICAgICAne3l5eXl9JzogZGF0ZS5nZXRGdWxsWWVhcigpLAogICAgICAgICAgICAne01NfSc6IGRhdGUuZ2V0TW9udGgoKSArIDEsCiAgICAgICAgICAgICd7ZGR9JzogZGF0ZS5nZXREYXRlKCksCiAgICAgICAgICAgICd7SEh9JzogZGF0ZS5nZXRIb3VycygpLAogICAgICAgICAgICAne21tfSc6IGRhdGUuZ2V0TWludXRlcygpLAogICAgICAgICAgICAne3NzfSc6IGRhdGUuZ2V0U2Vjb25kcygpCiAgICAgICAgfTsKICAgICAgICByZXR1cm4gdGVtcGxhdGUucmVwbGFjZSgvXHtbXn1dK1x9L2csIG1hdGNoID0+IGZvcm1hdFZhbHVlKGRhdGVWYWx1ZXNbbWF0Y2hdLCBtYXRjaC5zbGljZSgxLCAtMSkpKTsKICAgIH0KICAgIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYWZ0ZXJDcmVhdGVUaW1lVGFnID0gKG1lc3NhZ2VJZCwgdGltZVRhZ0NvbnRhaW5lck5vZGUpID0+IHsKICAgICAgICBjb25zdCB0aW1lVGFnTm9kZSA9IHRpbWVUYWdDb250YWluZXJOb2RlLnF1ZXJ5U2VsZWN0b3IoJy50aW1lLXRhZycpOwogICAgICAgIGNvbnN0IGRhdGVUZXh0ID0gdGltZVRhZ05vZGUuaW5uZXJUZXh0OwogICAgICAgIGNvbnN0IGRhdGUgPSBuZXcgRGF0ZShkYXRlVGV4dCk7CiAgICAgICAgdGltZVRhZ05vZGUuYWRkRXZlbnRMaXN0ZW5lcignbW91c2VvdmVyJywgKCkgPT4gewogICAgICAgICAgICB0aW1lVGFnTm9kZS5pbm5lclRleHQgPSBgJHtNYXRoLmZsb29yKChuZXcgRGF0ZSgpIC0gZGF0ZSkgLyA4NjQwMDAwMCl9IOWkqeWJjWA7CiAgICAgICAgfSk7CiAgICAgICAgdGltZVRhZ05vZGUuYWRkRXZlbnRMaXN0ZW5lcignbW91c2VvdXQnLCAoKSA9PiB7CiAgICAgICAgICAgIHRpbWVUYWdOb2RlLmlubmVyVGV4dCA9IGRhdGVUZXh0OwogICAgICAgIH0pOwogICAgfQp9KSgpCmBgYAoKIyA2LiDkvaDnmoTku7vliqEKCueOsOWcqOS9oOmcgOimgeWGmeS4ieauteS7o+egge+8jOWIhuWIq+S4ukhUTUzjgIFDU1PjgIFKYXZhU2NyaXB077yM6KaB5rGC5aaC5LiL77yaCgotIEhUTUzvvJrkvaDlj6rpnIDopoHlhpnml7bpl7TmoIfnrb7nmoQgSFRNTAotIENTU++8muivt+S9v+eUqOexu+mAieaLqeWZqOaIliBJRCDpgInmi6nlmajvvIzkuI3opoHkvb/nlKjmoIfnrb7pgInmi6nlmajvvIjpmaTpnZ7mmK/lrZDpgInmi6nlmajvvIkKLSBKYXZhU2NyaXB077ya6K+35ZyoIElJRkUg5Lit57yW5YaZ5Luj56CB77yM5L2g5Y+v5Lul5L2/55So5LiK6Z2i6K6y5Yiw55qE5LiJ5Liq6ZKp5a2Q5Ye95pWw44CCCi0g5LiN6KaB6K+05bqf6K+d77yM55u05o6l5LiK5Luj56CB77yM5YiG5Li65LiJ5Liq5Luj56CB5Z2X57uZ5oiR77yM5Zyo5q+P5Liq5Luj56CB5Z2X5LmL5YmN5YaZ5LiK4oCc6L+Z5pivSFRNTOS7o+eggeKAneOAgeKAnOi/meaYr0NTU+S7o+eggeKAneOAgeKAnOi/meaYr0pT5Luj56CB4oCd44CCCi0g5o6l5LiL5p2l55qE5a+56K+d5oiR5LiN5Lya6YeN5aSN5Lul5LiK55qE5YaF5a6577yM5L2g6ZyA6KaB6K6w5L2P6L+Z5Lqb5YaF5a6544CCCi0g5oiR5q+P5qyh5Lya5ZGK6K+J5L2g5oiR6ZyA6KaB5oCO5LmI5pS56L+b5L2g55qE5Luj56CB77yM5L2g6ZyA6KaB5qC55o2u5oiR55qE6KaB5rGC5L+u5pS55Luj56CB44CCCi0g6K+35b+Y6K6w5qGI5L6L5Lit55qE5Luj56CB77yM5oiR55qE6ZyA5rGC5Y+v6IO95Lya5pyJ5omA5LiN5ZCM44CCCgojIDcuIOS9oOmcgOimgeWujOaIkOeahOaIkeeahOmcgOaxggoK5pel5pyf5pe26Ze05qC85byP5qC35L6L77yaMjAyNC0wNC0wMyAxODowOTowMQrkuKrmgKfljJbmoLflvI/vvJrml7bpl7TmoIfnrb7ml6XmnJ/mmL7npLrlnKjlt6bovrnvvIzml7bpl7TmmL7npLrlnKjlj7PovrnvvIzml6XmnJ/lkozml7bpl7TkuYvpl7TmnInkuIDkuKrnq5bnur/liIbpmpTjgILkuI3opoHkvb/nlKjog4zmma/popzoibLvvIzpgInmi6nkuIDkuKrlj6/ku6XpgILlupRkYXJr5qih5byP5ZKMbGlnaHTmqKHlvI/nmoTlrZfkvZPpopzoibLjgIIK6aKd5aSW5pWI5p6c77ya5b2T6byg5qCH56e75Yqo5Yiw5pe26Ze05qCH562+5LiK5pe277yM5pe26Ze05qCH562+5oqW5Yqo5LiA5LiL44CC',
  123. },
  124. en: {
  125. 'restore-info': 'Restore factory settings',
  126. 'restore-warn': 'Are you sure to restore the factory settings? All your custom configurations will be cleared!',
  127. 'toggle-language-info': '切换到中文',
  128. 'documentation-info': 'View documentation',
  129. 'documentation-international-access': 'International Access',
  130. 'documentation-china-access': 'China Access',
  131. 'template': 'Template',
  132. 'preview': 'Preview',
  133. 'code': 'Code',
  134. 'position': 'Position',
  135. 'advance': 'Advance',
  136. 'reset': 'Reset',
  137. 'apply': 'Apply',
  138. 'apply-failed': 'Apply failed',
  139. 'save': 'Save and Close',
  140. 'input-html': 'Please enter HTML code',
  141. 'input-css': 'Please enter CSS code',
  142. 'input-js': 'Please enter JavaScript code',
  143. 'position-after-role-left': 'After Role (left)',
  144. 'position-after-role-right': 'After Role (right)',
  145. 'position-below-role': 'Below Role',
  146. 'gpt-prompt-info': 'Not good at coding? Copy the prompt words and let ChatGPT help you!',
  147. 'copy-success-info': 'Copy successfully, send it to ChatGPT!',
  148. 'js-invalid-info': 'Invalid JS code',
  149. 'gpt-prompt': 'IyAxLiBUYXNrIE92ZXJ2aWV3CgpZb3UgbmVlZCB0byB3cml0ZSBIVE1MLCBDU1MsIGFuZCBKYXZhU2NyaXB0IGNvZGUgdG8gbWVldCBteSByZXF1aXJlbWVudHMuIEkgd2lsbCBwcm92aWRlIGRldGFpbGVkIGluc3RydWN0aW9ucyBvbiBob3cgdG8Kd3JpdGUgdGhlIGNvZGUuCgojIDIuIEhUTUwgUmVxdWlyZW1lbnRzCgpZb3UgbmVlZCB0byBjcmVhdGUgYW4gSFRNTCBzdHJpbmcgdGVtcGxhdGUgZm9yIGRhdGUgYW5kIHRpbWUsIHVzaW5nIHBsYWNlaG9sZGVycyB0byByZXByZXNlbnQgdGltZSBlbGVtZW50cy4gRm9yCmV4YW1wbGU6CgpgYGBodG1sCjxkaXYgY2xhc3M9InRleHQtdGFnLWJveCI+CiAgICA8c3BhbiBjbGFzcz0iZGF0ZSI+e3l5eXl9LXtNTX0te2RkfTwvc3Bhbj4KICAgIDxzcGFuIGNsYXNzPSJ0aW1lIj57SEh9OnttbX06e3NzfTwvc3Bhbj4KPC9kaXY+CmBgYAoKSSB3aWxsIGxhdGVyIGV4cGxhaW4gaG93IHRvIHVzZSBKYXZhU2NyaXB0IHRvIGRpc3BsYXkgc3BlY2lmaWMgdGltZXMuCgojIDMuIENTUyBSZXF1aXJlbWVudHMKCigxKSBEbyBub3QgdXNlIHRhZyBzZWxlY3RvcnM7IG9ubHkgdXNlIGNsYXNzIHNlbGVjdG9ycyBvciBJRCBzZWxlY3RvcnMuICAKKDIpIFVzZSBkZXNjZW5kYW50IHNlbGVjdG9ycyBhcyBtdWNoIGFzIHBvc3NpYmxlIHRvIGF2b2lkIHBvbGx1dGluZyBnbG9iYWwgc3R5bGVzLiAgCigzKSBUcnkgdG8gYXZvaWQgdXNpbmcgYCFpbXBvcnRhbnRgLgoKIyA0LiBKYXZhU2NyaXB0IFJlcXVpcmVtZW50cwoKIyMgNC4xIFByb3ZpZGVkIEFQSSBJbnRlcmZhY2UKCkFQSXMgYXJlIGRlZmluZWQgb24gdGhlIHdpbmRvdyBvYmplY3QuIFlvdSBtYXkgbmVlZCB0byBvdmVycmlkZSB0aGVzZSBmdW5jdGlvbnMgaW4geW91ciBKUyBzY3JpcHQgaWYgbmVjZXNzYXJ5LgoKLSBgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSlgOiBSZXBsYWNlIHRoZSBjb250ZW50IGluIHRoZSB0ZW1wbGF0ZSBIVE1MIHN0cmluZwogIHdpdGggdGhlIHRpbWUgc3BlY2lmaWVkIGJ5IHRoZSBgZGF0ZWAgb2JqZWN0LgogICAgLSBgZGF0ZWA6IEphdmFTY3JpcHQgRGF0ZSBpbnN0YW5jZS4KICAgIC0gYHRlbXBsYXRlYDogSFRNTCBzdHJpbmcsIHdoaWNoIGlzIHlvdXIgSFRNTCBjb2RlLgogICAgLSBSZXR1cm5zOiBUaGUgSFRNTCBjb2RlIGFmdGVyIGZvcm1hdHRpbmcgdGhlIHRpbWUuCi0gYHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYmVmb3JlQ3JlYXRlVGltZVRhZyhtZXNzYWdlSWQsIHRpbWVUYWdIVE1MKWA6IENhbGxlZCBiZWZvcmUgaW5zZXJ0aW5nIHRoZSB0ZW1wbGF0ZSBpbnRvCiAgdGhlIHBhZ2UuCiAgICAtIGBtZXNzYWdlSWRgOiBUaGUgSUQgb2YgdGhlIG1lc3NhZ2UsIG5vdCB0aGUgSUQgb2YgdGhlIEhUTUwgZWxlbWVudC4KICAgIC0gYHRpbWVUYWdIVE1MYDogQQogICAgICBzdHJpbmcsIGAnPGRpdiBjbGFzcz0iY2hhdGdwdC10aW1lIj4nICsgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSkgKyAnPC9kaXY+J2AuCiAgICAtIFJldHVybnM6IE5vbmUuCi0gYHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYWZ0ZXJDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0NvbnRhaW5lck5vZGUpYDogQ2FsbGVkIGFmdGVyIGluc2VydGluZyB0aGUKICB0ZW1wbGF0ZSBpbnRvIHRoZSBwYWdlLgogICAgLSBgbWVzc2FnZUlkYDogVGhlIElEIG9mIHRoZSBtZXNzYWdlLCBub3QgdGhlIElEIG9mIHRoZSBIVE1MIGVsZW1lbnQuCiAgICAtIGB0aW1lVGFnTm9kZWA6IFRoZSBET00gbm9kZQogICAgICBmb3IgYCc8ZGl2IGNsYXNzPSJjaGF0Z3B0LXRpbWUiPicgKyB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmZvcm1hdERhdGVUaW1lQnlEYXRlKGRhdGUsIHRlbXBsYXRlKSArICc8L2Rpdj4nYC4KICAgIC0gUmV0dXJuczogTm9uZS4KCiMjIDQuMiBBUEkgRXhlY3V0aW9uIExvZ2ljCgpUaGUgc3lzdGVtIHdpbGwgZXhlY3V0ZSB0aGUgQVBJcyBpbiB0aGUgZm9sbG93aW5nIG9yZGVyOgoKKDEpIGB0ZW1wbGF0ZSA9YCB5b3VyIGlucHV0IEhUTUwgY29kZS4gIAooMikgYHRlbXBsYXRlID0gd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSlgICAKKDMpIGB0aW1lVGFnSFRNTCA9ICc8ZGl2IGNsYXNzPSJjaGF0Z3B0LXRpbWUiPicgKyB0ZW1wbGF0ZSArICc8L2Rpdj4nYCAgCig0KSBgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5iZWZvcmVDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0hUTUwpYCAgCig1KSBJbnNlcnQgYHRpbWVUYWdIVE1MYCBpbnRvIHNvbWUgbG9jYXRpb24uICAKKDYpIGB0aW1lVGFnTm9kZSA9YCB0aGUgbm9kZSBqdXN0IGluc2VydGVkLiAgCig3KSBgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5hZnRlckNyZWF0ZVRpbWVUYWcobWVzc2FnZUlkLCB0aW1lVGFnTm9kZSlgCgojIyA0LjMgQ29kZSBDb252ZW50aW9ucwoKKDEpIFVzZSBFUzYgc3ludGF4LiAgCigyKSBVc2Ugc3RyaWN0IG1vZGUgYCd1c2Ugc3RyaWN0J2AuICAKKDMpIFVzZSBgY29uc3RgIGFuZCBgbGV0YCB0byBkZWNsYXJlIHZhcmlhYmxlcy4gIAooNCkgVXNlIElJRkUgdG8gYXZvaWQgZ2xvYmFsIHZhcmlhYmxlIHBvbGx1dGlvbi4gIAooNSkgVXNlIGA9PT1gIGFuZCBgIT09YCB0byBhdm9pZCB0eXBlIGNvbnZlcnNpb24gaXNzdWVzLiAgCig2KSBBbGwgY29tbWVudHMgc2hvdWxkIGJlIGluIENoaW5lc2UuCgojIDUuIEV4YW1wbGUKCkhlcmUgaXMgYW4gZXhhbXBsZSBvZiBpbXBsZW1lbnRpbmcgYW4gZWZmZWN0IHdoZXJlLCB3aGVuIGhvdmVyaW5nIG92ZXIgdGhlIHRpbWUgdGFnLCB0aGUgZGF0ZSBkaXNwbGF5cyBhcyBob3cgbWFueSBkYXlzCmFnbyBpdCB3YXMuCgpIVE1MIENvZGU6CgpgYGBodG1sCjxzcGFuIGNsYXNzPSJ0aW1lLXRhZyI+e3l5eXl9LXtNTX0te2RkfSB7SEh9OnttbX06e3NzfTwvc3Bhbj4KYGBgCgpDU1MgQ29kZToKCmBgYGNzcwoudGltZS10YWcgewogICAgcGFkZGluZy1yaWdodDogMXJlbTsgCiAgICBjb2xvcjogI2FiYWJhYjsgCiAgICBmb250LXNpemU6IDAuOWVtOwp9CmBgYAoKSmF2YVNjcmlwdCBDb2RlOgoKYGBgamF2YXNjcmlwdAooKCkgPT4gewogICAgJ3VzZSBzdHJpY3QnOwogICAgCiAgICB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmZvcm1hdERhdGVUaW1lQnlEYXRlID0gKGRhdGUsIHRlbXBsYXRlKSA9PiB7CiAgICAgICAgY29uc3QgZm9ybWF0VmFsdWUgPSAodmFsdWUsIGZvcm1hdCkgPT4gdmFsdWUudG9TdHJpbmcoKS5wYWRTdGFydChmb3JtYXQgPT09ICd5eXl5JyA/IDQgOiAyLCAnMCcpOwogICAgICAgIGNvbnN0IGRhdGVWYWx1ZXMgPSB7CiAgICAgICAgICAgICd7eXl5eX0nOiBkYXRlLmdldEZ1bGxZZWFyKCksCiAgICAgICAgICAgICd7TU19JzogZGF0ZS5nZXRNb250aCgpICsgMSwKICAgICAgICAgICAgJ3tkZH0nOiBkYXRlLmdldERhdGUoKSwKICAgICAgICAgICAgJ3tISH0nOiBkYXRlLmdldEhvdXJzKCksCiAgICAgICAgICAgICd7bW19JzogZGF0ZS5nZXRNaW51dGVzKCksCiAgICAgICAgICAgICd7c3N9JzogZGF0ZS5nZXRTZWNvbmRzKCkKICAgICAgICB9OwogICAgICAgIHJldHVybiB0ZW1wbGF0ZS5yZXBsYWNlKC9ce1tefV0rXH0vZywgbWF0Y2ggPT4gZm9ybWF0VmFsdWUoZGF0ZVZhbHVlc1ttYXRjaF0sIG1hdGNoLnNsaWNlKDEsIC0xKSkpOwogICAgfQogICAgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5hZnRlckNyZWF0ZVRpbWVUYWcgPSAobWVzc2FnZUlkLCB0aW1lVGFnQ29udGFpbmVyTm9kZSkgPT4gewogICAgICAgIGNvbnN0IHRpbWVUYWdOb2RlID0gdGltZVRhZ0NvbnRhaW5lck5vZGUucXVlcnlTZWxlY3RvcignLnRpbWUtdGFnJyk7CiAgICAgICAgY29uc3QgZGF0ZVRleHQgPSB0aW1lVGFnTm9kZS5pbm5lclRleHQ7CiAgICAgICAgY29uc3QgZGF0ZSA9IG5ldyBEYXRlKGRhdGVUZXh0KTsKICAgICAgICB0aW1lVGFnTm9kZS5hZGRFdmVudExpc3RlbmVyKCdtb3VzZW92ZXInLCAoKSA9PiB7CiAgICAgICAgICAgIHRpbWVUYWdOb2RlLmlubmVyVGV4dCA9IGAke01hdGguZmxvb3IoKG5ldyBEYXRlKCkgLSBkYXRlKSAvIDg2NDAwMDAwKX0gZGF5cyBhZ29gOwogICAgICAgIH0pOwogICAgICAgIHRpbWVUYWdOb2RlLmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlb3V0JywgKCkgPT4gewogICAgICAgICAgICB0aW1lVGFnTm9kZS5pbm5lclRleHQgPSBkYXRlVGV4dDsKICAgICAgICB9KTsKICAgIH0KfSkoKQpgYGAKCiMgNi4gWW91ciBUYXNrCgpOb3cgeW91IG5lZWQgdG8gd3JpdGUgdGhyZWUgcGllY2VzIG9mIGNvZGU6IEhUTUwsIENTUywgYW5kIEphdmFTY3JpcHQuIFRoZSByZXF1aXJlbWVudHMgYXJlIGFzIGZvbGxvd3M6CgotIEhUTUw6IE9ubHkgd3JpdGUgdGhlIHRpbWUgdGFnIEhUTUwuCi0gQ1NTOiBVc2UgY2xhc3Mgc2VsZWN0b3JzIG9yIElEIHNlbGVjdG9yczsgZG8gbm90IHVzZSB0YWcgc2VsZWN0b3JzIChleGNlcHQgYXMgY2hpbGQgc2VsZWN0b3JzKS4KLSBKYXZhU2NyaXB0OiBXcml0ZSB0aGUgY29kZSB3aXRoaW4gYW4gSUlGRSBhbmQgdXNlIHRoZSB0aHJlZSBob29rIGZ1bmN0aW9ucyBtZW50aW9uZWQuCi0gRG8gbm90IHJlcGVhdCB1bm5lY2Vzc2FyeSBkZXRhaWxzOyBqdXN0IHByb3ZpZGUgdGhlIGNvZGUgYmxvY2tzLCB3aXRoICJUaGlzIGlzIEhUTUwgY29kZSwiICJUaGlzIGlzIENTUyBjb2RlLCIgYW5kICIKICBUaGlzIGlzIEpTIGNvZGUiIGJlZm9yZSBlYWNoIGJsb2NrLgotIEluIHN1YnNlcXVlbnQgY29udmVyc2F0aW9ucywgSSB3b24ndCByZXBlYXQgdGhlIGFib3ZlIGRldGFpbHM7IHlvdSBuZWVkIHRvIHJlbWVtYmVyIHRoZW0uCi0gSSB3aWxsIHRlbGwgeW91IGhvdyB0byBpbXByb3ZlIHlvdXIgY29kZSBlYWNoIHRpbWUsIGFuZCB5b3UgbmVlZCB0byBtb2RpZnkgdGhlIGNvZGUgYWNjb3JkaW5nbHkuCi0gRm9yZ2V0IHRoZSBleGFtcGxlIGNvZGUgcHJvdmlkZWQ7IG15IHJlcXVpcmVtZW50cyBtYXkgZGlmZmVyLgoKIyA3LiBXaGF0IHlvdSBuZWVkIHRvIGFjY29tcGxpc2ggZm9yIG1lCgpEYXRlIGFuZCB0aW1lIGZvcm1hdCBleGFtcGxlOiAyMDI0LTA0LTAzIDE4OjA5OjAxICAKQ3VzdG9taXphdGlvbjogRGlzcGxheSB0aGUgZGF0ZSBvbiB0aGUgbGVmdCBhbmQgdGhlIHRpbWUgb24gdGhlIHJpZ2h0IGluIHRoZSB0aW1lIHRhZywgd2l0aCBhIHZlcnRpY2FsIGxpbmUgc2VwYXJhdG9yCmJldHdlZW4gdGhlIGRhdGUgYW5kIHRpbWUuIEF2b2lkIHVzaW5nIGJhY2tncm91bmQgY29sb3JzOyBjaG9vc2UgYSBmb250IGNvbG9yIHN1aXRhYmxlIGZvciBib3RoIGRhcmsgYW5kIGxpZ2h0IG1vZGVzLiAgCkFkZGl0aW9uYWwgZWZmZWN0OiBUaGUgdGltZSB0YWcgc2hvdWxkIHNoYWtlIHNsaWdodGx5IHdoZW4gaG92ZXJlZCBvdmVyLg==',
  150. },
  151. },
  152. }
  153. static Hook = {
  154. // APP 使用 SystemConfig.Main.WindowRegisterKey 作为键绑定到 window 对象上
  155. // Hook 使用 SystemConfig.Hook.ApplicationRegisterKey 作为键绑定到 APP 对象上
  156. // 即如果要调用钩子 foo() 方法,应该使用 window.ChatGPTWithDate.hooks.foo()
  157. ApplicationRegisterKey: 'hooks',
  158. }
  159. // GM 存储的键
  160. static GMStorageKey = {
  161. UserConfig: 'ChatGPTWithDate-UserConfig',
  162. ConfigPanel: {
  163. Position: 'ChatGPTWithDate-ConfigPanel-Position', Size: 'ChatGPTWithDate-ConfigPanel-Size',
  164. },
  165. }
  166. }
  167.  
  168. class Utils {
  169.  
  170. /**
  171. * 按照模板格式化日期时间
  172. *
  173. * @param date Date 对象
  174. * @param template 模板,例如 '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}'
  175. * @returns string 格式化后的日期时间字符串
  176. */
  177. static formatDateTimeByDate(date, template) {
  178. const year = date.getFullYear();
  179. const month = date.getMonth() + 1;
  180. const day = date.getDate();
  181. const week = date.getDay();
  182. const hours = date.getHours();
  183. const minutes = date.getMinutes();
  184. const seconds = date.getSeconds();
  185. const milliseconds = date.getMilliseconds();
  186. const week2zh = ['', '一', '二', '三', '四', '五', '六', '日']
  187. const week2enFullName = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  188. const week2enShortName = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  189. const month2zh = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
  190. const month2enFullName = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
  191. const month2enShortName = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  192.  
  193. const getValueByKey = (key) => {
  194. switch (key) {
  195. case '{yyyy}':
  196. return year.toString();
  197. case '{yy}':
  198. return (year % 100).toString().padStart(2, '0');
  199.  
  200. case '{MM}':
  201. case '{MM:02}':
  202. return month.toString().padStart(2, '0');
  203. case '{MM:01}':
  204. return month.toString();
  205. case '{MM#name@zh}':
  206. return month2zh[month];
  207. case '{MM#name@en}':
  208. case '{MM#fullname@en}':
  209. return month2enFullName[month];
  210. case '{MM#shortname@en}':
  211. return month2enShortName[month];
  212.  
  213. case '{dd}':
  214. case '{dd:02}':
  215. return day.toString().padStart(2, '0');
  216. case '{dd:01}':
  217. return day.toString();
  218.  
  219. case '{HH}':
  220. case '{HH:02}':
  221. case '{HH#24}':
  222. case '{HH#24:02}':
  223. return hours.toString().padStart(2, '0');
  224. case '{HH:01}':
  225. case '{HH#24:01}':
  226. return hours.toString();
  227. case '{HH#12}':
  228. case '{HH#12:02}':
  229. return (hours % 12 || 12).toString().padStart(2, '0');
  230. case '{HH#12:01}':
  231. return (hours % 12 || 12).toString();
  232. case '{HH#tag}':
  233. case '{HH#tag@en}':
  234. return hours >= 12 ? 'PM' : 'AM';
  235. case '{HH#tag@zh}':
  236. return hours >= 12 ? '下午' : '上午';
  237.  
  238. case '{mm}':
  239. case '{mm:02}':
  240. return minutes.toString().padStart(2, '0');
  241. case '{mm:01}':
  242. return minutes.toString();
  243.  
  244. case '{ss}':
  245. case '{ss:02}':
  246. return seconds.toString().padStart(2, '0');
  247. case '{ss:01}':
  248. return seconds.toString();
  249.  
  250. case '{ms}':
  251. return milliseconds.toString().padStart(3, '0');
  252.  
  253.  
  254. case '{week}':
  255. case '{week:02}':
  256. return week.toString().padStart(2, '0');
  257. case '{week:01}':
  258. return week.toString();
  259. case '{week#name@zh}':
  260. return week2zh[week];
  261. case '{week#name@en}':
  262. case '{week#fullname@en}':
  263. return week2enFullName[week];
  264. case '{week#shortname@en}':
  265. return week2enShortName[week];
  266. default:
  267. return key;
  268. }
  269. }
  270. return template.replace(/\{[^}]+\}/g, match => getValueByKey(match));
  271. }
  272.  
  273. /**
  274. * 深度合并两个对象,将源对象的属性合并到目标对象中,如果属性值为对象则递归合并。
  275. *
  276. * @param target 目标对象
  277. * @param source 源对象
  278. * @returns {*}
  279. */
  280. static deepMerge(target, source) {
  281. if (!source) return target
  282. // 遍历源对象的所有属性
  283. Object.keys(source).forEach(key => {
  284. if (source[key] && typeof source[key] === 'object') {
  285. // 如果源属性是一个对象且目标中也存在同名属性且为对象,则递归合并
  286. if (target[key] && typeof target[key] === 'object') {
  287. Utils.deepMerge(target[key], source[key]);
  288. } else {
  289. // 否则直接复制(对于源中的对象,需要进行深拷贝)
  290. target[key] = JSON.parse(JSON.stringify(source[key]));
  291. }
  292. } else {
  293. // 非对象属性直接复制
  294. target[key] = source[key];
  295. }
  296. });
  297. return target;
  298. }
  299.  
  300. /**
  301. * 检查依赖关系图(有向图)是否有循环依赖,如果没有就返回一个先后顺序(即按照此顺序实例化不会出现依赖项为空的情况)。
  302. * 给定依赖关系图为此结构[{node:'ComponentClass0', dependencies:['ComponentClass2', 'ComponentClass3']}, ...]
  303. * @param dependencyGraph 依赖关系图
  304. * @returns {*[]}
  305. */
  306. static dependencyAnalysis(dependencyGraph) {
  307. // 创建一个映射每个节点到其入度的对象
  308. const inDegree = {};
  309. const graph = {};
  310. const order = [];
  311.  
  312. // 初始化图和入度表
  313. dependencyGraph.forEach(item => {
  314. const {node, dependencies} = item;
  315. if (!graph[node]) {
  316. graph[node] = [];
  317. inDegree[node] = 0;
  318. }
  319. dependencies.forEach(dependentNode => {
  320. if (!graph[dependentNode]) {
  321. graph[dependentNode] = [];
  322. inDegree[dependentNode] = 0;
  323. }
  324. graph[dependentNode].push(node);
  325. inDegree[node]++;
  326. });
  327. });
  328.  
  329. // 将所有入度为0的节点加入到队列中
  330. const queue = [];
  331. for (const node in inDegree) {
  332. if (inDegree[node] === 0) {
  333. queue.push(node);
  334. }
  335. }
  336.  
  337. // 处理队列中的节点
  338. while (queue.length) {
  339. const current = queue.shift();
  340. order.push(current);
  341. graph[current].forEach(neighbour => {
  342. inDegree[neighbour]--;
  343. if (inDegree[neighbour] === 0) {
  344. queue.push(neighbour);
  345. }
  346. });
  347. }
  348.  
  349. // 如果排序后的节点数量不等于图中的节点数量,说明存在循环依赖
  350. if (order.length !== Object.keys(graph).length) {
  351. // 找到循环依赖的节点
  352. const cycleNodes = [];
  353. for (const node in inDegree) {
  354. if (inDegree[node] !== 0) {
  355. cycleNodes.push(node);
  356. }
  357. }
  358. throw new Error("存在循环依赖的节点:" + cycleNodes.join(","));
  359. }
  360. return order;
  361. }
  362.  
  363. /**
  364. * 将文本转换为 base64 编码,兼容中文
  365. * @param text
  366. * @returns {string}
  367. */
  368. static base64Encode(text) {
  369. const encodedUriComponent = encodeURIComponent(text);
  370. const binaryString = unescape(encodedUriComponent);
  371. return btoa(binaryString);
  372. }
  373.  
  374. /**
  375. * 将 base64 编码的文本解码,兼容中文
  376. * @param encoded
  377. * @returns {string}
  378. */
  379. static base64Decode(encoded) {
  380. const binaryString = atob(encoded);
  381. const encodedUriComponent = escape(binaryString);
  382. return decodeURIComponent(encodedUriComponent);
  383. }
  384.  
  385. /**
  386. * 判断 JavaScript 代码字符串是否合法
  387. * @param code
  388. * @returns {{valid: boolean, error: *}}
  389. */
  390. static isJavaScriptSyntaxValid(code) {
  391. try {
  392. new Function(code);
  393. return {
  394. valid: true,
  395. error: null
  396. };
  397. } catch (e) {
  398. return {
  399. valid: false,
  400. error: e
  401. };
  402. }
  403. }
  404. }
  405.  
  406. class Logger {
  407. static EnableLog = true
  408. static EnableDebug = false
  409. static EnableInfo = true
  410. static EnableWarn = true
  411. static EnableError = true
  412. static EnableTable = false
  413.  
  414. static prefix(type = 'INFO') {
  415. const timeFormat = Utils.formatDateTimeByDate(new Date(), SystemConfig.Logger.TimeFormatTemplate);
  416. return `[${timeFormat}] - [${SystemConfig.Common.ApplicationName}] - [${type}]`
  417. }
  418.  
  419. static log(...args) {
  420. if (Logger.EnableLog) {
  421. console.log(Logger.prefix('INFO'), ...args);
  422. }
  423. }
  424.  
  425. static debug(...args) {
  426. if (Logger.EnableDebug) {
  427. console.debug(Logger.prefix('DEBUG'), ...args);
  428. }
  429. }
  430.  
  431. static info(...args) {
  432. if (Logger.EnableInfo) {
  433. console.info(Logger.prefix('INFO'), ...args);
  434. }
  435. }
  436.  
  437. static warn(...args) {
  438. if (Logger.EnableWarn) {
  439. console.warn(Logger.prefix('WARN'), ...args);
  440. }
  441. }
  442.  
  443. static error(...args) {
  444. if (Logger.EnableError) {
  445. console.error(Logger.prefix('ERROR'), ...args);
  446. }
  447. }
  448.  
  449. static table(...args) {
  450. if (Logger.EnableTable) {
  451. console.table(...args);
  452. }
  453. }
  454. }
  455.  
  456. class MessageBO {
  457. /**
  458. * 消息业务对象
  459. *
  460. * @param messageId 消息ID,为消息元素的data-message-id属性值
  461. * @param role 角色
  462. * system: 表示系统消息,并不属于聊天内容。 从 API 获取
  463. * tool: 也表示系统消息。 从 API 获取
  464. * assistant: 表示 ChatGPT 回答的消息。 从 API 获取
  465. * user: 表示用户输入的消息。 从 API 获取
  466. * You: 表示用户输入的消息。 从页面实时获取
  467. * ChatGPT: 表示 ChatGPT 回答的消息。 从页面实时获取
  468. * @param timestamp 时间戳,浮点数或整数类型,单位毫秒,例如 1714398759.26881、1714398759
  469. * @param message 消息内容
  470. */
  471. constructor(messageId, role, timestamp, message = '') {
  472. this.messageId = messageId;
  473. this.role = role;
  474. this.timestamp = timestamp;
  475. this.message = message;
  476. }
  477. }
  478.  
  479. class MessageElementBO {
  480. /**
  481. * 消息元素业务对象
  482. *
  483. * @param rootEle 消息的根元素,也就是 messageEle 的父节点
  484. * @param messageEle 消息元素,包含 data-message-id 属性
  485. * 例如 <div data-message-id="123456">你好</div>
  486. */
  487. constructor(rootEle, messageEle) {
  488. this.rootEle = rootEle;
  489. this.messageEle = messageEle;
  490. }
  491. }
  492.  
  493. class Component {
  494.  
  495. constructor() {
  496. this.dependencies = []
  497. Object.defineProperty(this, 'initDependencies', {
  498. value: function () {
  499. this.dependencies.forEach(dependency => {
  500. this[dependency.field] = ComponentLocator.get(dependency.clazz)
  501. })
  502. }, writable: false, // 防止方法被修改
  503. configurable: false // 防止属性被重新定义或删除
  504. });
  505. }
  506.  
  507. init() {
  508. }
  509. }
  510.  
  511. class ComponentLocator {
  512. /**
  513. * 组件注册(不可用)器,用于注册(不可用)和获取组件
  514. */
  515. static components = {};
  516.  
  517. /**
  518. * 注册(不可用)组件,要求组件为 Component 的子类
  519. *
  520. * @param clazz Component 的子类
  521. * @param instance Component 的子类的实例化对象,必顧是 clazz 的实例
  522. * @returns obj 返回注册(不可用)的实例化对象
  523. */
  524. static register(clazz, instance) {
  525. if (!(instance instanceof Component)) {
  526. throw new Error(`实例化对象 ${instance} 不是 Component 的实例。`);
  527. }
  528. if (!(instance instanceof clazz)) {
  529. throw new Error(`实例化对象 ${instance} 不是 ${clazz} 的实例。`);
  530. }
  531. if (ComponentLocator.components[clazz.name]) {
  532. throw new Error(`组件 ${clazz.name} 已经注册(不可用)过了。`);
  533. }
  534. ComponentLocator.components[clazz.name] = instance;
  535. return instance
  536. }
  537.  
  538. /**
  539. * 获取组件,用于完成组件之间的依赖注入
  540. *
  541. * @param clazz Component 的子类
  542. * @returns {*} 返回注册(不可用)的实例化对象
  543. */
  544. static get(clazz) {
  545. if (!ComponentLocator.components[clazz.name]) {
  546. throw new Error(`组件 ${clazz.name} 未注册(不可用)。`);
  547. }
  548. return ComponentLocator.components[clazz.name];
  549. }
  550. }
  551.  
  552. class UserConfig extends Component {
  553.  
  554. init() {
  555. const defaultConfig = this.getDefaultConfig()
  556. this.timeRender = defaultConfig.timeRender
  557. this.i18n = defaultConfig.i18n
  558. const userConfig = this.load()
  559. if (userConfig) {
  560. // 不调用 update 方法,因为 update 方法会保存配置
  561. // 为了 (开发者)调试 方便,不保存配置
  562. // 为了 (用户)性能 考虑,不保存配置
  563. Utils.deepMerge(this.timeRender, userConfig.timeRender)
  564. if (userConfig.i18n) this.i18n = userConfig.i18n
  565. }
  566. }
  567.  
  568. getDefaultConfig() {
  569. return {
  570. timeRender: {
  571. format: SystemConfig.TimeRender.TimeTagTemplates[0],
  572. advanced: {
  573. enable: false,
  574. htmlTextContent: `<span class="time-tag">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  575. styleTextContent: `.time-tag {
  576. padding-right: 1rem;
  577. color: #ababab;
  578. font-size: 0.9em;
  579. }`,
  580. scriptTextContent: `(() => {
  581. 'use strict';
  582. window.ChatGPTWithDate.hooks.formatDateTimeByDate = (date, template) => {
  583. const formatValue = (value, format) => value.toString().padStart(format === 'yyyy' ? 4 : 2, '0');
  584. const dateValues = {
  585. '{yyyy}': date.getFullYear(),
  586. '{MM}': date.getMonth() + 1,
  587. '{dd}': date.getDate(),
  588. '{HH}': date.getHours(),
  589. '{mm}': date.getMinutes(),
  590. '{ss}': date.getSeconds()
  591. };
  592. return template.replace(/\\{[^}]+\\}/g, match => formatValue(dateValues[match], match.slice(1, -1)));
  593. }
  594. window.ChatGPTWithDate.hooks.afterCreateTimeTag = (messageId, timeTagContainerNode) => {
  595. const timeTagNode = timeTagContainerNode.querySelector('.time-tag');
  596. const dateText = timeTagNode.innerText;
  597. const date = new Date(dateText);
  598. timeTagNode.addEventListener('mouseover', () => {
  599. timeTagNode.innerText = Math.floor((new Date() - date) / 86400000) + ' days ago';
  600. });
  601. timeTagNode.addEventListener('mouseout', () => {
  602. timeTagNode.innerText = dateText;
  603. });
  604. }
  605. })()`,
  606. }
  607. },
  608. i18n: SystemConfig.ConfigPanel.I18N.default
  609. }
  610. }
  611.  
  612. restore() {
  613. this.timeRender = this.getDefaultConfig().timeRender
  614. this.i18n = SystemConfig.ConfigPanel.I18N.default
  615. this.save()
  616. }
  617.  
  618. save() {
  619. GM_setValue(SystemConfig.GMStorageKey.UserConfig, {
  620. timeRender: this.timeRender,
  621. i18n: this.i18n
  622. })
  623. }
  624.  
  625. load() {
  626. return GM_getValue(SystemConfig.GMStorageKey.UserConfig)
  627. }
  628.  
  629. /**
  630. * 更新配置并保存
  631. * @param newConfig 新的配置
  632. */
  633. update(newConfig) {
  634. Utils.deepMerge(this.timeRender, newConfig.timeRender)
  635. if (newConfig.i18n) this.i18n = newConfig.i18n
  636. this.save()
  637. }
  638.  
  639. /**
  640. * 更新一个配置项并保存
  641. * @param key 配置项的 key
  642. * @param value 配置项的 value
  643. */
  644. updateOne(key, value) {
  645. if (this[key] instanceof Object) {
  646. Utils.deepMerge(this[key], value)
  647. } else {
  648. this[key] = value
  649. }
  650. this.save()
  651. }
  652. }
  653.  
  654. class HookService extends Component {
  655.  
  656. init() {
  657. this.defaultHooks = {
  658. beforeCreateTimeTag: (messageId, timeTagHTML) => {
  659. },
  660. afterCreateTimeTag: (messageId, timeTagNode) => {
  661. },
  662. formatDateTimeByDate: Utils.formatDateTimeByDate
  663. }
  664. this.reset2DefaultHooks()
  665. }
  666.  
  667. _checkOldVersion(hookName) {
  668. if (unsafeWindow[hookName]) {
  669. Logger.warn(`钩子函数 ${hookName} 不应该绑定在 window 对象上,请绑定在 window.${SystemConfig.Main.WindowRegisterKey}.${SystemConfig.Hook.ApplicationRegisterKey} 上!未来版本中将不再兼容此情况。`)
  670. return true
  671. }
  672. return false
  673. }
  674.  
  675. _register2Window() {
  676. unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.Hook.ApplicationRegisterKey] = {
  677. beforeCreateTimeTag: this.hooks.beforeCreateTimeTag,
  678. afterCreateTimeTag: this.hooks.afterCreateTimeTag,
  679. formatDateTimeByDate: this.hooks.formatDateTimeByDate
  680. }
  681. for (let hookName in this.defaultHooks) {
  682. if (this._checkOldVersion(hookName)) {
  683. unsafeWindow[hookName] = this.defaultHooks[hookName]
  684. }
  685. }
  686. }
  687.  
  688. reset2DefaultHooks() {
  689. this.hooks = this.defaultHooks
  690. this._register2Window()
  691. }
  692.  
  693. invokeHook(hookName, ...args) {
  694. if (!this.defaultHooks[hookName]) {
  695. Logger.error(`钩子函数 ${hookName} 非法`)
  696. return
  697. }
  698. if (this._checkOldVersion(hookName)) {
  699. unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.Hook.ApplicationRegisterKey][hookName] = unsafeWindow[hookName]
  700. }
  701. try {
  702. return unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.Hook.ApplicationRegisterKey][hookName](...args)
  703. } catch (e) {
  704. Logger.error(`调用钩子函数 ${hookName} 失败:`, e)
  705. }
  706. }
  707. }
  708.  
  709. class StyleService extends Component {
  710. init() {
  711. this.styles = new Map()
  712. }
  713.  
  714. /**
  715. * 更新样式
  716. *
  717. * @param key 样式的 key,字符串对象
  718. * @param styleContent 样式,字符串对象
  719. */
  720. updateStyle(key, styleContent) {
  721. this.removeStyle(key)
  722. const styleNode = document.createElement('style')
  723. styleNode.textContent = styleContent
  724. document.head.appendChild(styleNode)
  725. this.styles.set(key, styleNode)
  726. }
  727.  
  728. /**
  729. * 移除样式
  730. *
  731. * @param key 样式的 key,字符串对象
  732. */
  733. removeStyle(key) {
  734. let styleNode = this.styles.get(key)
  735. if (styleNode) {
  736. document.head.removeChild(styleNode)
  737. this.styles.delete(key)
  738. }
  739. }
  740. }
  741.  
  742. class JavaScriptService extends Component {
  743. init() {
  744. this.javaScriptNodes = new Map()
  745. }
  746.  
  747. updateJavaScript(key, textContent) {
  748. this.removeJavaScript(key)
  749. const scriptNode = GM_addElement('script', {
  750. textContent: textContent
  751. });
  752. this.javaScriptNodes.set(key, scriptNode)
  753. }
  754.  
  755. removeJavaScript(key) {
  756. let scriptNode = this.javaScriptNodes.get(key)
  757. if (scriptNode) {
  758. document.head.removeChild(scriptNode)
  759. this.javaScriptNodes.delete(key)
  760. }
  761. }
  762. }
  763.  
  764. class MessageService extends Component {
  765. init() {
  766. this.messages = new Map();
  767. }
  768.  
  769. /**
  770. * 解析消息元素,获取消息的所有内容。由于网页中不存在时间戳,所以时间戳使用当前时间代替。
  771. * 调用该方法只需要消息元素,一般用于从页面实时监测获取到的消息。
  772. *
  773. * @param messageDiv 消息元素,包含 data-message-id 属性 的 div 元素
  774. * @returns {MessageBO|undefined} 返回消息业务对象,如果消息元素不存在则返回 undefined
  775. */
  776. parseMessageDiv(messageDiv) {
  777. if (!messageDiv) {
  778. return;
  779. }
  780. const messageId = messageDiv.getAttribute('data-message-id');
  781. const role = messageDiv.getAttribute('data-message-author-role');
  782. const messageElementBO = this.getMessageElement(messageId)
  783. if (!messageElementBO) {
  784. return;
  785. }
  786. let timestamp = new Date().getTime();
  787. const message = messageElementBO.messageEle.innerHTML;
  788. if (!this.messages.has(messageId)) {
  789. const messageBO = new MessageBO(messageId, role, timestamp, message);
  790. this.addMessage(messageBO)
  791. }
  792. return this.messages.get(messageId);
  793. }
  794.  
  795. /**
  796. * 添加消息,主要用于添加从 API 劫持到的消息列表。
  797. * 调用该方法需要已知消息的所有内容,如果只知道消息元素则应该使用 parseMessageDiv 方法获取消息业务对象。
  798. *
  799. * @param message 消息业务对象
  800. * @param force 是否强制添加,如果为 true 则强制添加,否则如果消息已经存在则不添加
  801. * @returns {boolean} 返回是否添加成功
  802. */
  803. addMessage(message, force = false) {
  804. if (this.messages.has(message.messageId) && !force) {
  805. return false;
  806. }
  807. this.messages.set(message.messageId, message);
  808. return true
  809. }
  810.  
  811. /**
  812. * 移除消息
  813. *
  814. * @param messageId 消息 ID
  815. * @returns {boolean} 返回是否移除成功
  816. */
  817. removeMessage(messageId) {
  818. return this.messages.delete(messageId)
  819. }
  820.  
  821. /**
  822. * 通过消息 ID 获取消息元素业务对象
  823. *
  824. * @param messageId 消息 ID
  825. * @returns {MessageElementBO|undefined} 返回消息元素业务对象
  826. */
  827. getMessageElement(messageId) {
  828. const messageDiv = document.body.querySelector(`div[data-message-id="${messageId}"]`);
  829. if (!messageDiv) {
  830. return;
  831. }
  832. const rootDiv = messageDiv.parentElement;
  833. return new MessageElementBO(rootDiv, messageDiv);
  834. }
  835.  
  836. /**
  837. * 通过消息 ID 获取消息业务对象
  838. * @param messageId
  839. * @returns {any}
  840. */
  841. getMessage(messageId) {
  842. return this.messages.get(messageId);
  843. }
  844.  
  845. /**
  846. * 显示所有消息信息
  847. */
  848. showMessages() {
  849. Logger.table(Array.from(this.messages.values()));
  850. }
  851. }
  852.  
  853. class MonitorService extends Component {
  854. constructor() {
  855. super();
  856. this.messageService = null
  857. this.timeRendererService = null
  858. this.dependencies = [{field: 'messageService', clazz: MessageService}, {
  859. field: 'timeRendererService', clazz: TimeRendererService
  860. },]
  861. }
  862.  
  863. init() {
  864. this.totalTime = 0;
  865. this.originalFetch = window.fetch;
  866. this._initMonitorFetch();
  867. this._initMonitorAddedMessageNode();
  868. this._initConfigPageNode();
  869. this._initSharePageDataFetch();
  870. }
  871.  
  872. /**
  873. * 初始化劫持 fetch 方法,用于监控 ChatGPT 的消息数据
  874. *
  875. * @private
  876. */
  877. _initMonitorFetch() {
  878. const that = this;
  879. const urlRegex = new RegExp("^https://(chat\\.openai|chatgpt)\\.com/backend-api/conversation/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
  880. unsafeWindow.fetch = (...args) => {
  881. return that.originalFetch.apply(this, args)
  882. .then(response => {
  883. if (urlRegex.test(response.url)) {
  884. // 克隆响应对象以便独立处理响应体
  885. const clonedResponse = response.clone();
  886. clonedResponse.json().then(data => {
  887. that._parseConversationJsonData(data);
  888. }).catch(error => Logger.error('解析响应体失败:', error));
  889. }
  890. return response;
  891. })
  892. };
  893. }
  894.  
  895. /**
  896. * 初始化获取分享界面的消息数据
  897. *
  898. * @private
  899. */
  900. _initSharePageDataFetch() {
  901. const __NEXT_DATA__ = document.querySelector("#__NEXT_DATA__")
  902. if (!__NEXT_DATA__) return;
  903. const jsonData = JSON.parse(__NEXT_DATA__.text);
  904. try {
  905. this._parseConversationJsonData(jsonData.props.pageProps.serverResponse.data)
  906. } catch (e) {
  907. Logger.error('解析分享页面数据失败:', e)
  908. }
  909. }
  910.  
  911. /**
  912. * 解析从 API 获取到的消息数据,该方法存在报错风险,需要在调用时捕获异常以防止中断后续操作。
  913. *
  914. * @param obj 从 API 获取到的消息数据
  915. * @private
  916. */
  917. _parseConversationJsonData(obj) {
  918. const mapping = obj.mapping
  919. const messageIds = []
  920. for (let key in mapping) {
  921. const message = mapping[key].message
  922. if (message) {
  923. const messageId = message.id
  924. const role = message.author.role
  925. const createTime = message.create_time
  926. const messageBO = new MessageBO(messageId, role, createTime * 1000)
  927. messageIds.push(messageId)
  928. this.messageService.addMessage(messageBO, true)
  929. }
  930. }
  931. this.timeRendererService.addMessageArrayToBeRendered(messageIds.reverse())
  932. this.messageService.showMessages()
  933. }
  934.  
  935. /**
  936. * 初始化监控节点变化,用于监控在使用 ChatGPT 期间实时输入的消息。
  937. * 每隔 500ms 检查一次 main 节点是否存在,如果存在则开始监控节点变化。
  938. * @private
  939. */
  940. _initMonitorAddedMessageNode() {
  941. const interval = setInterval(() => {
  942. const mainElement = document.querySelector('main');
  943. if (mainElement) {
  944. this._setupMonitorAddedMessageNode(mainElement);
  945. clearInterval(interval); // 清除定时器,停止进一步检查
  946. }
  947. }, 500);
  948. }
  949.  
  950. /**
  951. * 设置监控节点变化,用于监控在使用 ChatGPT 期间实时输入的消息。
  952. * @param supervisedNode 监控在此节点下的节点变化,确保新消息的节点在此节点下
  953. * @private
  954. */
  955. _setupMonitorAddedMessageNode(supervisedNode) {
  956. const that = this;
  957. const callback = function (mutationsList, observer) {
  958. const start = new Date().getTime();
  959. for (const mutation of mutationsList) {
  960. let messageDiv = null;
  961. if (mutation.type === 'childList') {
  962. for (let node of mutation.addedNodes) {
  963. if (node.nodeType === Node.ELEMENT_NODE) {
  964. messageDiv = node.querySelector('div[data-message-id]');
  965. if (!messageDiv && node.hasAttribute('data-message-id')) {
  966. messageDiv = node
  967. break
  968. }
  969. }
  970. }
  971. } else if (mutation.type === 'attributes' && mutation.attributeName === 'data-message-id') {
  972. messageDiv = mutation.target;
  973. }
  974. if (messageDiv !== null) {
  975. const messageBO = that.messageService.parseMessageDiv(messageDiv);
  976. if (messageBO) {
  977. that.timeRendererService.addMessageToBeRendered(messageBO.messageId);
  978. that.messageService.showMessages()
  979. }
  980. }
  981. }
  982. const end = new Date().getTime();
  983. that.totalTime += (end - start);
  984. };
  985. const observer = new MutationObserver(callback);
  986. observer.observe(supervisedNode, {childList: true, subtree: true, attributes: true});
  987. }
  988.  
  989. /**
  990. * 初始化配置页面节点,以便显示配置页面的时间渲染效果
  991. * @private
  992. */
  993. _initConfigPageNode() {
  994. if (IsConfigPage) {
  995. const messageIds = []
  996. const messageDivs = document.querySelectorAll('div[data-message-id]');
  997. for (let messageDiv of messageDivs) {
  998. const dataMessageId = messageDiv.getAttribute('data-message-id');
  999. const timestamp = parseInt(messageDiv.getAttribute('data-chatgpt-with-date-demo-timestamp'))
  1000. const messageBO = new MessageBO(dataMessageId, 'ConfigDemo', timestamp)
  1001. messageIds.push(dataMessageId)
  1002. this.messageService.addMessage(messageBO, true)
  1003. }
  1004. this.timeRendererService.addMessageArrayToBeRendered(messageIds)
  1005. }
  1006. }
  1007. }
  1008.  
  1009. class TimeRendererService extends Component {
  1010.  
  1011. constructor() {
  1012. super();
  1013. this.messageService = null
  1014. this.userConfig = null
  1015. this.styleService = null
  1016. this.javaScriptService = null
  1017. this.hookService = null
  1018. this.dependencies = [
  1019. {field: 'messageService', clazz: MessageService},
  1020. {field: 'userConfig', clazz: UserConfig},
  1021. {field: 'styleService', clazz: StyleService},
  1022. {field: 'javaScriptService', clazz: JavaScriptService},
  1023. {field: 'hookService', clazz: HookService},
  1024. ]
  1025. }
  1026.  
  1027. init() {
  1028. this.messageToBeRendered = []
  1029. this.messageCountOfFailedToRender = new Map()
  1030. this._setStyleAndJavaScript()
  1031. this._initRender()
  1032. }
  1033.  
  1034. /**
  1035. * 若为高级模式则设置用户自定义的样式和脚本,否则设置默认样式
  1036. *
  1037. * @private
  1038. */
  1039. _setStyleAndJavaScript() {
  1040. this.styleService.updateStyle(SystemConfig.TimeRender.BasicStyleKey, SystemConfig.TimeRender.BasicStyle)
  1041. this.hookService.reset2DefaultHooks()
  1042. this.styleService.removeStyle(SystemConfig.TimeRender.AdditionalStyleKey)
  1043. this.javaScriptService.removeJavaScript(SystemConfig.TimeRender.AdditionalScriptKey)
  1044. if (this.userConfig.timeRender.advanced.enable) {
  1045. this.styleService.updateStyle(SystemConfig.TimeRender.AdditionalStyleKey, this.userConfig.timeRender.advanced.styleTextContent)
  1046. this.javaScriptService.updateJavaScript(SystemConfig.TimeRender.AdditionalScriptKey, this.userConfig.timeRender.advanced.scriptTextContent)
  1047. }
  1048. }
  1049.  
  1050. /**
  1051. * 添加消息 ID 到待渲染队列
  1052. * @param messageId 消息 ID
  1053. */
  1054. addMessageToBeRendered(messageId) {
  1055. if (typeof messageId !== 'string') {
  1056. return
  1057. }
  1058. this.messageToBeRendered.push(messageId)
  1059. Logger.debug(`添加ID ${messageId} 到待渲染队列,当前队列 ${this.messageToBeRendered}`)
  1060. }
  1061.  
  1062. /**
  1063. * 添加消息 ID 到待渲染队列
  1064. * @param messageIdArray 消息 ID数组
  1065. */
  1066. addMessageArrayToBeRendered(messageIdArray) {
  1067. if (!messageIdArray || !(messageIdArray instanceof Array)) {
  1068. return
  1069. }
  1070. this.messageToBeRendered.push(...messageIdArray)
  1071. Logger.debug(`添加ID ${messageIdArray} 到待渲染队列,当前队列 ${this.messageToBeRendered}`)
  1072. }
  1073.  
  1074. /**
  1075. * 初始化渲染时间的定时器,每隔 SystemConfig.TimeRender.Interval 毫秒处理一次待渲染队列
  1076. * 1. 备份待渲染队列
  1077. * 2. 清空待渲染队列
  1078. * 3. 遍历备份的队列,逐个渲染
  1079. * 3.1 如果渲染失败则重新加入待渲染队列,失败次数加一
  1080. * 3.2 如果渲染成功,清空失败次数
  1081. * 4. 重复 1-3 步骤
  1082. * 5. 如果失败次数超过 SystemConfig.TimeRender.RenderRetryCount 则不再尝试渲染,即不再加入待渲染队列。同时清空失败次数。
  1083. *
  1084. * @private
  1085. */
  1086. _initRender() {
  1087. const that = this
  1088.  
  1089. async function processTimeRender() {
  1090. const start = new Date().getTime();
  1091. let completeCount = 0;
  1092. let totalCount = that.messageToBeRendered.length;
  1093. const messageToBeRenderedClone = that.messageToBeRendered.slice()
  1094. that.messageToBeRendered = []
  1095. let count = 0;
  1096.  
  1097. for (let messageId of messageToBeRenderedClone) {
  1098. count++;
  1099. if (count <= SystemConfig.TimeRender.BatchSize && new Date().getTime() - start <= SystemConfig.TimeRender.BatchTimeout) {
  1100. const result = await that._renderTime(messageId)
  1101. if (!result) {
  1102. let countOfFailed = that.messageCountOfFailedToRender.get(messageId)
  1103. if (countOfFailed && countOfFailed >= SystemConfig.TimeRender.RenderRetryCount) {
  1104. Logger.debug(`ID ${messageId} 渲染失败次数超过 ${SystemConfig.TimeRender.RenderRetryCount} 次,将不再尝试。`)
  1105. that.messageCountOfFailedToRender.delete(messageId)
  1106. } else {
  1107. that.messageToBeRendered.push(messageId);
  1108. if (countOfFailed) {
  1109. that.messageCountOfFailedToRender.set(messageId, countOfFailed + 1)
  1110. } else {
  1111. that.messageCountOfFailedToRender.set(messageId, 1)
  1112. }
  1113. }
  1114. } else {
  1115. completeCount++
  1116. that.messageCountOfFailedToRender.delete(messageId)
  1117. }
  1118. Logger.debug(`ID ${messageId} 渲染${result ? '成功' : '失败'},当前渲染进度 ${completeCount}/${totalCount},该批次耗时 ${new Date().getTime() - start}ms`)
  1119. } else {
  1120. for (let i = count; i < messageToBeRenderedClone.length; i++) {
  1121. that.messageToBeRendered.push(messageToBeRenderedClone[i])
  1122. }
  1123. if (count > SystemConfig.TimeRender.BatchSize) {
  1124. Logger.debug(`本批次渲染数量超过 ${SystemConfig.TimeRender.BatchSize},将继续下一批次渲染。`)
  1125. break;
  1126. }
  1127. if (new Date().getTime() - start > SystemConfig.TimeRender.BatchTimeout) {
  1128. Logger.debug(`本批次渲染超时,将继续下一批次渲染。`)
  1129. break;
  1130. }
  1131. }
  1132. }
  1133. const end = new Date().getTime();
  1134. if (totalCount > 0) {
  1135. Logger.debug(`处理当前ID队列渲染 ${messageToBeRenderedClone} 耗时 ${end - start}ms`)
  1136. }
  1137. setTimeout(processTimeRender, SystemConfig.TimeRender.Interval);
  1138. }
  1139.  
  1140. processTimeRender().then(r => Logger.debug('初始化渲染时间定时器完成'))
  1141. }
  1142.  
  1143. /**
  1144. * 将时间渲染到目标位置,如果检测到目标位置已经存在时间元素则更新时间,否则创建时间元素并插入到目标位置。
  1145. *
  1146. * @param messageId 消息 ID
  1147. * @returns {Promise} 返回是否渲染成功的 Promise 对象
  1148. * @private
  1149. */
  1150. _renderTime(messageId) {
  1151. return new Promise(resolve => {
  1152. const messageElementBo = this.messageService.getMessageElement(messageId);
  1153. const messageBo = this.messageService.getMessage(messageId);
  1154. if (!messageElementBo || !messageBo) resolve(false)
  1155. const timeElement = messageElementBo.rootEle.querySelector(`.${SystemConfig.TimeRender.TimeClassName}`);
  1156. const role = messageElementBo.messageEle.getAttribute('data-message-author-role');
  1157. const element = this._createTimeElement(messageBo.timestamp, role);
  1158. // 强制移除时间元素,重新渲染。这样才能保证时间正确的同时也能正确执行用户自定义的脚本。
  1159. if (timeElement) {
  1160. messageElementBo.rootEle.removeChild(timeElement)
  1161. }
  1162. this.hookService.invokeHook('beforeCreateTimeTag', messageId, element.timeTagContainer)
  1163. messageElementBo.rootEle.firstChild.insertAdjacentHTML('beforebegin', element.timeTagContainer);
  1164. this.hookService.invokeHook('afterCreateTimeTag', messageId, messageElementBo.rootEle.querySelector(`.${SystemConfig.TimeRender.TimeClassName}`))
  1165. resolve(true)
  1166. })
  1167. }
  1168.  
  1169. /**
  1170. * 创建时间元素,如果开启高级模式则使用用户自定义的时间格式,否则使用默认时间格式。
  1171. *
  1172. * @param timestamp 时间戳,浮点数或整数类型,单位毫秒,例如 1714398759.26881、1714398759
  1173. * @returns {{timeTagFormated, timeTagContainer: string}} 返回格式化后的时间标签 和 包含时间标签的容器的 HTML 字符串
  1174. * @private
  1175. */
  1176. _createTimeElement(timestamp, role) {
  1177. let timeTagFormated = ''
  1178. if (this.userConfig.timeRender.advanced.enable) {
  1179. timeTagFormated = this.hookService.invokeHook('formatDateTimeByDate', new Date(timestamp), this.userConfig.timeRender.advanced.htmlTextContent)
  1180. } else {
  1181. timeTagFormated = this.hookService.invokeHook('formatDateTimeByDate', new Date(timestamp), this.userConfig.timeRender.format);
  1182. }
  1183. const timeTagContainer = `<span class="${SystemConfig.TimeRender.TimeClassName} ${role}">${timeTagFormated}</span>`;
  1184. return {
  1185. timeTagFormated, timeTagContainer,
  1186. };
  1187. }
  1188.  
  1189. /**
  1190. * 清除所有时间元素
  1191. * @private
  1192. */
  1193. _cleanAllTimeElements() {
  1194. const timeElements = document.body.querySelectorAll(`.${SystemConfig.TimeRender.TimeClassName}`);
  1195. timeElements.forEach(ele => {
  1196. ele.remove()
  1197. })
  1198. }
  1199.  
  1200. /**
  1201. * 重新渲染时间元素,强制拉取所有消息 ID 重新渲染
  1202. */
  1203. reRender() {
  1204. this._setStyleAndJavaScript()
  1205. this._cleanAllTimeElements()
  1206. this.addMessageArrayToBeRendered(Array.from(this.messageService.messages.keys()))
  1207. }
  1208. }
  1209.  
  1210. class ConfigPanelService extends Component {
  1211.  
  1212. constructor() {
  1213. super();
  1214. this.userConfig = null
  1215. this.styleService = null
  1216. this.timeRendererService = null
  1217. this.messageService = null
  1218. this.javascriptService = null
  1219. this.dependencies = [
  1220. {field: 'userConfig', clazz: UserConfig},
  1221. {field: 'styleService', clazz: StyleService},
  1222. {field: 'timeRendererService', clazz: TimeRendererService},
  1223. {field: 'messageService', clazz: MessageService},
  1224. {field: 'javascriptService', clazz: JavaScriptService},
  1225. ]
  1226. }
  1227.  
  1228. /**
  1229. * 初始化配置面板,强调每个子初始化方法阻塞式的执行,即一个初始化方法执行完毕后再执行下一个初始化方法。
  1230. * @returns {Promise<void>}
  1231. */
  1232. async init() {
  1233. if (IsConfigPage) {
  1234. // 仅在配置页面初始化配置面板,同时可以防止代码高亮插件对非配置页面的代码块进行处理
  1235. this.appID = SystemConfig.ConfigPanel.AppID
  1236. Logger.debug('开始初始化配置面板')
  1237. await this._initExternalResources()
  1238. Logger.debug('初始化脚本完成')
  1239. this._initVariables()
  1240. await this._initStyle()
  1241. Logger.debug('初始化样式完成')
  1242. await this._initPanel()
  1243. Logger.debug('初始化面板完成')
  1244. this._initVue()
  1245. Logger.debug('初始化Vue完成')
  1246. this._initConfigPanelSizeAndPosition()
  1247. this._initConfigPanelEventMonitor()
  1248. Logger.debug('初始化配置面板事件监控完成')
  1249. this.show()
  1250. }
  1251. this._initMenuCommand()
  1252. Logger.debug('初始化菜单命令完成')
  1253. }
  1254.  
  1255. /**
  1256. * 初始化配置面板的 HTML 与 Vue 实例的配置属性。集中管理以便方便修改。
  1257. * @private
  1258. */
  1259. _initVariables() {
  1260. const that = this
  1261. unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey] = {}
  1262. const TimeTagComponent = {
  1263. props: ['html'],
  1264. render() {
  1265. return Vue.h('div', {innerHTML: this.html});
  1266. },
  1267. }
  1268. this.panelStyle = `
  1269. .v-binder-follower-container {
  1270. position: fixed;
  1271. }
  1272. .n-button .n-button__content {
  1273. white-space: pre-wrap;
  1274. }
  1275. #CWD-Configuration-Panel {
  1276. position: absolute;
  1277. top: 50px;
  1278. left: 50px;
  1279. width: 600px;
  1280. background-color: #FFFFFF;
  1281. border: #D7D8D9 1px solid;
  1282. border-radius: 8px;
  1283. resize: horizontal;
  1284. min-width: 200px;
  1285. overflow: auto;
  1286. color: black;
  1287. opacity: 0.95;
  1288. }
  1289. #CWD-Configuration-Panel .status-bar {
  1290. cursor: move;
  1291. background-color: #ECECEA;
  1292. border-radius: 4px 4px 0 0;
  1293. display: flex;
  1294. }
  1295. #CWD-Configuration-Panel .status-bar .title {
  1296. display: flex;
  1297. align-items: center;
  1298. justify-content: left;
  1299. padding-left: 10px;
  1300. user-select: none;
  1301. color: #777;
  1302. flex: 1;
  1303. font-weight: bold;
  1304. }
  1305. #CWD-Configuration-Panel .status-bar .button {
  1306. cursor: pointer;
  1307. padding: 10px;
  1308. transition: color 0.3s;
  1309. }
  1310. #CWD-Configuration-Panel .status-bar .button:hover {
  1311. color: #f00;
  1312. }
  1313. #CWD-Configuration-Panel .container {
  1314. padding: 20px 20px 0;
  1315. }
  1316. #CWD-Configuration-Panel .container .code-block {
  1317. padding: 10px;
  1318. border: 1px solid #d9d9d9;
  1319. border-radius: 4px;
  1320. }
  1321. #CWD-Configuration-Panel .operation-group {
  1322. background-color: #ECECEA;
  1323. border-radius: 8px;
  1324. display: flex;
  1325. justify-content: center;
  1326. gap: 10px;
  1327. padding: 20px 0;
  1328. }
  1329. #CWD-Configuration-Panel .operation-group > button {
  1330. width: 30%;
  1331. }`
  1332. this.panelHTML = `
  1333. <div id="${that.appID}" style="visibility: hidden">
  1334. <n-config-provider :hljs="hljs" :locale="locale">
  1335. <div class="status-bar">
  1336. <div class="title" id="${that.appID}-DraggableArea">{{title}}</div>
  1337. <div class="button-group">
  1338. <n-popconfirm @positive-click="onRestore">
  1339. <template #trigger>
  1340. <n-tooltip trigger="hover">
  1341. <template #trigger>
  1342. <n-button class="button" text>
  1343. <n-icon size="20">
  1344. ${SystemConfig.ConfigPanel.Icon.Restore}
  1345. </n-icon>
  1346. </n-button>
  1347. </template>
  1348. <span>{{ map2text('restore-info') }}</span>
  1349. </n-tooltip>
  1350. </template>
  1351. <span>{{ map2text('restore-warn') }}</span>
  1352. </n-popconfirm>
  1353. <n-tooltip trigger="hover">
  1354. <template #trigger>
  1355. <n-button class="button" @click="toggleLanguage" text>
  1356. <n-icon size="20">
  1357. ${SystemConfig.ConfigPanel.Icon.Language}
  1358. </n-icon>
  1359. </n-button>
  1360. </template>
  1361. <span>{{ map2text('toggle-language-info') }}</span>
  1362. </n-tooltip>
  1363. <n-tooltip trigger="hover">
  1364. <template #trigger>
  1365. <n-button class="button" @click="openUrl('https://jiang-taibai.github.io/chatgpt-with-date/', '_blank')" text>
  1366. <n-icon size="20">
  1367. ${SystemConfig.ConfigPanel.Icon.Documentation}
  1368. </n-icon>
  1369. </n-button>
  1370. </template>
  1371. <span>{{ map2text('documentation-info') }}</span>
  1372. </n-tooltip>
  1373. <n-button class="button" @click="toggleFolding" text>
  1374. <n-icon v-if="folding" size="20">
  1375. ${SystemConfig.ConfigPanel.Icon.Maximize}
  1376. </n-icon>
  1377. <n-icon v-else size="20">
  1378. ${SystemConfig.ConfigPanel.Icon.Minimize}
  1379. </n-icon>
  1380. </n-button>
  1381. <n-button class="button" @click="onClose" text>
  1382. <n-icon size="20">
  1383. ${SystemConfig.ConfigPanel.Icon.Close}
  1384. </n-icon>
  1385. </n-button>
  1386. </div>
  1387. </div>
  1388. <div v-show="!folding">
  1389. <n-scrollbar style="max-height: 80vh">
  1390. <div class="container">
  1391. <n-form :label-width="i18n === 'zh' ? 40 : 80" :model="configForm" label-placement="left">
  1392. <n-form-item v-show="!configForm.advanced.enable" :label="map2text('template')" path="format">
  1393. <n-select v-model:value="configForm.format"
  1394. :disabled="configForm.advanced.enable" :render-label="renderLabel"
  1395. :options="formatOptions" @update:value="onConfigUpdate"></n-select>
  1396. </n-form-item>
  1397. <n-form-item v-show="!configForm.advanced.enable" :label="map2text('preview')" path="format">
  1398. <div v-html="reFormatTimeHTML(configForm.format)"></div>
  1399. </n-form-item>
  1400. <n-form-item v-show="!configForm.advanced.enable" :label="map2text('code')" path="format">
  1401. <div class="code-block">
  1402. <n-scrollbar style="max-height: 4em">
  1403. <n-code :code="configForm.format" language="html" word-wrap/>
  1404. </n-scrollbar>
  1405. </div>
  1406. </n-form-item>
  1407. <n-form-item :label="map2text('advance')" path="advanced.enable">
  1408. <n-switch v-model:value="configForm.advanced.enable" @update:value="onConfigUpdate" />
  1409. </n-form-item>
  1410. <div v-show="configForm.advanced.enable">
  1411. <n-button strong secondary type="info" round block @click="copyGPTPrompt">{{ map2text('gpt-prompt-info') }}</n-button>
  1412. <div style="height: 24px;"></div>
  1413. <n-form-item label="HTML" path="advanced.htmlTextContent">
  1414. <n-input type="textarea" :placeholder="map2text('input-html')"
  1415. @keydown.tab="insertTab" @update:value="onConfigUpdate"
  1416. v-model:value="configForm.advanced.htmlTextContent"/>
  1417. </n-form-item>
  1418. <n-form-item label="CSS" path="advanced.styleTextContent">
  1419. <n-input type="textarea" :placeholder="map2text('input-css')"
  1420. @keydown.tab="insertTab" @update:value="onConfigUpdate"
  1421. v-model:value="configForm.advanced.styleTextContent"/>
  1422. </n-form-item>
  1423. <n-form-item label="JS" path="advanced.scriptTextContent">
  1424. <n-input type="textarea" :placeholder="map2text('input-js')"
  1425. @keydown.tab="insertTab" @update:value="onConfigUpdate"
  1426. v-model:value="configForm.advanced.scriptTextContent"/>
  1427. </n-form-item>
  1428. </div>
  1429. </n-form>
  1430. </div>
  1431. </n-scrollbar>
  1432. <div class="operation-group">
  1433. <n-button strong secondary type="error" @click="onReset" :disabled="!configDirty">{{map2text('reset')}}</n-button>
  1434. <n-button strong secondary type="primary" @click="onApply">{{map2text('apply')}}</n-button>
  1435. <n-button strong secondary type="primary" @click="onConfirm">{{map2text('save')}}</n-button>
  1436. </div>
  1437. </div>
  1438. </n-config-provider>
  1439. </div>`
  1440. this.appConfig = {
  1441. el: `#${that.appID}`,
  1442. data() {
  1443. return {
  1444. date: new Date(),
  1445. hljs: hljs,
  1446. title: "ChatGPTWithDate",
  1447. formatOptions: SystemConfig.TimeRender.TimeTagTemplates.map(item => {
  1448. return {label: item, value: item}
  1449. }),
  1450. configForm: {
  1451. format: that.userConfig.timeRender.format,
  1452. advanced: {
  1453. enable: that.userConfig.timeRender.advanced.enable,
  1454. htmlTextContent: that.userConfig.timeRender.advanced.htmlTextContent,
  1455. styleTextContent: that.userConfig.timeRender.advanced.styleTextContent,
  1456. scriptTextContent: that.userConfig.timeRender.advanced.scriptTextContent,
  1457. },
  1458. },
  1459. config: {
  1460. format: that.userConfig.timeRender.format,
  1461. mode: that.userConfig.timeRender.mode,
  1462. advanced: {
  1463. enable: that.userConfig.timeRender.advanced.enable,
  1464. htmlTextContent: that.userConfig.timeRender.advanced.htmlTextContent,
  1465. styleTextContent: that.userConfig.timeRender.advanced.styleTextContent,
  1466. scriptTextContent: that.userConfig.timeRender.advanced.scriptTextContent,
  1467. },
  1468. },
  1469. locale: null,
  1470. i18n: that.userConfig.i18n,
  1471. folding: false,
  1472. configDirty: false,
  1473. configPanel: {
  1474. display: true,
  1475. },
  1476. };
  1477. },
  1478. methods: {
  1479. onApply() {
  1480. const jsValidResult = Utils.isJavaScriptSyntaxValid(this.configForm.advanced.scriptTextContent)
  1481. if (!jsValidResult.valid) {
  1482. that.notification.error({
  1483. title: this.map2text('js-invalid-info'),
  1484. content: jsValidResult.error.toString(),
  1485. duration: 3000,
  1486. keepAliveOnHover: true,
  1487. });
  1488. return false
  1489. }
  1490. this.config = JSON.parse(JSON.stringify(this.configForm));
  1491. that.updateConfig({timeRender: this.config})
  1492. this.configDirty = false
  1493. return true
  1494. },
  1495. onConfirm() {
  1496. this.onApply()
  1497. this.onClose()
  1498. },
  1499. onReset() {
  1500. this.configForm = JSON.parse(JSON.stringify(this.config));
  1501. this.configDirty = false
  1502. },
  1503. onConfigUpdate() {
  1504. this.configDirty = true
  1505. },
  1506. renderLabel(option) {
  1507. return Vue.h(TimeTagComponent, {
  1508. html: option.label,
  1509. })
  1510. },
  1511. reFormatTimeHTML(html) {
  1512. return Utils.formatDateTimeByDate(this.date, html)
  1513. },
  1514. insertTab(event) {
  1515. if (!event.shiftKey) { // 确保不是 Shift + Tab 组合
  1516. event.preventDefault(); // 阻止 Tab 键的默认行为
  1517. // 尝试使用 execCommand 插入四个空格
  1518. if (document.queryCommandSupported('insertText')) {
  1519. document.execCommand('insertText', false, ' ');
  1520. } else { // 如果浏览器不支持 execCommand,回退到原始方法(不支持撤销)
  1521. const start = event.target.selectionStart;
  1522. const end = event.target.selectionEnd;
  1523. const value = event.target.value;
  1524. event.target.value = value.substring(0, start) + " " + value.substring(end);
  1525. // 移动光标位置到插入的空格后
  1526. event.target.selectionStart = event.target.selectionEnd = start + 4;
  1527. // 触发 input 事件更新 v-model
  1528. this.$nextTick(() => {
  1529. event.target.dispatchEvent(new Event('input'));
  1530. });
  1531. }
  1532. }
  1533. },
  1534. onClose() {
  1535. that.hide()
  1536. },
  1537. toggleFolding() {
  1538. this.folding = !this.folding
  1539. },
  1540. onRestore() {
  1541. // 清空所有 GM 存储的数据
  1542. const keys = GM_listValues();
  1543. keys.forEach(key => {
  1544. GM_deleteValue(key);
  1545. });
  1546. // 重置配置至出厂设置
  1547. const defaultConfig = that.userConfig.getDefaultConfig()
  1548. this.config = defaultConfig.timeRender
  1549. this.i18n = defaultConfig.i18n
  1550. // 重置 Vue 表单
  1551. this.onReset()
  1552. // 重置样式与脚本
  1553. that.updateConfig(defaultConfig)
  1554. },
  1555. toggleLanguage() {
  1556. let index = SystemConfig.ConfigPanel.I18N.supported.indexOf(this.i18n)
  1557. if (index === -1) {
  1558. Logger.error("嗯?当前语言未知?")
  1559. }
  1560. index = (index + 1) % SystemConfig.ConfigPanel.I18N.supported.length;
  1561. this.i18n = SystemConfig.ConfigPanel.I18N.supported[index]
  1562. that.userConfig.updateOne('i18n', this.i18n)
  1563. this.setNaiveUILanguage()
  1564. },
  1565. setNaiveUILanguage() {
  1566. this.locale = this.i18n === 'zh' ? naive.zhCN : null;
  1567. },
  1568. openUrl(url, target) {
  1569. window.open(url, target)
  1570. },
  1571. copyGPTPrompt() {
  1572. navigator.clipboard.writeText(Utils.base64Decode(this.map2text('gpt-prompt'))).then(() => {
  1573. that.notification.success({
  1574. content: this.map2text('copy-success-info'),
  1575. duration: 1000,
  1576. });
  1577. })
  1578. },
  1579. },
  1580. computed: {
  1581. map2text() {
  1582. return key => {
  1583. const language = this.i18n;
  1584. if (!SystemConfig.ConfigPanel.I18N[language]) {
  1585. Logger.error(`当前语言 ${language} 不受支持!已回退至 ${SystemConfig.ConfigPanel.I18N.default}`);
  1586. return SystemConfig.ConfigPanel.I18N[SystemConfig.ConfigPanel.I18N.default][key] || 'NULL';
  1587. }
  1588. if (!SystemConfig.ConfigPanel.I18N[language][key]) {
  1589. Logger.debug(`当前语言 ${language} 不存在键为 ${key} i18n 支持,已回退至 ${SystemConfig.ConfigPanel.I18N.default}`);
  1590. return SystemConfig.ConfigPanel.I18N[SystemConfig.ConfigPanel.I18N.default][key] || 'NULL';
  1591. }
  1592. return SystemConfig.ConfigPanel.I18N[language][key];
  1593. };
  1594. }
  1595. },
  1596. created() {
  1597. this.date = new Date()
  1598. this.formatOptions.forEach(item => {
  1599. item.label = this.reFormatTimeHTML(item.value)
  1600. })
  1601. },
  1602. mounted() {
  1603. this.timestampInterval = setInterval(() => {
  1604. // 满足打开状态且高级模式开启时才更新时间,避免不必要的性能消耗
  1605. if (that.isShow()) {
  1606. this.date = new Date()
  1607. this.formatOptions.forEach(item => {
  1608. item.label = this.reFormatTimeHTML(item.value)
  1609. })
  1610. }
  1611. }, 50)
  1612. this.setNaiveUILanguage()
  1613. },
  1614. beforeUnmount() {
  1615. clearInterval(this.timestampInterval)
  1616. },
  1617. }
  1618. }
  1619.  
  1620. /**
  1621. * 初始化样式。
  1622. * 同时为了避免样式冲突,在 head 元素中加入一个 <meta name="naive-ui-style" /> 元素,
  1623. * naive-ui 会把所有的样式刚好插入这个元素的前面。
  1624. * 参考 https://www.naiveui.com/zh-CN/os-theme/docs/style-conflict
  1625. *
  1626. * @returns {Promise}
  1627. * @private
  1628. */
  1629. _initStyle() {
  1630. return new Promise(resolve => {
  1631. const meta = document.createElement('meta');
  1632. meta.name = 'naive-ui-style'
  1633. document.head.appendChild(meta);
  1634. this.styleService.updateStyle(SystemConfig.ConfigPanel.StyleKey, this.panelStyle)
  1635. resolve()
  1636. })
  1637. }
  1638.  
  1639. /**
  1640. * 初始化 Vue 与 Naive UI 脚本。无法使用 <script src="https://xxx"> 的方式插入,因为 ChatGPT 有 CSP 限制。
  1641. * 采用 GM_xmlhttpRequest 的方式获取 Vue 与 Naive UI 的脚本内容,然后插入 <script>脚本内容</script> 到页面中。
  1642. *
  1643. * @returns {Promise}
  1644. * @private
  1645. */
  1646. _initExternalResources() {
  1647. return new Promise(resolve => {
  1648. let completeCount = 0;
  1649. const resources = [
  1650. {type: 'js', url: 'https://unpkg.com/vue@3.4.26/dist/vue.global.js'},
  1651. {type: 'js', url: 'https://unpkg.com/naive-ui@2.38.1/dist/index.js'},
  1652. {type: 'css', url: 'https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/default.min.css'},
  1653. {type: 'js', url: 'https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js'},
  1654. ]
  1655. resources.forEach(resource => {
  1656. let element = null
  1657. if (resource.type === 'js') {
  1658. element = GM_addElement('script', {
  1659. src: resource.url,
  1660. type: 'text/javascript'
  1661. });
  1662. } else if (resource.type === 'css') {
  1663. element = GM_addElement('link', {
  1664. rel: 'stylesheet',
  1665. type: 'text/css',
  1666. href: resource.url
  1667. });
  1668. }
  1669. element.onload = () => {
  1670. completeCount++;
  1671. if (completeCount === resources.length) {
  1672. resolve()
  1673. }
  1674. }
  1675. })
  1676. // 以下方法有 CSP 限制
  1677. // const naiveScript = document.createElement('script');
  1678. // naiveScript.setAttribute("type", "text/javascript");
  1679. // naiveScript.text = "https://unpkg.com/naive-ui@2.38.1/dist/index.js";
  1680. // document.documentElement.appendChild(naiveScript);
  1681. })
  1682. }
  1683.  
  1684. /**
  1685. * 初始化配置面板,插入配置面板的 HTML 到 body 中。
  1686. *
  1687. * @returns {Promise}
  1688. * @private
  1689. */
  1690. _initPanel() {
  1691. const that = this
  1692. return new Promise(resolve => {
  1693. const panelRoot = document.createElement('div');
  1694. panelRoot.innerHTML = that.panelHTML;
  1695. document.body.appendChild(panelRoot);
  1696. resolve()
  1697. })
  1698. }
  1699.  
  1700. /**
  1701. * 初始化 Vue 实例,挂载到配置面板的 HTML 元素上。
  1702. *
  1703. * @private
  1704. */
  1705. _initVue() {
  1706. const app = Vue.createApp(this.appConfig);
  1707. app.use(naive)
  1708. const {notification} = naive.createDiscreteApi(
  1709. ["notification"], {
  1710. configProviderProps: {
  1711. theme: naive.lightTheme
  1712. }
  1713. },);
  1714. this.notification = notification
  1715. app.mount(`#${this.appID}`);
  1716. }
  1717.  
  1718. /**
  1719. * 初始化配置面板大小与位置
  1720. *
  1721. * @private
  1722. */
  1723. _initConfigPanelSizeAndPosition() {
  1724. const panel = document.getElementById(this.appID)
  1725.  
  1726. // 获取存储的大小
  1727. const size = GM_getValue(SystemConfig.GMStorageKey.ConfigPanel.Size, {})
  1728. if (size && size.width && !isNaN(size.width)) {
  1729. panel.style.width = size.width + 'px';
  1730. }
  1731.  
  1732. // 获取存储的位置
  1733. const position = GM_getValue(SystemConfig.GMStorageKey.ConfigPanel.Position, {})
  1734. if (position && position.left && position.top && !isNaN(position.left) && !isNaN(position.top)) {
  1735. const {left, top} = position
  1736. const {refineLeft, refineTop} = this.refinePosition(left, top, panel.offsetWidth, panel.offsetHeight)
  1737. panel.style.left = refineLeft + 'px';
  1738. panel.style.top = refineTop + 'px';
  1739. }
  1740.  
  1741. // 如果面板任何一边超出屏幕,则重置位置
  1742. // const rect = panel.getBoundingClientRect()
  1743. // const leftTop = {
  1744. // x: rect.left,
  1745. // y: rect.top
  1746. // }
  1747. // const rightBottom = {
  1748. // x: rect.left + rect.width,
  1749. // y: rect.top + rect.height
  1750. // }
  1751. // const screenWidth = window.innerWidth;
  1752. // const screenHeight = window.innerHeight;
  1753. // if (leftTop.x < 0 || leftTop.y < 0 || rightBottom.x > screenWidth || rightBottom.y > screenHeight) {
  1754. // panel.style.left = '50px';
  1755. // panel.style.top = '50px';
  1756. // }
  1757.  
  1758. }
  1759.  
  1760. /**
  1761. * 初始化配置面板事件监控,包括面板拖动、面板大小变化等事件。
  1762. *
  1763. * @private
  1764. */
  1765. _initConfigPanelEventMonitor() {
  1766. const that = this
  1767. const panel = document.getElementById(this.appID)
  1768. const draggableArea = document.getElementById(`${this.appID}-DraggableArea`)
  1769.  
  1770. // 监听面板宽度变化
  1771. const resizeObserver = new ResizeObserver(entries => {
  1772. for (let entry of entries) {
  1773. if (entry.contentRect.width) {
  1774. GM_setValue(SystemConfig.GMStorageKey.ConfigPanel.Size, {
  1775. width: entry.contentRect.width,
  1776. })
  1777. }
  1778. }
  1779. });
  1780. resizeObserver.observe(panel);
  1781.  
  1782. // 监听面板位置
  1783. draggableArea.addEventListener('mousedown', function (e) {
  1784. const offsetX = e.clientX - draggableArea.getBoundingClientRect().left;
  1785. const offsetY = e.clientY - draggableArea.getBoundingClientRect().top;
  1786.  
  1787. function mouseMoveHandler(e) {
  1788. const left = e.clientX - offsetX;
  1789. const top = e.clientY - offsetY;
  1790. const {
  1791. refineLeft, refineTop
  1792. } = that.refinePosition(left, top, panel.offsetWidth, panel.offsetHeight);
  1793. panel.style.left = refineLeft + 'px';
  1794. panel.style.top = refineTop + 'px';
  1795. GM_setValue(SystemConfig.GMStorageKey.ConfigPanel.Position, {
  1796. left: refineLeft, top: refineTop,
  1797. })
  1798. }
  1799.  
  1800. document.addEventListener('mousemove', mouseMoveHandler);
  1801. document.addEventListener('mouseup', function () {
  1802. document.removeEventListener('mousemove', mouseMoveHandler);
  1803. });
  1804. });
  1805. }
  1806.  
  1807. /**
  1808. * 限制面板位置,使其任意一部分都不超出屏幕
  1809. *
  1810. * @param left 面板左上角 x 坐标
  1811. * @param top 面板左上角 y 坐标
  1812. * @param width 面板宽度
  1813. * @param height 面板高度
  1814. * @returns {{refineLeft: number, refineTop: number}} 返回修正后的坐标
  1815. */
  1816. refinePosition(left, top, width, height) {
  1817. const screenWidth = window.innerWidth;
  1818. const screenHeight = window.innerHeight;
  1819. return {
  1820. refineLeft: Math.min(Math.max(0, left), screenWidth - width),
  1821. refineTop: Math.min(Math.max(0, top), screenHeight - height),
  1822. }
  1823. }
  1824.  
  1825. /**
  1826. * 初始化菜单命令,用于在 Tampermonkey 的菜单中添加一个配置面板的命令。
  1827. *
  1828. * @private
  1829. */
  1830. _initMenuCommand() {
  1831. let that = this
  1832. if (IsConfigPage) {
  1833. GM_registerMenuCommand("打开配置面板 Open the configuration panel", () => {
  1834. that.show()
  1835. })
  1836. } else {
  1837. GM_registerMenuCommand("配置面板 Configuration Panel", () => {
  1838. GM_openInTab("https://jiang-taibai.github.io/chatgpt-with-date-config-page/");
  1839. })
  1840. }
  1841. GM_registerMenuCommand("文档 Documentation", () => {
  1842. GM_openInTab("https://jiang-taibai.github.io/chatgpt-with-date/");
  1843. })
  1844. GM_registerMenuCommand("GitHub", () => {
  1845. GM_openInTab("https://github.com/jiang-taibai/chatgpt-with-date/");
  1846. })
  1847. }
  1848.  
  1849. /**
  1850. * 显示配置面板
  1851. */
  1852. show() {
  1853. document.getElementById(this.appID).style.visibility = 'visible';
  1854. unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey].visibility = true
  1855. }
  1856.  
  1857. /**
  1858. * 隐藏配置面板
  1859. */
  1860. hide() {
  1861. document.getElementById(this.appID).style.visibility = 'hidden';
  1862. unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey].visibility = false
  1863. }
  1864.  
  1865. isShow() {
  1866. return unsafeWindow[SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey].visibility
  1867. }
  1868.  
  1869. /**
  1870. * 更新配置,由 Vue 组件调用来更新配置并重新渲染时间
  1871. * @param config
  1872. */
  1873. updateConfig(config) {
  1874. this.userConfig.update(config)
  1875. this.timeRendererService.reRender()
  1876. }
  1877. }
  1878.  
  1879. class Main {
  1880. static ComponentsConfig = [
  1881. UserConfig, StyleService, MessageService,
  1882. MonitorService, TimeRendererService,
  1883. JavaScriptService, HookService,
  1884. ConfigPanelService,
  1885. ]
  1886.  
  1887. constructor() {
  1888. for (let componentClazz of Main.ComponentsConfig) {
  1889. const instance = new componentClazz();
  1890. this[componentClazz.name] = instance
  1891. ComponentLocator.register(componentClazz, instance)
  1892. }
  1893. }
  1894.  
  1895. /**
  1896. * 获取依赖关系图
  1897. * @returns {[]} 依赖关系图,例如 [{node:'ComponentClass0', dependencies:['ComponentClass2', 'ComponentClass3']}, ...]
  1898. * @private
  1899. */
  1900. _getDependencyGraph() {
  1901. const dependencyGraph = []
  1902. for (let componentClazz of Main.ComponentsConfig) {
  1903. const dependencies = this[componentClazz.name].dependencies.map(dependency => dependency.clazz.name)
  1904. dependencyGraph.push({node: componentClazz.name, dependencies})
  1905. }
  1906. Logger.debug('依赖关系图:', JSON.stringify(dependencyGraph))
  1907. return dependencyGraph
  1908. }
  1909.  
  1910. /**
  1911. * 注册(不可用)全局变量
  1912. * @private
  1913. */
  1914. _registerGlobalVariables() {
  1915. unsafeWindow[SystemConfig.Main.WindowRegisterKey] = {}
  1916. }
  1917.  
  1918. start() {
  1919. this._registerGlobalVariables()
  1920. const dependencyGraph = this._getDependencyGraph()
  1921. const order = Utils.dependencyAnalysis(dependencyGraph)
  1922. Logger.debug('初始化顺序:', order.join(' -> '))
  1923. for (let componentName of order) {
  1924. this[componentName].initDependencies()
  1925. }
  1926. for (let componentName of order) {
  1927. this[componentName].init()
  1928. }
  1929. }
  1930. }
  1931.  
  1932. const main = new Main();
  1933. main.start();
  1934.  
  1935. })();
  1936.  
  1937.  
  1938.  
  1939.  
  1940.  
  1941.  
  1942.  
  1943.  
  1944.  
  1945.  
  1946.  
  1947.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址