// ==UserScript== // @name ChatGPT with Date // @name:en ChatGPT with Date // @name:zh-CN ChatGPT with Date // @namespace https://github.com/jiang-taibai/chatgpt-with-date // @version 2.0.3 // @description 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。 // @description:zh-cn 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。 // @description:en Tampermonkey plugin for displaying ChatGPT historical and real-time conversation time. // @author CoderJiang // @license MIT // @match *://chat.openai.com/* // @match *://chatgpt.com/* // @match *://jiang-taibai.github.io/chatgpt-with-date-config-page* // @icon https://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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const IsConfigPage = window.location.hostname === 'jiang-taibai.github.io' class SystemConfig { static Common = { ApplicationName: 'ChatGPT with Date', } 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 代码无效', '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', }, 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', 'gpt-prompt': 'IyAxLiBUYXNrIE92ZXJ2aWV3CgpZb3UgbmVlZCB0byB3cml0ZSBIVE1MLCBDU1MsIGFuZCBKYXZhU2NyaXB0IGNvZGUgdG8gbWVldCBteSByZXF1aXJlbWVudHMuIEkgd2lsbCBwcm92aWRlIGRldGFpbGVkIGluc3RydWN0aW9ucyBvbiBob3cgdG8Kd3JpdGUgdGhlIGNvZGUuCgojIDIuIEhUTUwgUmVxdWlyZW1lbnRzCgpZb3UgbmVlZCB0byBjcmVhdGUgYW4gSFRNTCBzdHJpbmcgdGVtcGxhdGUgZm9yIGRhdGUgYW5kIHRpbWUsIHVzaW5nIHBsYWNlaG9sZGVycyB0byByZXByZXNlbnQgdGltZSBlbGVtZW50cy4gRm9yCmV4YW1wbGU6CgpgYGBodG1sCjxkaXYgY2xhc3M9InRleHQtdGFnLWJveCI+CiAgICA8c3BhbiBjbGFzcz0iZGF0ZSI+e3l5eXl9LXtNTX0te2RkfTwvc3Bhbj4KICAgIDxzcGFuIGNsYXNzPSJ0aW1lIj57SEh9OnttbX06e3NzfTwvc3Bhbj4KPC9kaXY+CmBgYAoKSSB3aWxsIGxhdGVyIGV4cGxhaW4gaG93IHRvIHVzZSBKYXZhU2NyaXB0IHRvIGRpc3BsYXkgc3BlY2lmaWMgdGltZXMuCgojIDMuIENTUyBSZXF1aXJlbWVudHMKCigxKSBEbyBub3QgdXNlIHRhZyBzZWxlY3RvcnM7IG9ubHkgdXNlIGNsYXNzIHNlbGVjdG9ycyBvciBJRCBzZWxlY3RvcnMuICAKKDIpIFVzZSBkZXNjZW5kYW50IHNlbGVjdG9ycyBhcyBtdWNoIGFzIHBvc3NpYmxlIHRvIGF2b2lkIHBvbGx1dGluZyBnbG9iYWwgc3R5bGVzLiAgCigzKSBUcnkgdG8gYXZvaWQgdXNpbmcgYCFpbXBvcnRhbnRgLgoKIyA0LiBKYXZhU2NyaXB0IFJlcXVpcmVtZW50cwoKIyMgNC4xIFByb3ZpZGVkIEFQSSBJbnRlcmZhY2UKCkFQSXMgYXJlIGRlZmluZWQgb24gdGhlIHdpbmRvdyBvYmplY3QuIFlvdSBtYXkgbmVlZCB0byBvdmVycmlkZSB0aGVzZSBmdW5jdGlvbnMgaW4geW91ciBKUyBzY3JpcHQgaWYgbmVjZXNzYXJ5LgoKLSBgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSlgOiBSZXBsYWNlIHRoZSBjb250ZW50IGluIHRoZSB0ZW1wbGF0ZSBIVE1MIHN0cmluZwogIHdpdGggdGhlIHRpbWUgc3BlY2lmaWVkIGJ5IHRoZSBgZGF0ZWAgb2JqZWN0LgogICAgLSBgZGF0ZWA6IEphdmFTY3JpcHQgRGF0ZSBpbnN0YW5jZS4KICAgIC0gYHRlbXBsYXRlYDogSFRNTCBzdHJpbmcsIHdoaWNoIGlzIHlvdXIgSFRNTCBjb2RlLgogICAgLSBSZXR1cm5zOiBUaGUgSFRNTCBjb2RlIGFmdGVyIGZvcm1hdHRpbmcgdGhlIHRpbWUuCi0gYHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYmVmb3JlQ3JlYXRlVGltZVRhZyhtZXNzYWdlSWQsIHRpbWVUYWdIVE1MKWA6IENhbGxlZCBiZWZvcmUgaW5zZXJ0aW5nIHRoZSB0ZW1wbGF0ZSBpbnRvCiAgdGhlIHBhZ2UuCiAgICAtIGBtZXNzYWdlSWRgOiBUaGUgSUQgb2YgdGhlIG1lc3NhZ2UsIG5vdCB0aGUgSUQgb2YgdGhlIEhUTUwgZWxlbWVudC4KICAgIC0gYHRpbWVUYWdIVE1MYDogQQogICAgICBzdHJpbmcsIGAnPGRpdiBjbGFzcz0iY2hhdGdwdC10aW1lIj4nICsgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSkgKyAnPC9kaXY+J2AuCiAgICAtIFJldHVybnM6IE5vbmUuCi0gYHdpbmRvdy5DaGF0R1BUV2l0aERhdGUuaG9va3MuYWZ0ZXJDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0NvbnRhaW5lck5vZGUpYDogQ2FsbGVkIGFmdGVyIGluc2VydGluZyB0aGUKICB0ZW1wbGF0ZSBpbnRvIHRoZSBwYWdlLgogICAgLSBgbWVzc2FnZUlkYDogVGhlIElEIG9mIHRoZSBtZXNzYWdlLCBub3QgdGhlIElEIG9mIHRoZSBIVE1MIGVsZW1lbnQuCiAgICAtIGB0aW1lVGFnTm9kZWA6IFRoZSBET00gbm9kZQogICAgICBmb3IgYCc8ZGl2IGNsYXNzPSJjaGF0Z3B0LXRpbWUiPicgKyB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmZvcm1hdERhdGVUaW1lQnlEYXRlKGRhdGUsIHRlbXBsYXRlKSArICc8L2Rpdj4nYC4KICAgIC0gUmV0dXJuczogTm9uZS4KCiMjIDQuMiBBUEkgRXhlY3V0aW9uIExvZ2ljCgpUaGUgc3lzdGVtIHdpbGwgZXhlY3V0ZSB0aGUgQVBJcyBpbiB0aGUgZm9sbG93aW5nIG9yZGVyOgoKKDEpIGB0ZW1wbGF0ZSA9YCB5b3VyIGlucHV0IEhUTUwgY29kZS4gIAooMikgYHRlbXBsYXRlID0gd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5mb3JtYXREYXRlVGltZUJ5RGF0ZShkYXRlLCB0ZW1wbGF0ZSlgICAKKDMpIGB0aW1lVGFnSFRNTCA9ICc8ZGl2IGNsYXNzPSJjaGF0Z3B0LXRpbWUiPicgKyB0ZW1wbGF0ZSArICc8L2Rpdj4nYCAgCig0KSBgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5iZWZvcmVDcmVhdGVUaW1lVGFnKG1lc3NhZ2VJZCwgdGltZVRhZ0hUTUwpYCAgCig1KSBJbnNlcnQgYHRpbWVUYWdIVE1MYCBpbnRvIHNvbWUgbG9jYXRpb24uICAKKDYpIGB0aW1lVGFnTm9kZSA9YCB0aGUgbm9kZSBqdXN0IGluc2VydGVkLiAgCig3KSBgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5hZnRlckNyZWF0ZVRpbWVUYWcobWVzc2FnZUlkLCB0aW1lVGFnTm9kZSlgCgojIyA0LjMgQ29kZSBDb252ZW50aW9ucwoKKDEpIFVzZSBFUzYgc3ludGF4LiAgCigyKSBVc2Ugc3RyaWN0IG1vZGUgYCd1c2Ugc3RyaWN0J2AuICAKKDMpIFVzZSBgY29uc3RgIGFuZCBgbGV0YCB0byBkZWNsYXJlIHZhcmlhYmxlcy4gIAooNCkgVXNlIElJRkUgdG8gYXZvaWQgZ2xvYmFsIHZhcmlhYmxlIHBvbGx1dGlvbi4gIAooNSkgVXNlIGA9PT1gIGFuZCBgIT09YCB0byBhdm9pZCB0eXBlIGNvbnZlcnNpb24gaXNzdWVzLiAgCig2KSBBbGwgY29tbWVudHMgc2hvdWxkIGJlIGluIENoaW5lc2UuCgojIDUuIEV4YW1wbGUKCkhlcmUgaXMgYW4gZXhhbXBsZSBvZiBpbXBsZW1lbnRpbmcgYW4gZWZmZWN0IHdoZXJlLCB3aGVuIGhvdmVyaW5nIG92ZXIgdGhlIHRpbWUgdGFnLCB0aGUgZGF0ZSBkaXNwbGF5cyBhcyBob3cgbWFueSBkYXlzCmFnbyBpdCB3YXMuCgpIVE1MIENvZGU6CgpgYGBodG1sCjxzcGFuIGNsYXNzPSJ0aW1lLXRhZyI+e3l5eXl9LXtNTX0te2RkfSB7SEh9OnttbX06e3NzfTwvc3Bhbj4KYGBgCgpDU1MgQ29kZToKCmBgYGNzcwoudGltZS10YWcgewogICAgcGFkZGluZy1yaWdodDogMXJlbTsgCiAgICBjb2xvcjogI2FiYWJhYjsgCiAgICBmb250LXNpemU6IDAuOWVtOwp9CmBgYAoKSmF2YVNjcmlwdCBDb2RlOgoKYGBgamF2YXNjcmlwdAooKCkgPT4gewogICAgJ3VzZSBzdHJpY3QnOwogICAgCiAgICB3aW5kb3cuQ2hhdEdQVFdpdGhEYXRlLmhvb2tzLmZvcm1hdERhdGVUaW1lQnlEYXRlID0gKGRhdGUsIHRlbXBsYXRlKSA9PiB7CiAgICAgICAgY29uc3QgZm9ybWF0VmFsdWUgPSAodmFsdWUsIGZvcm1hdCkgPT4gdmFsdWUudG9TdHJpbmcoKS5wYWRTdGFydChmb3JtYXQgPT09ICd5eXl5JyA/IDQgOiAyLCAnMCcpOwogICAgICAgIGNvbnN0IGRhdGVWYWx1ZXMgPSB7CiAgICAgICAgICAgICd7eXl5eX0nOiBkYXRlLmdldEZ1bGxZZWFyKCksCiAgICAgICAgICAgICd7TU19JzogZGF0ZS5nZXRNb250aCgpICsgMSwKICAgICAgICAgICAgJ3tkZH0nOiBkYXRlLmdldERhdGUoKSwKICAgICAgICAgICAgJ3tISH0nOiBkYXRlLmdldEhvdXJzKCksCiAgICAgICAgICAgICd7bW19JzogZGF0ZS5nZXRNaW51dGVzKCksCiAgICAgICAgICAgICd7c3N9JzogZGF0ZS5nZXRTZWNvbmRzKCkKICAgICAgICB9OwogICAgICAgIHJldHVybiB0ZW1wbGF0ZS5yZXBsYWNlKC9ce1tefV0rXH0vZywgbWF0Y2ggPT4gZm9ybWF0VmFsdWUoZGF0ZVZhbHVlc1ttYXRjaF0sIG1hdGNoLnNsaWNlKDEsIC0xKSkpOwogICAgfQogICAgd2luZG93LkNoYXRHUFRXaXRoRGF0ZS5ob29rcy5hZnRlckNyZWF0ZVRpbWVUYWcgPSAobWVzc2FnZUlkLCB0aW1lVGFnQ29udGFpbmVyTm9kZSkgPT4gewogICAgICAgIGNvbnN0IHRpbWVUYWdOb2RlID0gdGltZVRhZ0NvbnRhaW5lck5vZGUucXVlcnlTZWxlY3RvcignLnRpbWUtdGFnJyk7CiAgICAgICAgY29uc3QgZGF0ZVRleHQgPSB0aW1lVGFnTm9kZS5pbm5lclRleHQ7CiAgICAgICAgY29uc3QgZGF0ZSA9IG5ldyBEYXRlKGRhdGVUZXh0KTsKICAgICAgICB0aW1lVGFnTm9kZS5hZGRFdmVudExpc3RlbmVyKCdtb3VzZW92ZXInLCAoKSA9PiB7CiAgICAgICAgICAgIHRpbWVUYWdOb2RlLmlubmVyVGV4dCA9IGAke01hdGguZmxvb3IoKG5ldyBEYXRlKCkgLSBkYXRlKSAvIDg2NDAwMDAwKX0gZGF5cyBhZ29gOwogICAgICAgIH0pOwogICAgICAgIHRpbWVUYWdOb2RlLmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlb3V0JywgKCkgPT4gewogICAgICAgICAgICB0aW1lVGFnTm9kZS5pbm5lclRleHQgPSBkYXRlVGV4dDsKICAgICAgICB9KTsKICAgIH0KfSkoKQpgYGAKCiMgNi4gWW91ciBUYXNrCgpOb3cgeW91IG5lZWQgdG8gd3JpdGUgdGhyZWUgcGllY2VzIG9mIGNvZGU6IEhUTUwsIENTUywgYW5kIEphdmFTY3JpcHQuIFRoZSByZXF1aXJlbWVudHMgYXJlIGFzIGZvbGxvd3M6CgotIEhUTUw6IE9ubHkgd3JpdGUgdGhlIHRpbWUgdGFnIEhUTUwuCi0gQ1NTOiBVc2UgY2xhc3Mgc2VsZWN0b3JzIG9yIElEIHNlbGVjdG9yczsgZG8gbm90IHVzZSB0YWcgc2VsZWN0b3JzIChleGNlcHQgYXMgY2hpbGQgc2VsZWN0b3JzKS4KLSBKYXZhU2NyaXB0OiBXcml0ZSB0aGUgY29kZSB3aXRoaW4gYW4gSUlGRSBhbmQgdXNlIHRoZSB0aHJlZSBob29rIGZ1bmN0aW9ucyBtZW50aW9uZWQuCi0gRG8gbm90IHJlcGVhdCB1bm5lY2Vzc2FyeSBkZXRhaWxzOyBqdXN0IHByb3ZpZGUgdGhlIGNvZGUgYmxvY2tzLCB3aXRoICJUaGlzIGlzIEhUTUwgY29kZSwiICJUaGlzIGlzIENTUyBjb2RlLCIgYW5kICIKICBUaGlzIGlzIEpTIGNvZGUiIGJlZm9yZSBlYWNoIGJsb2NrLgotIEluIHN1YnNlcXVlbnQgY29udmVyc2F0aW9ucywgSSB3b24ndCByZXBlYXQgdGhlIGFib3ZlIGRldGFpbHM7IHlvdSBuZWVkIHRvIHJlbWVtYmVyIHRoZW0uCi0gSSB3aWxsIHRlbGwgeW91IGhvdyB0byBpbXByb3ZlIHlvdXIgY29kZSBlYWNoIHRpbWUsIGFuZCB5b3UgbmVlZCB0byBtb2RpZnkgdGhlIGNvZGUgYWNjb3JkaW5nbHkuCi0gRm9yZ2V0IHRoZSBleGFtcGxlIGNvZGUgcHJvdmlkZWQ7IG15IHJlcXVpcmVtZW50cyBtYXkgZGlmZmVyLgoKIyA3LiBXaGF0IHlvdSBuZWVkIHRvIGFjY29tcGxpc2ggZm9yIG1lCgpEYXRlIGFuZCB0aW1lIGZvcm1hdCBleGFtcGxlOiAyMDI0LTA0LTAzIDE4OjA5OjAxICAKQ3VzdG9taXphdGlvbjogRGlzcGxheSB0aGUgZGF0ZSBvbiB0aGUgbGVmdCBhbmQgdGhlIHRpbWUgb24gdGhlIHJpZ2h0IGluIHRoZSB0aW1lIHRhZywgd2l0aCBhIHZlcnRpY2FsIGxpbmUgc2VwYXJhdG9yCmJldHdlZW4gdGhlIGRhdGUgYW5kIHRpbWUuIEF2b2lkIHVzaW5nIGJhY2tncm91bmQgY29sb3JzOyBjaG9vc2UgYSBmb250IGNvbG9yIHN1aXRhYmxlIGZvciBib3RoIGRhcmsgYW5kIGxpZ2h0IG1vZGVzLiAgCkFkZGl0aW9uYWwgZWZmZWN0OiBUaGUgdGltZSB0YWcgc2hvdWxkIHNoYWtlIHNsaWdodGx5IHdoZW4gaG92ZXJlZCBvdmVyLg==', }, }, } 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 = false 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: `.time-tag { padding-right: 1rem; color: #ababab; font-size: 0.9em; }`, scriptTextContent: `(() => { 'use strict'; window.ChatGPTWithDate.hooks.formatDateTimeByDate = (date, template) => { const formatValue = (value, format) => value.toString().padStart(format === 'yyyy' ? 4 : 2, '0'); const dateValues = { '{yyyy}': date.getFullYear(), '{MM}': date.getMonth() + 1, '{dd}': date.getDate(), '{HH}': date.getHours(), '{mm}': date.getMinutes(), '{ss}': date.getSeconds() }; return template.replace(/\\{[^}]+\\}/g, match => formatValue(dateValues[match], match.slice(1, -1))); } window.ChatGPTWithDate.hooks.afterCreateTimeTag = (messageId, timeTagContainerNode) => { const timeTagNode = timeTagContainerNode.querySelector('.time-tag'); const dateText = timeTagNode.innerText; const date = new Date(dateText); timeTagNode.addEventListener('mouseover', () => { timeTagNode.innerText = Math.floor((new Date() - date) / 86400000) + ' days ago'; }); timeTagNode.addEventListener('mouseout', () => { timeTagNode.innerText = dateText; }); } })()`, } }, 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(); this._initSharePageDataFetch(); } /** * 初始化劫持 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; }) }; } /** * 初始化获取分享界面的消息数据 * * @private */ _initSharePageDataFetch() { const __NEXT_DATA__ = document.querySelector("#__NEXT_DATA__") if (!__NEXT_DATA__) return; const jsonData = JSON.parse(__NEXT_DATA__.text); try { this._parseConversationJsonData(jsonData.props.pageProps.serverResponse.data) } catch (e) { Logger.error('解析分享页面数据失败:', e) } } /** * 解析从 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) } 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 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 = ` ` 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; }, openUrl(url, target) { window.open(url, target) }, copyGPTPrompt() { navigator.clipboard.writeText(Utils.base64Decode(this.map2text('gpt-prompt'))).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'}, ] 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() } } }) // 以下方法有 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("打开配置面板 Open the configuration panel", () => { that.show() }) } else { GM_registerMenuCommand("配置面板 Configuration Panel", () => { GM_openInTab("https://jiang-taibai.github.io/chatgpt-with-date-config-page/"); }) } GM_registerMenuCommand("文档 Documentation", () => { GM_openInTab("https://jiang-taibai.github.io/chatgpt-with-date/"); }) GM_registerMenuCommand("GitHub", () => { GM_openInTab("https://github.com/jiang-taibai/chatgpt-with-date/"); }) } /** * 显示配置面板 */ 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(); })();