// ==UserScript==
// @name ChatGPT with Date
// @namespace https://github.com/jiang-taibai/chatgpt-with-date
// @version 2.0.0
// @description Tampermonkey plugin for displaying ChatGPT historical and real-time conversation time. 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。
// @author CoderJiang
// @license MIT
// @match *chat.openai.com/*
// @match *chatgpt.com/*
// @match *jiang-taibai.github.io/chatgpt-with-date-config-page*
// @match *project.coderjiang.com/chatgpt-with-date-config-page*
// @icon *cdn.coderjiang.com/project/chatgpt-with-date/logo.svg
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_addElement
// @grant GM_addStyle
// @grant GM_openInTab
// @grant unsafeWindow
// @run-at document-end
// @noframes
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const IsConfigPage = window.location.hostname === 'jiang-taibai.github.io' ||
window.location.hostname === 'project.coderjiang.com'
class SystemConfig {
static Common = {
ApplicationName: 'ChatGPT with Date',
GPTPrompt: 'IyAxLiDku7vliqHnroDku4sKCuS9oOmcgOimgeWGmSBIVE1M44CBQ1NT44CBSmF2YVNjcmlwdCDku6PnoIHvvIzlrp7njrDmiJHnmoTpnIDmsYLvvIzlkI7pnaLmiJHlsIbor6bnu4bku4vnu43kvaDlupTor6XmgI7kuYjlhpnku6PnoIHjgIIKCiMgMi4gSFRNTCDopoHmsYIKCuS9oOmcgOimgeWGmeS4gOS4quaXpeacn+aXtumXtOeahOaooeadvyBIVE1MIOWtl+espuS4su+8jOS9oOWPr+S7peS9v+eUqOWNoOS9jeespuadpeihqOekuuaXtumXtOWFg+e0oO+8jOS+i+Wmgu+8mgoKYGBgaHRtbAo8ZGl2IGNsYXNzPSJ0ZXh0LXRhZy1ib3giPgogICAgPHNwYW4gY2xhc3M9ImRhdGUiPnt5eXl5fS17TU19LXtkZH08L3NwYW4+CiAgICA8c3BhbiBjbGFzcz0idGltZSI+e0hIfTp7bW19Ontzc308L3NwYW4+CjwvZGl2PgpgYGAKCuWQjumdouS8muS7i+e7jeS9oOaAjuS5iOeUqCBKYXZhU2NyaXB0IOadpeWunueOsOaYvuekuueJueWumueahOaXtumXtOOAggoKIyAzLiBDU1Mg6KaB5rGCCgooMSkg5LiN5YWB6K645YaZ5qCH562+6YCJ5oup5Zmo77yM5Y+q6IO95YaZ57G76YCJ5oup5Zmo5oiWIElEIOmAieaLqeWZqAooMikg5bC96YeP5YaZ5ZCO5Luj6YCJ5oup5Zmo77yM5LiN5rGh5p+T5YWo5bGA5qC35byPCigzKSDlsL3ph4/kuI3opoHkvb/nlKggYCFpbXBvcnRhbnRgCgojIDQuIEphdmFTY3JpcHQg6KaB5rGCCgojIyA0LjEg5o+Q5L6b55qEIEFQSSDmjqXlj6MKCkFQSSDlrprkuYnlnKggd2luZG93IOS4iu+8jOWmguacieW/heimgeS9oOmcgOimgeWcqCBKUyDohJrmnKzlhoXph43lhpnlh73mlbDjgIIKCi0gd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSk6IOagueaNriBEYXRlIOWvueixoeWwhuaooeadvyBIVE1MIOWtl+espuS4suS4reeahOWGheWuueabv+aNouS4uiBkYXRlCiAg5a+56LGh5oyH5a6a55qE5pe26Ze0CiAgLSBkYXRlOiDml6XmnJ8gRGF0ZSDlr7nosaEKICAtIHRlbXBsYXRlOiBIVE1MIOWtl+espuS4su+8jOWNs+S9oOWGmeeahCBIVE1MIOS7o+eggQogIC0g6L+U5Zue5YC8OiDmoLzlvI/ljJblkI7nmoQgSFRNTCDku6PnoIEKLSB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmJlZm9yZUNyZWF0ZVRpbWVUYWcobWVzc2FnZUlkLCB0aW1lVGFnSFRNTCk6IOWwhiB0ZW1wbGF0ZSDmj5LlhaXliLDpobXpnaLkuYvliY3osIPnlKgKICAtIG1lc3NhZ2VJZDog5raI5oGv55qEIElE77yM5bm26Z2eIEhUTUwg5YWD57Sg55qEIElECiAgLSB0aW1lVGFnSFRNTDog5q2k5pe255qE5a2X56ym5Liy5pivICc8ZGl2IGNsYXNzPSJjaGF0Z3B0LXRpbWUiPicgKyB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmZvcm1hdERhdGVUaW1lQnlEYXRlKGRhdGUsCiAgICB0ZW1wbGF0ZSkgKyAnPC9kaXY+JwogIC0g6L+U5Zue5YC8OiDml6AKLSB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmFmdGVyQ3JlYXRlVGltZVRhZyhtZXNzYWdlSWQsIHRpbWVUYWdOb2RlKTog5bCGIHRlbXBsYXRlIOaPkuWFpeWIsOmhtemdouS5i+WQjuiwg+eUqAogIC0gbWVzc2FnZUlkOiDmtojmga/nmoQgSUTvvIzlubbpnZ4gSFRNTCDlhYPntKDnmoQgSUQKICAtIHRpbWVUYWdOb2RlOiDmraTml7bnmoToioLngrnmmK8gJzxkaXYgY2xhc3M9ImNoYXRncHQtdGltZSI+JyArIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuZm9ybWF0RGF0ZVRpbWVCeURhdGUoZGF0ZSwKICAgIHRlbXBsYXRlKSArICc8L2Rpdj4nIOeahCBET00g6IqC54K5CiAgLSDov5Tlm57lgLw6IOaXoAoKIyMgNC4yIEFQSSDmiafooYzpgLvovpEKCuezu+e7n+S8muaMieeFp+S7peS4i+mhuuW6j+aJp+ihjCBBUEnvvJoKCigxKSB0ZW1wbGF0ZSA9IOS9oOi+k+WFpeeahCBIVE1MIOS7o+eggQooMikgdGVtcGxhdGUgPSB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmZvcm1hdERhdGVUaW1lQnlEYXRlKGRhdGUsIHRlbXBsYXRlKQooMykgdGltZVRhZ0hUTUwgPSAnPGRpdiBjbGFzcz0iY2hhdGdwdC10aW1lIj4nICsgdGVtcGxhdGUgKyAnPC9kaXY+JwooNCkgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5iZWZvcmVDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0hUTUwpCig1KSDlsIYgdGltZVRhZ0hUTUwg5o+S5YWl5Yiw5p+Q5L2N572uCig2KSB0aW1lVGFnTm9kZSA9IOWImuWImuaPkuWFpeeahCB0aW1lVGFnSFRNTCDoioLngrkKKDcpIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYWZ0ZXJDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ05vZGUpCgojIyA0LjMg5Luj56CB6KeE6IyDCgooMSkg6K+35L2/55SoIEVTNiDor63ms5UKKDIpIOivt+S9v+eUqOS4peagvOaooeW8jyBgJ3VzZSBzdHJpY3QnYAooMykg6K+35L2/55SoIGBjb25zdGAg5ZKMIGBsZXRgIOWjsOaYjuWPmOmHjwooNCkg6K+35L2/55SoIElJRkUg6YG/5YWN5YWo5bGA5Y+Y6YeP5rGh5p+TCig1KSDor7fkvb/nlKggYD09PWAg5ZKMIGAhPT1gIOmBv+WFjeexu+Wei+i9rOaNoumXrumimAooNikg5rOo6YeK5LiA5b6L5YaZ5Lit5paH5rOo6YeKCgojIDUuIOahiOS+iwoK5Lul5LiL5piv5LiA5Liq5qGI5L6L77yM5a6e546w5YWJ5qCH56e75Yqo5Yiw5pe26Ze05qCH562+5LiK5pe277yM5pel5pyf5pi+56S65Li65Yeg5aSp5YmN55qE5pWI5p6c44CCCgpIVE1MIOS7o+egge+8mgoKYGBgaHRtbAo8ZGl2IGNsYXNzPSJ0ZXh0LXRhZy1ib3giPgogICAgPHNwYW4gY2xhc3M9ImRhdGUiPnt5eXl5fS17TU19LXtkZH08L3NwYW4+CiAgICA8c3BhbiBjbGFzcz0idGltZSI+e0hIfTp7bW19Ontzc308L3NwYW4+CjwvZGl2PgpgYGAKCkNTUyDku6PnoIHvvJoKCmBgYGNzcwoudGV4dC10YWctYm94IHsKICAgIGJvcmRlci1yYWRpdXM6IDhweDsKICAgIGNvbG9yOiAjRTBFMEUwOwogICAgZm9udC1zaXplOiAwLjllbTsKICAgIG92ZXJmbG93OiBoaWRkZW47CiAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7Cn0KCi50ZXh0LXRhZy1ib3ggLmRhdGUgewogICAgYmFja2dyb3VuZDogIzMzMzsKICAgIGZsb2F0OiBsZWZ0OwogICAgcGFkZGluZzogMnB4IDhweCAycHggMTBweDsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKfQoKLnRleHQtdGFnLWJveCAudGltZSB7CiAgICBiYWNrZ3JvdW5kOiAjNjA2MDYwOwogICAgZmxvYXQ6IGxlZnQ7CiAgICBwYWRkaW5nOiAycHggMTBweCAycHggOHB4OwogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwp9CmBgYAoKSmF2YVNjcmlwdCDku6PnoIHvvJoKCmBgYGphdmFzY3JpcHQKKCgpID0+IHsKICAgIHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuZm9ybWF0RGF0ZVRpbWVCeURhdGUgPSAoZGF0ZSwgdGVtcGxhdGUpID0+IHsKICAgICAgICBjb25zdCBmb3JtYXRWYWx1ZSA9ICh2YWx1ZSwgZm9ybWF0KSA9PiB2YWx1ZS50b1N0cmluZygpLnBhZFN0YXJ0KGZvcm1hdCA9PT0gJ3l5eXknID8gNCA6IDIsICcwJyk7CiAgICAgICAgY29uc3QgZGF0ZVZhbHVlcyA9IHsKICAgICAgICAgICAgJ3t5eXl5fSc6IGRhdGUuZ2V0RnVsbFllYXIoKSwKICAgICAgICAgICAgJ3tNTX0nOiBkYXRlLmdldE1vbnRoKCkgKyAxLAogICAgICAgICAgICAne2RkfSc6IGRhdGUuZ2V0RGF0ZSgpLAogICAgICAgICAgICAne0hIfSc6IGRhdGUuZ2V0SG91cnMoKSwKICAgICAgICAgICAgJ3ttbX0nOiBkYXRlLmdldE1pbnV0ZXMoKSwKICAgICAgICAgICAgJ3tzc30nOiBkYXRlLmdldFNlY29uZHMoKQogICAgICAgIH07CiAgICAgICAgcmV0dXJuIHRlbXBsYXRlLnJlcGxhY2UoL1x7W159XStcfS9nLCBtYXRjaCA9PiBmb3JtYXRWYWx1ZShkYXRlVmFsdWVzW21hdGNoXSwgbWF0Y2guc2xpY2UoMSwgLTEpKSk7CiAgICB9CiAgICB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmFmdGVyQ3JlYXRlVGltZVRhZyA9IChtZXNzYWdlSWQsIHRpbWVUYWdOb2RlKSA9PiB7CiAgICAgICAgY29uc3QgZGF0ZU5vZGUgPSB0aW1lVGFnTm9kZS5xdWVyeVNlbGVjdG9yKCcuZGF0ZScpOwogICAgICAgIGNvbnN0IGRhdGVUZXh0ID0gZGF0ZU5vZGUuaW5uZXJUZXh0OwogICAgICAgIGNvbnN0IGRhdGUgPSBuZXcgRGF0ZShkYXRlVGV4dCk7CiAgICAgICAgdGltZVRhZ05vZGUuYWRkRXZlbnRMaXN0ZW5lcignbW91c2VvdmVyJywgKCkgPT4gewogICAgICAgICAgICBkYXRlTm9kZS5pbm5lclRleHQgPSBgJHtNYXRoLmZsb29yKChuZXcgRGF0ZSgpIC0gZGF0ZSkgLyA4NjQwMDAwMCl95aSp5YmNYDsKICAgICAgICB9KTsKICAgICAgICB0aW1lVGFnTm9kZS5hZGRFdmVudExpc3RlbmVyKCdtb3VzZW91dCcsICgpID0+IHsKICAgICAgICAgICAgZGF0ZU5vZGUuaW5uZXJUZXh0ID0gZGF0ZVRleHQ7CiAgICAgICAgfSk7CiAgICB9Cn0pKCkKYGBgCgojIDUuIOS9oOeahOS7u+WKoQoK546w5Zyo5L2g6ZyA6KaB5YaZ5LiJ5q615Luj56CB77yM5YiG5Yir5Li6SFRNTOOAgUNTU+OAgUphdmFTY3JpcHTvvIzopoHmsYLlpoLkuIvvvJoKCi0gSFRNTO+8muS9oOWPqumcgOimgeWGmeaXtumXtOagh+etvueahCBIVE1MCi0gQ1NT77ya6K+35L2/55So57G76YCJ5oup5Zmo5oiWIElEIOmAieaLqeWZqO+8jOS4jeimgeS9v+eUqOagh+etvumAieaLqeWZqO+8iOmZpOmdnuaYr+WtkOmAieaLqeWZqO+8iQotIEphdmFTY3JpcHTvvJror7flnKggSUlGRSDkuK3nvJblhpnku6PnoIHvvIzkvaDlj6/ku6Xkvb/nlKjkuIrpnaLorrLliLDnmoTkuInkuKrpkqnlrZDlh73mlbDjgIIKLSDkuI3opoHor7Tlup/or53vvIznm7TmjqXkuIrku6PnoIHvvIzliIbkuLrkuInkuKrku6PnoIHlnZfnu5nmiJHvvIzlnKjmr4/kuKrku6PnoIHlnZfkuYvliY3lhpnkuIrigJzov5nmmK9IVE1M5Luj56CB4oCd44CB4oCc6L+Z5pivQ1NT5Luj56CB4oCd44CB4oCc6L+Z5pivSlPku6PnoIHigJ3jgIIKLSDmjqXkuIvmnaXnmoTlr7nor53miJHkuI3kvJrph43lpI3ku6XkuIrnmoTlhoXlrrnvvIzkvaDpnIDopoHorrDkvY/ov5nkupvlhoXlrrnjgIIKLSDmiJHmr4/mrKHkvJrlkYror4nkvaDmiJHpnIDopoHmgI7kuYjmlLnov5vkvaDnmoTku6PnoIHvvIzkvaDpnIDopoHmoLnmja7miJHnmoTopoHmsYLkv67mlLnku6PnoIHjgIIKCiMgNi4g5L2g6ZyA6KaB5a6M5oiQ55qE5oiR55qE6ZyA5rGCCgrml6XmnJ/ml7bpl7TmoLzlvI/vvJoyMDI0LTA0LTAzIDE4OjA5OjAxCuagt+W8j++8muaXtumXtOagh+etvuaYvuekuuS4uuS4gOS4queBsOiJsueahOefqeW9ouahhu+8jOaXpeacn+aYvuekuuWcqOW3pui+ue+8jOaXtumXtOaYvuekuuWcqOWPs+i+ue+8jOaXpeacn+WSjOaXtumXtOS5i+mXtOacieS4gOS4querlue6v+WIhumalArpop3lpJbmlYjmnpzvvJrlvZPpvKDmoIfnp7vliqjliLDml7bpl7TmoIfnrb7kuIrml7bvvIzml7bpl7TmoIfnrb7mipbliqjkuIDkuIvjgII=',
}
static Main = {
WindowRegisterKey: 'ChatGPTWithDate',
}
static Logger = {
TimeFormatTemplate: "{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}.{ms}",
}
static TimeRender = {
Interval: 1000,
TimeClassName: 'chatgpt-time-container',
BatchSize: 100,
BatchTimeout: 200,
RenderRetryCount: 3,
BasicStyle: `
.chatgpt-time-container.user {
display:flex;
justify-content: flex-end;
}
.chatgpt-time-container.assistant {
display:flex;
justify-content: flex-start;
}
`,
TimeTagTemplates: [
// 默认:2023-10-15 12:01:00
`{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}`,
// 美国:Oct 15, 2023 12:01 PM
`{MM#shortname@en} {dd}, {yyyy} {HH#12}:{mm} {HH#tag}`,
// 英国:01/01/2024 12:01
`{dd}/{MM}/{yyyy} {HH}:{mm}`,
// 日本:2023 年 10 月 15 日 12:01
`{yyyy} 年 {MM} 月 {dd} 日 {HH}:{mm}`,
// 显示毫秒数:2023-10-15 12:01:00.000
`{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}.{ms}`,
// 复杂模板
`{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}`,
`{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}`,
`{yyyy}-{MM}-{dd}{HH}:{mm}:{ss}`,
`{yyyy}-{MM}-{dd}{HH}:{mm}:{ss}`,
],
BasicStyleKey: 'time-render',
AdditionalStyleKey: 'time-render-advanced',
AdditionalScriptKey: 'time-render-advanced',
}
static ConfigPanel = {
AppID: 'CWD-Configuration-Panel',
Icon: {
Close: '',
Restore: '',
Language: '',
Documentation: '',
Minimize: '',
Maximize: '',
},
StyleKey: 'config-panel',
ApplicationRegisterKey: 'configPanel',
I18N: {
default: 'zh',
rollback: 'zh',
supported: ['zh', 'en'],
zh: {
'restore-info': ' 恢复出厂设置 ',
'restore-warn': ' 确定恢复出厂设置?你的所有自定义配置将被清除!',
'toggle-language-info': 'Switch to English',
'documentation-info': ' 查看教程 ',
'documentation-international-access': ' 国际访问 ',
'documentation-china-access': ' 中国访问 ',
'template': ' 模板 ',
'preview': ' 预览 ',
'code': ' 代码 ',
'position': ' 位置 ',
'advance': ' 高级 ',
'reset': ' 重置 ',
'apply': ' 应用 ',
'save': ' 保存并关闭 ',
'apply-failed': ' 应用失败 ',
'input-html': ' 请输入 HTML 代码 ',
'input-css': ' 请输入 CSS 代码 ',
'input-js': ' 请输入 JavaScript 代码 ',
'position-after-role-left': ' 角色之后(靠左)',
'position-after-role-right': ' 角色之后(靠右)',
'position-below-role': ' 角色之下 ',
'gpt-prompt-info': ' 不会写代码?复制提示词让 ChatGPT 帮你写!',
'copy-success-info': ' 复制成功,发给 ChatGPT 吧!',
'js-invalid-info': 'JS 代码无效 ',
},
en: {
'restore-info': 'Restore factory settings',
'restore-warn': 'Are you sure to restore the factory settings? All your custom configurations will be cleared!',
'toggle-language-info': ' 切换到中文 ',
'documentation-info': 'View documentation',
'documentation-international-access': 'International Access',
'documentation-china-access': 'China Access',
'template': 'Template',
'preview': 'Preview',
'code': 'Code',
'position': 'Position',
'advance': 'Advance',
'reset': 'Reset',
'apply': 'Apply',
'apply-failed': 'Apply failed',
'save': 'Save and Close',
'input-html': 'Please enter HTML code',
'input-css': 'Please enter CSS code',
'input-js': 'Please enter JavaScript code',
'position-after-role-left': 'After Role (left)',
'position-after-role-right': 'After Role (right)',
'position-below-role': 'Below Role',
'gpt-prompt-info': 'Not good at coding? Copy the prompt words and let ChatGPT help you!',
'copy-success-info': 'Copy successfully, send it to ChatGPT!',
'js-invalid-info': 'Invalid JS code',
},
},
}
static Hook = {
// APP 使用 SystemConfig.Main.WindowRegisterKey 作为键绑定到 window 对象上
// Hook 使用 SystemConfig.Hook.ApplicationRegisterKey 作为键绑定到 APP 对象上
// 即如果要调用钩子 foo () 方法,应该使用 window.ChatGPTWithDate.hooks.foo ()
ApplicationRegisterKey: 'hooks',
}
// GM 存储的键
static GMStorageKey = {
UserConfig: 'ChatGPTWithDate-UserConfig',
ConfigPanel: {
Position: 'ChatGPTWithDate-ConfigPanel-Position', Size: 'ChatGPTWithDate-ConfigPanel-Size',
},
}
}
class Utils {
/**
* 按照模板格式化日期时间
*
* @param date Date 对象
* @param template 模板,例如 '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}'
* @returns string 格式化后的日期时间字符串
*/
static formatDateTimeByDate (date, template) {
const year = date.getFullYear ();
const month = date.getMonth () + 1;
const day = date.getDate ();
const week = date.getDay ();
const hours = date.getHours ();
const minutes = date.getMinutes ();
const seconds = date.getSeconds ();
const milliseconds = date.getMilliseconds ();
const week2zh = ['', ' 一 ', ' 二 ', ' 三 ', ' 四 ', ' 五 ', ' 六 ', ' 日 ']
const week2enFullName = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
const week2enShortName = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const month2zh = ['', ' 一 ', ' 二 ', ' 三 ', ' 四 ', ' 五 ', ' 六 ', ' 七 ', ' 八 ', ' 九 ', ' 十 ', ' 十一 ', ' 十二 ']
const month2enFullName = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
const month2enShortName = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const getValueByKey = (key) => {
switch (key) {
case '{yyyy}':
return year.toString ();
case '{yy}':
return (year % 100).toString ().padStart (2, '0');
case '{MM}':
case '{MM:02}':
return month.toString ().padStart (2, '0');
case '{MM:01}':
return month.toString ();
case '{MM#name@zh}':
return month2zh [month];
case '{MM#name@en}':
case '{MM#fullname@en}':
return month2enFullName [month];
case '{MM#shortname@en}':
return month2enShortName [month];
case '{dd}':
case '{dd:02}':
return day.toString ().padStart (2, '0');
case '{dd:01}':
return day.toString ();
case '{HH}':
case '{HH:02}':
case '{HH#24}':
case '{HH#24:02}':
return hours.toString ().padStart (2, '0');
case '{HH:01}':
case '{HH#24:01}':
return hours.toString ();
case '{HH#12}':
case '{HH#12:02}':
return (hours % 12 || 12).toString ().padStart (2, '0');
case '{HH#12:01}':
return (hours % 12 || 12).toString ();
case '{HH#tag}':
case '{HH#tag@en}':
return hours >= 12 ? 'PM' : 'AM';
case '{HH#tag@zh}':
return hours >= 12 ? ' 下午 ' : ' 上午 ';
case '{mm}':
case '{mm:02}':
return minutes.toString ().padStart (2, '0');
case '{mm:01}':
return minutes.toString ();
case '{ss}':
case '{ss:02}':
return seconds.toString ().padStart (2, '0');
case '{ss:01}':
return seconds.toString ();
case '{ms}':
return milliseconds.toString ().padStart (3, '0');
case '{week}':
case '{week:02}':
return week.toString ().padStart (2, '0');
case '{week:01}':
return week.toString ();
case '{week#name@zh}':
return week2zh [week];
case '{week#name@en}':
case '{week#fullname@en}':
return week2enFullName [week];
case '{week#shortname@en}':
return week2enShortName [week];
default:
return key;
}
}
return template.replace (/\{[^}]+\}/g, match => getValueByKey (match));
}
/**
* 深度合并两个对象,将源对象的属性合并到目标对象中,如果属性值为对象则递归合并。
*
* @param target 目标对象
* @param source 源对象
* @returns {*}
*/
static deepMerge (target, source) {
if (!source) return target
// 遍历源对象的所有属性
Object.keys (source).forEach (key => {
if (source [key] && typeof source [key] === 'object') {
// 如果源属性是一个对象且目标中也存在同名属性且为对象,则递归合并
if (target [key] && typeof target [key] === 'object') {
Utils.deepMerge (target [key], source [key]);
} else {
// 否则直接复制(对于源中的对象,需要进行深拷贝)
target [key] = JSON.parse (JSON.stringify (source [key]));
}
} else {
// 非对象属性直接复制
target [key] = source [key];
}
});
return target;
}
/**
* 检查依赖关系图(有向图)是否有循环依赖,如果没有就返回一个先后顺序(即按照此顺序实例化不会出现依赖项为空的情况)。
* 给定依赖关系图为此结构 [{node:'ComponentClass0', dependencies:['ComponentClass2', 'ComponentClass3']}, ...]
* @param dependencyGraph 依赖关系图
* @returns {*[]}
*/
static dependencyAnalysis (dependencyGraph) {
// 创建一个映射每个节点到其入度的对象
const inDegree = {};
const graph = {};
const order = [];
// 初始化图和入度表
dependencyGraph.forEach (item => {
const {node, dependencies} = item;
if (!graph [node]) {
graph [node] = [];
inDegree [node] = 0;
}
dependencies.forEach (dependentNode => {
if (!graph [dependentNode]) {
graph [dependentNode] = [];
inDegree [dependentNode] = 0;
}
graph [dependentNode].push (node);
inDegree [node]++;
});
});
// 将所有入度为 0 的节点加入到队列中
const queue = [];
for (const node in inDegree) {
if (inDegree [node] === 0) {
queue.push (node);
}
}
// 处理队列中的节点
while (queue.length) {
const current = queue.shift ();
order.push (current);
graph [current].forEach (neighbour => {
inDegree [neighbour]--;
if (inDegree [neighbour] === 0) {
queue.push (neighbour);
}
});
}
// 如果排序后的节点数量不等于图中的节点数量,说明存在循环依赖
if (order.length !== Object.keys (graph).length) {
// 找到循环依赖的节点
const cycleNodes = [];
for (const node in inDegree) {
if (inDegree [node] !== 0) {
cycleNodes.push (node);
}
}
throw new Error ("存在循环依赖的节点:" + cycleNodes.join (","));
}
return order;
}
/**
* 将文本转换为 base64 编码,兼容中文
* @param text
* @returns {string}
*/
static base64Encode (text) {
const encodedUriComponent = encodeURIComponent (text);
const binaryString = unescape (encodedUriComponent);
return btoa (binaryString);
}
/**
* 将 base64 编码的文本解码,兼容中文
* @param encoded
* @returns {string}
*/
static base64Decode (encoded) {
const binaryString = atob (encoded);
const encodedUriComponent = escape (binaryString);
return decodeURIComponent (encodedUriComponent);
}
/**
* 判断 JavaScript 代码字符串是否合法
* @param code
* @returns {{valid: boolean, error: *}}
*/
static isJavaScriptSyntaxValid (code) {
try {
new Function (code);
return {
valid: true,
error: null
};
} catch (e) {
return {
valid: false,
error: e
};
}
}
}
class Logger {
static EnableLog = true
static EnableDebug = true
static EnableInfo = true
static EnableWarn = true
static EnableError = true
static EnableTable = false
static prefix (type = 'INFO') {
const timeFormat = Utils.formatDateTimeByDate (new Date (), SystemConfig.Logger.TimeFormatTemplate);
return `[${timeFormat}] - [${SystemConfig.Common.ApplicationName}] - [${type}]`
}
static log (...args) {
if (Logger.EnableLog) {
console.log (Logger.prefix ('INFO'), ...args);
}
}
static debug (...args) {
if (Logger.EnableDebug) {
console.debug (Logger.prefix ('DEBUG'), ...args);
}
}
static info (...args) {
if (Logger.EnableInfo) {
console.info (Logger.prefix ('INFO'), ...args);
}
}
static warn (...args) {
if (Logger.EnableWarn) {
console.warn (Logger.prefix ('WARN'), ...args);
}
}
static error (...args) {
if (Logger.EnableError) {
console.error (Logger.prefix ('ERROR'), ...args);
}
}
static table (...args) {
if (Logger.EnableTable) {
console.table (...args);
}
}
}
class MessageBO {
/**
* 消息业务对象
*
* @param messageId 消息 ID,为消息元素的 data-message-id 属性值
* @param role 角色
* system: 表示系统消息,并不属于聊天内容。 从 API 获取
* tool: 也表示系统消息。 从 API 获取
* assistant: 表示 ChatGPT 回答的消息。 从 API 获取
* user: 表示用户输入的消息。 从 API 获取
* You: 表示用户输入的消息。 从页面实时获取
* ChatGPT: 表示 ChatGPT 回答的消息。 从页面实时获取
* @param timestamp 时间戳,浮点数或整数类型,单位毫秒,例如 1714398759.26881、1714398759
* @param message 消息内容
*/
constructor (messageId, role, timestamp, message = '') {
this.messageId = messageId;
this.role = role;
this.timestamp = timestamp;
this.message = message;
}
}
class MessageElementBO {
/**
* 消息元素业务对象
*
* @param rootEle 消息的根元素,也就是 messageEle 的父节点
* @param messageEle 消息元素,包含 data-message-id 属性
* 例如
你好
*/
constructor (rootEle, messageEle) {
this.rootEle = rootEle;
this.messageEle = messageEle;
}
}
class Component {
constructor () {
this.dependencies = []
Object.defineProperty (this, 'initDependencies', {
value: function () {
this.dependencies.forEach (dependency => {
this [dependency.field] = ComponentLocator.get (dependency.clazz)
})
}, writable: false, // 防止方法被修改
configurable: false // 防止属性被重新定义或删除
});
}
init () {
}
}
class ComponentLocator {
/**
* 组件注册器,用于注册和获取组件
*/
static components = {};
/**
* 注册组件,要求组件为 Component 的子类
*
* @param clazz Component 的子类
* @param instance Component 的子类的实例化对象,必顧是 clazz 的实例
* @returns obj 返回注册的实例化对象
*/
static register (clazz, instance) {
if (!(instance instanceof Component)) {
throw new Error (`实例化对象 ${instance} 不是 Component 的实例。`);
}
if (!(instance instanceof clazz)) {
throw new Error (`实例化对象 ${instance} 不是 ${clazz} 的实例。`);
}
if (ComponentLocator.components [clazz.name]) {
throw new Error (`组件 ${clazz.name} 已经注册过了。`);
}
ComponentLocator.components [clazz.name] = instance;
return instance
}
/**
* 获取组件,用于完成组件之间的依赖注入
*
* @param clazz Component 的子类
* @returns {*} 返回注册的实例化对象
*/
static get (clazz) {
if (!ComponentLocator.components [clazz.name]) {
throw new Error (`组件 ${clazz.name} 未注册。`);
}
return ComponentLocator.components [clazz.name];
}
}
class UserConfig extends Component {
init () {
const defaultConfig = this.getDefaultConfig ()
this.timeRender = defaultConfig.timeRender
this.i18n = defaultConfig.i18n
const userConfig = this.load ()
if (userConfig) {
// 不调用 update 方法,因为 update 方法会保存配置
// 为了 (开发者)调试 方便,不保存配置
// 为了 (用户)性能 考虑,不保存配置
Utils.deepMerge (this.timeRender, userConfig.timeRender)
if (userConfig.i18n) this.i18n = userConfig.i18n
}
}
getDefaultConfig () {
return {
timeRender: {
format: SystemConfig.TimeRender.TimeTagTemplates [0],
advanced: {
enable: false,
htmlTextContent: `
{yyyy}-{MM}-{dd}
{HH}:{mm}:{ss}
`,
styleTextContent: `.text-tag-box {
border-radius: 8px;
color: #E0E0E0;
font-size: 0.9em;
overflow: hidden;
display: inline-block;
}
.text-tag-box .date {
background: #333;
float: left;
padding: 2px 8px 2px 10px;
display: inline-block;
transition: width 0.5s ease-out;
white-space: nowrap;
}
.text-tag-box .time {
background: #606060;
float: left;
padding: 2px 10px 2px 8px;
display: inline-block;
}`,
scriptTextContent: `(() => {
const getNewWidth = (targetNode, text) => {
// 创建一个临时元素来测量文本宽度
const temp = targetNode.cloneNode ();
temp.style.width = 'auto'; // 自动宽度
temp.style.visibility = 'hidden'; // 隐藏元素,不影响布局
temp.style.position = 'absolute'; // 避免影响其他元素
temp.style.whiteSpace = 'nowrap'; // 无换行
temp.innerText = text;
document.body.appendChild (temp);
const newWidth = temp.offsetWidth;
document.body.removeChild (temp);
return newWidth;
}
window.ChatGPTWithDate.hooks.afterCreateTimeTag = (messageId, timeTagNode) => {
const dateNode = timeTagNode.querySelector ('.date');
const date = dateNode.innerText;
const originalWidth = getNewWidth (dateNode, date);
const paddingWidth = 18;
dateNode.style.width = (originalWidth + paddingWidth) + 'px';
timeTagNode.addEventListener ('mouseover', () => {
const now = new Date ();
const offset = now - new Date (date);
const days = Math.floor (offset / (24 * 60 * 60 * 1000));
let text = '';
if (days < 1)
text = ' 今天 ';
else if (days < 2)
text = ' 昨天 ';
else if (days < 3)
text = ' 前天 ';
else if (days < 7)
text = days + ' 天前 ';
else if (days < 30)
text = Math.floor (days / 7) + ' 周前 ';
else if (days < 365)
text = Math.floor (days / 30) + ' 个月前 ';
else
text = Math.floor (days / 365) + ' 年前 ';
dateNode.innerText = text;
dateNode.style.width = (getNewWidth (dateNode, text) + paddingWidth) + 'px';
});
// 鼠标移出 timeTagNode 时恢复 dateNode 的内容为原来的日期
timeTagNode.addEventListener ('mouseout', () => {
dateNode.innerText = date;
dateNode.style.width = (originalWidth + paddingWidth) + 'px';
});
}
})()`,
}
},
i18n: SystemConfig.ConfigPanel.I18N.default
}
}
restore () {
this.timeRender = this.getDefaultConfig ().timeRender
this.i18n = SystemConfig.ConfigPanel.I18N.default
this.save ()
}
save () {
GM_setValue (SystemConfig.GMStorageKey.UserConfig, {
timeRender: this.timeRender,
i18n: this.i18n
})
}
load () {
return GM_getValue (SystemConfig.GMStorageKey.UserConfig)
}
/**
* 更新配置并保存
* @param newConfig 新的配置
*/
update (newConfig) {
Utils.deepMerge (this.timeRender, newConfig.timeRender)
if (newConfig.i18n) this.i18n = newConfig.i18n
this.save ()
}
/**
* 更新一个配置项并保存
* @param key 配置项的 key
* @param value 配置项的 value
*/
updateOne (key, value) {
if (this [key] instanceof Object) {
Utils.deepMerge (this [key], value)
} else {
this [key] = value
}
this.save ()
}
}
class HookService extends Component {
init () {
this.defaultHooks = {
beforeCreateTimeTag: (messageId, timeTagHTML) => {
},
afterCreateTimeTag: (messageId, timeTagNode) => {
},
formatDateTimeByDate: Utils.formatDateTimeByDate
}
this.reset2DefaultHooks ()
}
_checkOldVersion (hookName) {
if (unsafeWindow [hookName]) {
Logger.warn (`钩子函数 ${hookName} 不应该绑定在 window 对象上,请绑定在 window.${SystemConfig.Main.WindowRegisterKey}.${SystemConfig.Hook.ApplicationRegisterKey} 上!未来版本中将不再兼容此情况。`)
return true
}
return false
}
_register2Window () {
unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.Hook.ApplicationRegisterKey] = {
beforeCreateTimeTag: this.hooks.beforeCreateTimeTag,
afterCreateTimeTag: this.hooks.afterCreateTimeTag,
formatDateTimeByDate: this.hooks.formatDateTimeByDate
}
for (let hookName in this.defaultHooks) {
if (this._checkOldVersion (hookName)) {
unsafeWindow [hookName] = this.defaultHooks [hookName]
}
}
}
reset2DefaultHooks () {
this.hooks = this.defaultHooks
this._register2Window ()
}
invokeHook (hookName, ...args) {
if (!this.defaultHooks [hookName]) {
Logger.error (`钩子函数 ${hookName} 非法`)
return
}
if (this._checkOldVersion (hookName)) {
unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.Hook.ApplicationRegisterKey][hookName] = unsafeWindow [hookName]
}
try {
return unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.Hook.ApplicationRegisterKey][hookName](...args)
} catch (e) {
Logger.error (`调用钩子函数 ${hookName} 失败:`, e)
}
}
}
class StyleService extends Component {
init () {
this.styles = new Map ()
}
/**
* 更新样式
*
* @param key 样式的 key,字符串对象
* @param styleContent 样式,字符串对象
*/
updateStyle (key, styleContent) {
this.removeStyle (key)
const styleNode = document.createElement ('style')
styleNode.textContent = styleContent
document.head.appendChild (styleNode)
this.styles.set (key, styleNode)
}
/**
* 移除样式
*
* @param key 样式的 key,字符串对象
*/
removeStyle (key) {
let styleNode = this.styles.get (key)
if (styleNode) {
document.head.removeChild (styleNode)
this.styles.delete (key)
}
}
}
class JavaScriptService extends Component {
init () {
this.javaScriptNodes = new Map ()
}
updateJavaScript (key, textContent) {
this.removeJavaScript (key)
const scriptNode = GM_addElement ('script', {
textContent: textContent
});
this.javaScriptNodes.set (key, scriptNode)
}
removeJavaScript (key) {
let scriptNode = this.javaScriptNodes.get (key)
if (scriptNode) {
document.head.removeChild (scriptNode)
this.javaScriptNodes.delete (key)
}
}
}
class MessageService extends Component {
init () {
this.messages = new Map ();
}
/**
* 解析消息元素,获取消息的所有内容。由于网页中不存在时间戳,所以时间戳使用当前时间代替。
* 调用该方法只需要消息元素,一般用于从页面实时监测获取到的消息。
*
* @param messageDiv 消息元素,包含 data-message-id 属性 的 div 元素
* @returns {MessageBO|undefined} 返回消息业务对象,如果消息元素不存在则返回 undefined
*/
parseMessageDiv (messageDiv) {
if (!messageDiv) {
return;
}
const messageId = messageDiv.getAttribute ('data-message-id');
const role = messageDiv.getAttribute ('data-message-author-role');
const messageElementBO = this.getMessageElement (messageId)
if (!messageElementBO) {
return;
}
let timestamp = new Date ().getTime ();
const message = messageElementBO.messageEle.innerHTML;
if (!this.messages.has (messageId)) {
const messageBO = new MessageBO (messageId, role, timestamp, message);
this.addMessage (messageBO)
}
return this.messages.get (messageId);
}
/**
* 添加消息,主要用于添加从 API 劫持到的消息列表。
* 调用该方法需要已知消息的所有内容,如果只知道消息元素则应该使用 parseMessageDiv 方法获取消息业务对象。
*
* @param message 消息业务对象
* @param force 是否强制添加,如果为 true 则强制添加,否则如果消息已经存在则不添加
* @returns {boolean} 返回是否添加成功
*/
addMessage (message, force = false) {
if (this.messages.has (message.messageId) && !force) {
return false;
}
this.messages.set (message.messageId, message);
return true
}
/**
* 移除消息
*
* @param messageId 消息 ID
* @returns {boolean} 返回是否移除成功
*/
removeMessage (messageId) {
return this.messages.delete (messageId)
}
/**
* 通过消息 ID 获取消息元素业务对象
*
* @param messageId 消息 ID
* @returns {MessageElementBO|undefined} 返回消息元素业务对象
*/
getMessageElement (messageId) {
const messageDiv = document.body.querySelector (`div [data-message-id="${messageId}"]`);
if (!messageDiv) {
return;
}
const rootDiv = messageDiv.parentElement;
return new MessageElementBO (rootDiv, messageDiv);
}
/**
* 通过消息 ID 获取消息业务对象
* @param messageId
* @returns {any}
*/
getMessage (messageId) {
return this.messages.get (messageId);
}
/**
* 显示所有消息信息
*/
showMessages () {
Logger.table (Array.from (this.messages.values ()));
}
}
class MonitorService extends Component {
constructor () {
super ();
this.messageService = null
this.timeRendererService = null
this.dependencies = [{field: 'messageService', clazz: MessageService}, {
field: 'timeRendererService', clazz: TimeRendererService
},]
}
init () {
this.totalTime = 0;
this.originalFetch = window.fetch;
this._initMonitorFetch ();
this._initMonitorAddedMessageNode ();
this._initConfigPageNode ();
}
/**
* 初始化劫持 fetch 方法,用于监控 ChatGPT 的消息数据
*
* @private
*/
_initMonitorFetch () {
const that = this;
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}$");
unsafeWindow.fetch = (...args) => {
return that.originalFetch.apply (this, args)
.then (response => {
if (urlRegex.test (response.url)) {
// 克隆响应对象以便独立处理响应体
const clonedResponse = response.clone ();
clonedResponse.json ().then (data => {
that._parseConversationJsonData (data);
}).catch (error => Logger.error (' 解析响应体失败:', error));
}
return response;
});
};
}
/**
* 解析从 API 获取到的消息数据,该方法存在报错风险,需要在调用时捕获异常以防止中断后续操作。
*
* @param obj 从 API 获取到的消息数据
* @private
*/
_parseConversationJsonData (obj) {
const mapping = obj.mapping
const messageIds = []
for (let key in mapping) {
const message = mapping [key].message
if (message) {
const messageId = message.id
const role = message.author.role
const createTime = message.create_time
const messageBO = new MessageBO (messageId, role, createTime * 1000)
messageIds.push (messageId)
this.messageService.addMessage (messageBO, true)
}
}
this.timeRendererService.addMessageArrayToBeRendered (messageIds.reverse ())
this.messageService.showMessages ()
}
/**
* 初始化监控节点变化,用于监控在使用 ChatGPT 期间实时输入的消息。
* 每隔 500ms 检查一次 main 节点是否存在,如果存在则开始监控节点变化。
* @private
*/
_initMonitorAddedMessageNode () {
const interval = setInterval (() => {
const mainElement = document.querySelector ('main');
if (mainElement) {
this._setupMonitorAddedMessageNode (mainElement);
clearInterval (interval); // 清除定时器,停止进一步检查
}
}, 500);
}
/**
* 设置监控节点变化,用于监控在使用 ChatGPT 期间实时输入的消息。
* @param supervisedNode 监控在此节点下的节点变化,确保新消息的节点在此节点下
* @private
*/
_setupMonitorAddedMessageNode (supervisedNode) {
const that = this;
const callback = function (mutationsList, observer) {
const start = new Date ().getTime ();
for (const mutation of mutationsList) {
let messageDiv = null;
if (mutation.type === 'childList') {
for (let node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
messageDiv = node.querySelector ('div [data-message-id]');
if (!messageDiv && node.hasAttribute ('data-message-id')) {
messageDiv = node
break
}
}
}
} else if (mutation.type === 'attributes' && mutation.attributeName === 'data-message-id') {
messageDiv = mutation.target;
}
if (messageDiv !== null) {
const messageBO = that.messageService.parseMessageDiv (messageDiv);
if (messageBO) {
that.timeRendererService.addMessageToBeRendered (messageBO.messageId);
that.messageService.showMessages ()
}
}
}
const end = new Date ().getTime ();
that.totalTime += (end - start);
};
const observer = new MutationObserver (callback);
observer.observe (supervisedNode, {childList: true, subtree: true, attributes: true});
}
/**
* 初始化配置页面节点,以便显示配置页面的时间渲染效果
* @private
*/
_initConfigPageNode () {
if (IsConfigPage) {
const messageIds = []
const messageDivs = document.querySelectorAll ('div [data-message-id]');
for (let messageDiv of messageDivs) {
const dataMessageId = messageDiv.getAttribute ('data-message-id');
const timestamp = parseInt (messageDiv.getAttribute ('data-chatgpt-with-date-demo-timestamp'))
const messageBO = new MessageBO (dataMessageId, 'ConfigDemo', timestamp)
messageIds.push (dataMessageId)
this.messageService.addMessage (messageBO, true)
}
this.timeRendererService.addMessageArrayToBeRendered (messageIds)
}
}
}
class TimeRendererService extends Component {
constructor () {
super ();
this.messageService = null
this.userConfig = null
this.styleService = null
this.javaScriptService = null
this.hookService = null
this.dependencies = [
{field: 'messageService', clazz: MessageService},
{field: 'userConfig', clazz: UserConfig},
{field: 'styleService', clazz: StyleService},
{field: 'javaScriptService', clazz: JavaScriptService},
{field: 'hookService', clazz: HookService},
]
}
init () {
this.messageToBeRendered = []
this.messageCountOfFailedToRender = new Map ()
this._setStyleAndJavaScript ()
this._initRender ()
}
/**
* 若为高级模式则设置用户自定义的样式和脚本,否则设置默认样式
*
* @private
*/
_setStyleAndJavaScript () {
this.styleService.updateStyle (SystemConfig.TimeRender.BasicStyleKey, SystemConfig.TimeRender.BasicStyle)
this.hookService.reset2DefaultHooks ()
this.styleService.removeStyle (SystemConfig.TimeRender.AdditionalStyleKey)
this.javaScriptService.removeJavaScript (SystemConfig.TimeRender.AdditionalScriptKey)
if (this.userConfig.timeRender.advanced.enable) {
this.styleService.updateStyle (SystemConfig.TimeRender.AdditionalStyleKey, this.userConfig.timeRender.advanced.styleTextContent)
this.javaScriptService.updateJavaScript (SystemConfig.TimeRender.AdditionalScriptKey, this.userConfig.timeRender.advanced.scriptTextContent)
}
}
/**
* 添加消息 ID 到待渲染队列
* @param messageId 消息 ID
*/
addMessageToBeRendered (messageId) {
if (typeof messageId !== 'string') {
return
}
this.messageToBeRendered.push (messageId)
Logger.debug (`添加 ID ${messageId} 到待渲染队列,当前队列 ${this.messageToBeRendered}`)
}
/**
* 添加消息 ID 到待渲染队列
* @param messageIdArray 消息 ID 数组
*/
addMessageArrayToBeRendered (messageIdArray) {
if (!messageIdArray || !(messageIdArray instanceof Array)) {
return
}
this.messageToBeRendered.push (...messageIdArray)
Logger.debug (`添加 ID ${messageIdArray} 到待渲染队列,当前队列 ${this.messageToBeRendered}`)
}
/**
* 初始化渲染时间的定时器,每隔 SystemConfig.TimeRender.Interval 毫秒处理一次待渲染队列
* 1. 备份待渲染队列
* 2. 清空待渲染队列
* 3. 遍历备份的队列,逐个渲染
* 3.1 如果渲染失败则重新加入待渲染队列,失败次数加一
* 3.2 如果渲染成功,清空失败次数
* 4. 重复 1-3 步骤
* 5. 如果失败次数超过 SystemConfig.TimeRender.RenderRetryCount 则不再尝试渲染,即不再加入待渲染队列。同时清空失败次数。
*
* @private
*/
_initRender () {
const that = this
async function processTimeRender () {
const start = new Date ().getTime ();
let completeCount = 0;
let totalCount = that.messageToBeRendered.length;
const messageToBeRenderedClone = that.messageToBeRendered.slice ()
that.messageToBeRendered = []
let count = 0;
for (let messageId of messageToBeRenderedClone) {
count++;
if (count <= SystemConfig.TimeRender.BatchSize && new Date ().getTime () - start <= SystemConfig.TimeRender.BatchTimeout) {
const result = await that._renderTime (messageId)
if (!result) {
let countOfFailed = that.messageCountOfFailedToRender.get (messageId)
if (countOfFailed && countOfFailed >= SystemConfig.TimeRender.RenderRetryCount) {
Logger.debug (`ID ${messageId} 渲染失败次数超过 ${SystemConfig.TimeRender.RenderRetryCount} 次,将不再尝试。`)
that.messageCountOfFailedToRender.delete (messageId)
that.messageService.removeMessage (messageId)
} else {
that.messageToBeRendered.push (messageId);
if (countOfFailed) {
that.messageCountOfFailedToRender.set (messageId, countOfFailed + 1)
} else {
that.messageCountOfFailedToRender.set (messageId, 1)
}
}
} else {
completeCount++
that.messageCountOfFailedToRender.delete (messageId)
}
Logger.debug (`ID ${messageId} 渲染 ${result ? ' 成功 ' : ' 失败 '},当前渲染进度 ${completeCount}/${totalCount},该批次耗时 ${new Date ().getTime () - start} ms`)
} else {
for (let i = count; i < messageToBeRenderedClone.length; i++) {
that.messageToBeRendered.push (messageToBeRenderedClone [i])
}
if (count > SystemConfig.TimeRender.BatchSize) {
Logger.debug (`本批次渲染数量超过 ${SystemConfig.TimeRender.BatchSize},将继续下一批次渲染。`)
break;
}
if (new Date ().getTime () - start > SystemConfig.TimeRender.BatchTimeout) {
Logger.debug (`本批次渲染超时,将继续下一批次渲染。`)
break;
}
}
}
const end = new Date ().getTime ();
if (totalCount > 0) {
Logger.debug (`处理当前 ID 队列渲染 ${messageToBeRenderedClone} 耗时 ${end - start} ms`)
}
setTimeout (processTimeRender, SystemConfig.TimeRender.Interval);
}
processTimeRender ().then (r => Logger.debug (' 初始化渲染时间定时器完成 '))
}
/**
* 将时间渲染到目标位置,如果检测到目标位置已经存在时间元素则更新时间,否则创建时间元素并插入到目标位置。
*
* @param messageId 消息 ID
* @returns {Promise} 返回是否渲染成功的 Promise 对象
* @private
*/
_renderTime (messageId) {
return new Promise (resolve => {
const messageElementBo = this.messageService.getMessageElement (messageId);
const messageBo = this.messageService.getMessage (messageId);
if (!messageElementBo || !messageBo) resolve (false)
const timeElement = messageElementBo.rootEle.querySelector (`.${SystemConfig.TimeRender.TimeClassName}`);
const role = messageElementBo.messageEle.getAttribute ('data-message-author-role');
const element = this._createTimeElement (messageBo.timestamp, role);
// 强制移除时间元素,重新渲染。这样才能保证时间正确的同时也能正确执行用户自定义的脚本。
if (timeElement) {
messageElementBo.rootEle.removeChild (timeElement)
}
this.hookService.invokeHook ('beforeCreateTimeTag', messageId, element.timeTagContainer)
messageElementBo.rootEle.firstChild.insertAdjacentHTML ('beforebegin', element.timeTagContainer);
this.hookService.invokeHook ('afterCreateTimeTag', messageId, messageElementBo.rootEle.querySelector (`.${SystemConfig.TimeRender.TimeClassName}`))
resolve (true)
})
}
/**
* 创建时间元素,如果开启高级模式则使用用户自定义的时间格式,否则使用默认时间格式。
*
* @param timestamp 时间戳,浮点数或整数类型,单位毫秒,例如 1714398759.26881、1714398759
* @returns {{timeTagFormated, timeTagContainer: string}} 返回格式化后的时间标签 和 包含时间标签的容器的 HTML 字符串
* @private
*/
_createTimeElement (timestamp, role) {
let timeTagFormated = ''
if (this.userConfig.timeRender.advanced.enable) {
timeTagFormated = this.hookService.invokeHook ('formatDateTimeByDate', new Date (timestamp), this.userConfig.timeRender.advanced.htmlTextContent)
} else {
timeTagFormated = this.hookService.invokeHook ('formatDateTimeByDate', new Date (timestamp), this.userConfig.timeRender.format);
}
const timeTagContainer = `${timeTagFormated}`;
return {
timeTagFormated, timeTagContainer,
};
}
/**
* 清除所有时间元素
* @private
*/
_cleanAllTimeElements () {
const timeElements = document.body.querySelectorAll (`.${SystemConfig.TimeRender.TimeClassName}`);
timeElements.forEach (ele => {
ele.remove ()
})
}
/**
* 重新渲染时间元素,强制拉取所有消息 ID 重新渲染
*/
reRender () {
this._setStyleAndJavaScript ()
this._cleanAllTimeElements ()
this.addMessageArrayToBeRendered (Array.from (this.messageService.messages.keys ()))
}
}
class ConfigPanelService extends Component {
constructor () {
super ();
this.userConfig = null
this.styleService = null
this.timeRendererService = null
this.messageService = null
this.javascriptService = null
this.dependencies = [
{field: 'userConfig', clazz: UserConfig},
{field: 'styleService', clazz: StyleService},
{field: 'timeRendererService', clazz: TimeRendererService},
{field: 'messageService', clazz: MessageService},
{field: 'javascriptService', clazz: JavaScriptService},
]
}
/**
* 初始化配置面板,强调每个子初始化方法阻塞式的执行,即一个初始化方法执行完毕后再执行下一个初始化方法。
* @returns {Promise}
*/
async init () {
if (IsConfigPage) {
this.appID = SystemConfig.ConfigPanel.AppID
Logger.debug (' 开始初始化配置面板 ')
await this._initExternalResources ()
Logger.debug (' 初始化脚本完成 ')
this._initVariables ()
await this._initStyle ()
Logger.debug (' 初始化样式完成 ')
await this._initPanel ()
Logger.debug (' 初始化面板完成 ')
this._initVue ()
Logger.debug (' 初始化 Vue 完成 ')
this._initConfigPanelSizeAndPosition ()
this._initConfigPanelEventMonitor ()
Logger.debug (' 初始化配置面板事件监控完成 ')
this.show ()
}
this._initMenuCommand ()
Logger.debug (' 初始化菜单命令完成 ')
}
/**
* 初始化配置面板的 HTML 与 Vue 实例的配置属性。集中管理以便方便修改。
* @private
*/
_initVariables () {
const that = this
unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey] = {}
const GPTPrompt = Utils.base64Decode (SystemConfig.Common.GPTPrompt)
const TimeTagComponent = {
props: ['html'],
render () {
return Vue.h ('div', {innerHTML: this.html});
},
}
this.panelStyle = `
.v-binder-follower-container {
position: fixed;
}
.n-button .n-button__content {
white-space: pre-wrap;
}
#CWD-Configuration-Panel {
position: absolute;
top: 50px;
left: 50px;
width: 600px;
background-color: #FFFFFF;
border: #D7D8D9 1px solid;
border-radius: 8px;
resize: horizontal;
min-width: 200px;
overflow: auto;
color: black;
opacity: 0.95;
}
#CWD-Configuration-Panel .status-bar {
cursor: move;
background-color: #ECECEA;
border-radius: 4px 4px 0 0;
display: flex;
}
#CWD-Configuration-Panel .status-bar .title {
display: flex;
align-items: center;
justify-content: left;
padding-left: 10px;
user-select: none;
color: #777;
flex: 1;
font-weight: bold;
}
#CWD-Configuration-Panel .status-bar .button {
cursor: pointer;
padding: 10px;
transition: color 0.3s;
}
#CWD-Configuration-Panel .status-bar .button:hover {
color: #f00;
}
#CWD-Configuration-Panel .container {
padding: 20px 20px 0;
}
#CWD-Configuration-Panel .container .code-block {
padding: 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
#CWD-Configuration-Panel .operation-group {
background-color: #ECECEA;
border-radius: 8px;
display: flex;
justify-content: center;
gap: 10px;
padding: 20px 0;
}
#CWD-Configuration-Panel .operation-group > button {
width: 30%;
}`
this.panelHTML = `
{{title}}
${SystemConfig.ConfigPanel.Icon.Restore}
{{ map2text ('restore-info') }}
{{ map2text ('restore-warn') }}
${SystemConfig.ConfigPanel.Icon.Language}
{{ map2text ('toggle-language-info') }}
${SystemConfig.ConfigPanel.Icon.Documentation}
{{ map2text ('documentation-info') }}
{{ map2text ('documentation-international-access') }}
{{ map2text ('documentation-china-access') }}
${SystemConfig.ConfigPanel.Icon.Maximize}
${SystemConfig.ConfigPanel.Icon.Minimize}
${SystemConfig.ConfigPanel.Icon.Close}
{{ map2text ('gpt-prompt-info') }}
{{map2text ('reset')}}
{{map2text ('apply')}}
{{map2text ('save')}}
`
this.appConfig = {
el: `#${that.appID}`,
data () {
return {
date: new Date (),
hljs: hljs,
title: "ChatGPTWithDate",
formatOptions: SystemConfig.TimeRender.TimeTagTemplates.map (item => {
return {label: item, value: item}
}),
configForm: {
format: that.userConfig.timeRender.format,
advanced: {
enable: that.userConfig.timeRender.advanced.enable,
htmlTextContent: that.userConfig.timeRender.advanced.htmlTextContent,
styleTextContent: that.userConfig.timeRender.advanced.styleTextContent,
scriptTextContent: that.userConfig.timeRender.advanced.scriptTextContent,
},
},
config: {
format: that.userConfig.timeRender.format,
mode: that.userConfig.timeRender.mode,
advanced: {
enable: that.userConfig.timeRender.advanced.enable,
htmlTextContent: that.userConfig.timeRender.advanced.htmlTextContent,
styleTextContent: that.userConfig.timeRender.advanced.styleTextContent,
scriptTextContent: that.userConfig.timeRender.advanced.scriptTextContent,
},
},
locale: null,
i18n: that.userConfig.i18n,
folding: false,
configDirty: false,
configPanel: {
display: true,
},
};
},
methods: {
onApply () {
const jsValidResult = Utils.isJavaScriptSyntaxValid (this.configForm.advanced.scriptTextContent)
if (!jsValidResult.valid) {
that.notification.error ({
title: this.map2text ('js-invalid-info'),
content: jsValidResult.error.toString (),
duration: 3000,
keepAliveOnHover: true,
});
return false
}
this.config = JSON.parse (JSON.stringify (this.configForm));
that.updateConfig ({timeRender: this.config})
this.configDirty = false
return true
},
onConfirm () {
this.onApply ()
this.onClose ()
},
onReset () {
this.configForm = JSON.parse (JSON.stringify (this.config));
this.configDirty = false
},
onConfigUpdate () {
this.configDirty = true
},
renderLabel (option) {
return Vue.h (TimeTagComponent, {
html: option.label,
})
},
reFormatTimeHTML (html) {
return Utils.formatDateTimeByDate (this.date, html)
},
insertTab (event) {
if (!event.shiftKey) { // 确保不是 Shift + Tab 组合
event.preventDefault (); // 阻止 Tab 键的默认行为
// 尝试使用 execCommand 插入四个空格
if (document.queryCommandSupported ('insertText')) {
document.execCommand ('insertText', false, ' ');
} else { // 如果浏览器不支持 execCommand,回退到原始方法(不支持撤销)
const start = event.target.selectionStart;
const end = event.target.selectionEnd;
const value = event.target.value;
event.target.value = value.substring (0, start) + " " + value.substring (end);
// 移动光标位置到插入的空格后
event.target.selectionStart = event.target.selectionEnd = start + 4;
// 触发 input 事件更新 v-model
this.$nextTick (() => {
event.target.dispatchEvent (new Event ('input'));
});
}
}
},
onClose () {
that.hide ()
},
toggleFolding () {
this.folding = !this.folding
},
onRestore () {
// 清空所有 GM 存储的数据
const keys = GM_listValues ();
keys.forEach (key => {
GM_deleteValue (key);
});
// 重置配置至出厂设置
const defaultConfig = that.userConfig.getDefaultConfig ()
this.config = defaultConfig.timeRender
this.i18n = defaultConfig.i18n
// 重置 Vue 表单
this.onReset ()
// 重置样式与脚本
that.updateConfig (defaultConfig)
},
toggleLanguage () {
let index = SystemConfig.ConfigPanel.I18N.supported.indexOf (this.i18n)
if (index === -1) {
Logger.error ("嗯?当前语言未知?")
}
index = (index + 1) % SystemConfig.ConfigPanel.I18N.supported.length;
this.i18n = SystemConfig.ConfigPanel.I18N.supported [index]
that.userConfig.updateOne ('i18n', this.i18n)
this.setNaiveUILanguage ()
},
setNaiveUILanguage () {
this.locale = this.i18n === 'zh' ? naive.zhCN : null;
},
onDocumentation () {
},
openUrl (url, target) {
window.open (url, target)
},
copyGPTPrompt () {
navigator.clipboard.writeText (GPTPrompt).then (() => {
that.notification.success ({
content: this.map2text ('copy-success-info'),
duration: 1000,
});
})
},
},
computed: {
map2text () {
return key => {
const language = this.i18n;
if (!SystemConfig.ConfigPanel.I18N [language]) {
Logger.error (`当前语言 ${language} 不受支持!已回退至 ${SystemConfig.ConfigPanel.I18N.default}`);
return SystemConfig.ConfigPanel.I18N [SystemConfig.ConfigPanel.I18N.default][key] || 'NULL';
}
if (!SystemConfig.ConfigPanel.I18N [language][key]) {
Logger.debug (`当前语言 ${language} 不存在键为 ${key} 的 i18n 支持,已回退至 ${SystemConfig.ConfigPanel.I18N.default}`);
return SystemConfig.ConfigPanel.I18N [SystemConfig.ConfigPanel.I18N.default][key] || 'NULL';
}
return SystemConfig.ConfigPanel.I18N [language][key];
};
}
},
created () {
this.date = new Date ()
this.formatOptions.forEach (item => {
item.label = this.reFormatTimeHTML (item.value)
})
},
mounted () {
this.timestampInterval = setInterval (() => {
// 满足打开状态且高级模式开启时才更新时间,避免不必要的性能消耗
if (that.isShow ()) {
this.date = new Date ()
this.formatOptions.forEach (item => {
item.label = this.reFormatTimeHTML (item.value)
})
}
}, 50)
this.setNaiveUILanguage ()
},
beforeUnmount () {
clearInterval (this.timestampInterval)
},
}
}
/**
* 初始化样式。
* 同时为了避免样式冲突,在 head 元素中加入一个 元素,
* naive-ui 会把所有的样式刚好插入这个元素的前面。
* 参考 https://www.naiveui.com/zh-CN/os-theme/docs/style-conflict
*
* @returns {Promise}
* @private
*/
_initStyle () {
return new Promise (resolve => {
const meta = document.createElement ('meta');
meta.name = 'naive-ui-style'
document.head.appendChild (meta);
this.styleService.updateStyle (SystemConfig.ConfigPanel.StyleKey, this.panelStyle)
resolve ()
})
}
/**
* 初始化 Vue 与 Naive UI 脚本。无法使用 到页面中。
*
* @returns {Promise}
* @private
*/
_initExternalResources () {
return new Promise (resolve => {
let completeCount = 0;
const resources = [
{type: 'js', url: 'https://unpkg.com/vue@3.4.26/dist/vue.global.js'},
{type: 'js', url: 'https://unpkg.com/naive-ui@2.38.1/dist/index.js'},
{type: 'css', url: 'https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/default.min.css'},
{type: 'js', url: 'https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js'},
]
//const addScript = (content) => {
// let script = document.createElement ('script');
// script.textContent = content;
// //document.head.appendChild (script);
// iframeDocument.head.appendChild (script);
// completeCount++;
// if (completeCount === resources.length) {
// resolve ()
// }
// }
//const addStyle = (content) => {
// let style = document.createElement ('style');
// style.textContent = content;
// //document.head.appendChild (style);
// iframeDocument.head.appendChild (style);
// completeCount++;
// if (completeCount === resources.length) {
// resolve ()
// }
// }
resources.forEach (resource => {
let element = null
if (resource.type === 'js') {
element = GM_addElement ('script', {
src: resource.url,
type: 'text/javascript'
});
} else if (resource.type === 'css') {
element = GM_addElement ('link', {
rel: 'stylesheet',
type: 'text/css',
href: resource.url
});
}
element.onload = () => {
completeCount++;
if (completeCount === resources.length) {
resolve ()
}
}
// GM_xmlhttpRequest ({
// method: "GET", url: resource.url, onload: function (response) {
// if (resource.type === 'js') {
// addScript (response.responseText);
// } else if (resource.type === 'css') {
// addStyle (response.responseText);
// }
// }
// });
})
// 以下方法有 CSP 限制
//const naiveScript = document.createElement ('script');
//naiveScript.setAttribute ("type", "text/javascript");
//naiveScript.text = "https://unpkg.com/naive-ui@2.38.1/dist/index.js";
//document.documentElement.appendChild (naiveScript);
})
}
/**
* 初始化配置面板,插入配置面板的 HTML 到 body 中。
*
* @returns {Promise}
* @private
*/
_initPanel () {
const that = this
return new Promise (resolve => {
const panelRoot = document.createElement ('div');
panelRoot.innerHTML = that.panelHTML;
document.body.appendChild (panelRoot);
resolve ()
})
}
/**
* 初始化 Vue 实例,挂载到配置面板的 HTML 元素上。
*
* @private
*/
_initVue () {
const app = Vue.createApp (this.appConfig);
app.use (naive)
const {notification} = naive.createDiscreteApi (
["notification"], {
configProviderProps: {
theme: naive.lightTheme
}
},);
this.notification = notification
app.mount (`#${this.appID}`);
}
/**
* 初始化配置面板大小与位置
*
* @private
*/
_initConfigPanelSizeAndPosition () {
const panel = document.getElementById (this.appID)
// 获取存储的大小
const size = GM_getValue (SystemConfig.GMStorageKey.ConfigPanel.Size, {})
if (size && size.width && !isNaN (size.width)) {
panel.style.width = size.width + 'px';
}
// 获取存储的位置
const position = GM_getValue (SystemConfig.GMStorageKey.ConfigPanel.Position, {})
if (position && position.left && position.top && !isNaN (position.left) && !isNaN (position.top)) {
const {left, top} = position
const {refineLeft, refineTop} = this.refinePosition (left, top, panel.offsetWidth, panel.offsetHeight)
panel.style.left = refineLeft + 'px';
panel.style.top = refineTop + 'px';
}
// 如果面板任何一边超出屏幕,则重置位置
//const rect = panel.getBoundingClientRect ()
//const leftTop = {
// x: rect.left,
// y: rect.top
// }
//const rightBottom = {
// x: rect.left + rect.width,
// y: rect.top + rect.height
// }
//const screenWidth = window.innerWidth;
//const screenHeight = window.innerHeight;
//if (leftTop.x < 0 || leftTop.y < 0 || rightBottom.x > screenWidth || rightBottom.y > screenHeight) {
// panel.style.left = '50px';
// panel.style.top = '50px';
// }
}
/**
* 初始化配置面板事件监控,包括面板拖动、面板大小变化等事件。
*
* @private
*/
_initConfigPanelEventMonitor () {
const that = this
const panel = document.getElementById (this.appID)
const draggableArea = document.getElementById (`${this.appID}-DraggableArea`)
// 监听面板宽度变化
const resizeObserver = new ResizeObserver (entries => {
for (let entry of entries) {
if (entry.contentRect.width) {
GM_setValue (SystemConfig.GMStorageKey.ConfigPanel.Size, {
width: entry.contentRect.width,
})
}
}
});
resizeObserver.observe (panel);
// 监听面板位置
draggableArea.addEventListener ('mousedown', function (e) {
const offsetX = e.clientX - draggableArea.getBoundingClientRect ().left;
const offsetY = e.clientY - draggableArea.getBoundingClientRect ().top;
function mouseMoveHandler (e) {
const left = e.clientX - offsetX;
const top = e.clientY - offsetY;
const {
refineLeft, refineTop
} = that.refinePosition (left, top, panel.offsetWidth, panel.offsetHeight);
panel.style.left = refineLeft + 'px';
panel.style.top = refineTop + 'px';
GM_setValue (SystemConfig.GMStorageKey.ConfigPanel.Position, {
left: refineLeft, top: refineTop,
})
}
document.addEventListener ('mousemove', mouseMoveHandler);
document.addEventListener ('mouseup', function () {
document.removeEventListener ('mousemove', mouseMoveHandler);
});
});
}
/**
* 限制面板位置,使其任意一部分都不超出屏幕
*
* @param left 面板左上角 x 坐标
* @param top 面板左上角 y 坐标
* @param width 面板宽度
* @param height 面板高度
* @returns {{refineLeft: number, refineTop: number}} 返回修正后的坐标
*/
refinePosition (left, top, width, height) {
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
return {
refineLeft: Math.min (Math.max (0, left), screenWidth - width),
refineTop: Math.min (Math.max (0, top), screenHeight - height),
}
}
/**
* 初始化菜单命令,用于在 Tampermonkey 的菜单中添加一个配置面板的命令。
*
* @private
*/
_initMenuCommand () {
let that = this
if (IsConfigPage) {
GM_registerMenuCommand ("配置面板", () => {
that.show ()
})
} else {
GM_registerMenuCommand ("配置面板(中国访问)", () => {
GM_openInTab ("https://project.coderjiang.com/chatgpt-with-date-config-page/");
})
GM_registerMenuCommand ("Configuration Panel (International Access)", () => {
GM_openInTab ("https://jiang-taibai.github.io/chatgpt-with-date-config-page/");
})
}
}
/**
* 显示配置面板
*/
show () {
document.getElementById (this.appID).style.visibility = 'visible';
unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey].visibility = true
}
/**
* 隐藏配置面板
*/
hide () {
document.getElementById (this.appID).style.visibility = 'hidden';
unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey].visibility = false
}
isShow () {
return unsafeWindow [SystemConfig.Main.WindowRegisterKey][SystemConfig.ConfigPanel.ApplicationRegisterKey].visibility
}
/**
* 更新配置,由 Vue 组件调用来更新配置并重新渲染时间
* @param config
*/
updateConfig (config) {
this.userConfig.update (config)
this.timeRendererService.reRender ()
}
}
class Main {
static ComponentsConfig = [
UserConfig, StyleService, MessageService,
MonitorService, TimeRendererService,
JavaScriptService, HookService,
ConfigPanelService,
]
constructor () {
for (let componentClazz of Main.ComponentsConfig) {
const instance = new componentClazz ();
this [componentClazz.name] = instance
ComponentLocator.register (componentClazz, instance)
}
}
/**
* 获取依赖关系图
* @returns {[]} 依赖关系图,例如 [{node:'ComponentClass0', dependencies:['ComponentClass2', 'ComponentClass3']}, ...]
* @private
*/
_getDependencyGraph () {
const dependencyGraph = []
for (let componentClazz of Main.ComponentsConfig) {
const dependencies = this [componentClazz.name].dependencies.map (dependency => dependency.clazz.name)
dependencyGraph.push ({node: componentClazz.name, dependencies})
}
Logger.debug (' 依赖关系图:', JSON.stringify (dependencyGraph))
return dependencyGraph
}
/**
* 注册全局变量
* @private
*/
_registerGlobalVariables () {
unsafeWindow [SystemConfig.Main.WindowRegisterKey] = {}
}
start () {
this._registerGlobalVariables ()
const dependencyGraph = this._getDependencyGraph ()
const order = Utils.dependencyAnalysis (dependencyGraph)
Logger.debug (' 初始化顺序:', order.join (' -> '))
for (let componentName of order) {
this [componentName].initDependencies ()
}
for (let componentName of order) {
this [componentName].init ()
}
}
}
const main = new Main ();
main.start ();
})();