ChatGPT with Date

Tampermonkey plugin for displaying ChatGPT historical and real-time conversation time. 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。

当前为 2024-05-04 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT with Date
  3. // @namespace https://github.com/jiang-taibai/chatgpt-with-date
  4. // @version 1.2.1
  5. // @description Tampermonkey plugin for displaying ChatGPT historical and real-time conversation time. 显示 ChatGPT 历史对话时间 与 实时对话时间的 Tampermonkey 插件。
  6. // @author CoderJiang
  7. // @license MIT
  8. // @match https://chat.openai.com/*
  9. // @match https://chatgpt.com/*
  10. // @icon https://cdn.coderjiang.com/project/chatgpt-with-date/logo.svg
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant unsafeWindow
  16. // @run-at document-end
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. class SystemConfig {
  23. static Common = {
  24. ApplicationName: 'ChatGPT with Date',
  25. }
  26. static Logger = {
  27. TimeFormatTemplate: "{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}.{ms}",
  28. }
  29. static TimeRender = {
  30. Interval: 1000,
  31. TimeClassName: 'chatgpt-time',
  32. BatchSize: 100,
  33. BatchTimeout: 200,
  34. RenderRetryCount: 3,
  35. RenderModes: ['AfterRoleLeft', 'AfterRoleRight', 'BelowRole'],
  36. RenderModeStyles: {
  37. AfterRoleLeft: `
  38. .chatgpt-time {
  39. font-weight: normal;
  40. }
  41. `,
  42. AfterRoleRight: `
  43. .chatgpt-time {
  44. font-weight: normal;
  45. float: right;
  46. }
  47. `,
  48. BelowRole: `
  49. .chatgpt-time {
  50. font-weight: normal;
  51. display: block;
  52. }
  53. `,
  54. },
  55. TimeTagTemplates: [
  56. // 默认:2023-10-15 12:01:00
  57. `<span style="margin-left: 4px; color: #ababab; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  58. // 美国:Oct 15, 2023 12:01 PM
  59. `<span style="margin-left: 4px; color: #ababab; font-size: 0.9em;">{MM#shortname@en} {dd}, {yyyy} {HH#12}:{mm} {HH#tag}</span>`,
  60. // 英国:01/01/2024 12:01
  61. `<span style="margin-left: 4px; color: #ababab; font-size: 0.9em;">{dd}/{MM}/{yyyy} {HH}:{mm}</span>`,
  62. // 日本:2023年10月15日 12:01
  63. `<span style="margin-left: 4px; color: #ababab; font-size: 0.9em;">{yyyy}年{MM}月{dd}日 {HH}:{mm}</span>`,
  64. // 显示毫秒数:2023-10-15 12:01:00.000
  65. `<span style="margin-left: 4px; color: #ababab; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}.{ms}</span>`,
  66. // 复杂模板
  67. `<span style="margin-left: 4px; background: #2B2B2b; border-radius: 8px; padding: 1px 10px; color: #717171; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  68. `<span style="margin-left: 4px; background: #d7d7d7; border-radius: 8px; padding: 1px 10px; color: #2b2b2b; font-size: 0.9em;">{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}</span>`,
  69. `<span style="margin-left: 4px; color: #E0E0E0; font-size: 0.9em;"><span style="background: #333; padding: 1px 4px 1px 10px; display: inline-block; border-radius: 8px 0 0 8px;">{yyyy}-{MM}-{dd}</span><span style="background: #606060; padding: 1px 10px 1px 4px; display: inline-block; border-radius: 0 8px 8px 0;">{HH}:{mm}:{ss}</span></span>`,
  70. `<span style="margin-left: 4px; color: #E0E0E0; font-size: 0.9em;"><span style="background: #848484; padding: 1px 4px 1px 10px; display: inline-block; border-radius: 8px 0 0 8px;">{yyyy}-{MM}-{dd}</span><span style="background: #a6a6a6; padding: 1px 10px 1px 4px; display: inline-block; border-radius: 0 8px 8px 0;">{HH}:{mm}:{ss}</span></span>`,
  71. ],
  72. BasicStyleKey: 'time-render',
  73. AdditionalStyleKey: 'time-render-advanced',
  74. AdditionalScriptKey: 'time-render-advanced',
  75. }
  76. static ConfigPanel = {
  77. AppID: 'CWD-Configuration-Panel',
  78. Icon: {
  79. Close: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M649.179 512l212.839-212.84c37.881-37.881 37.881-99.298 0-137.179s-99.298-37.881-137.179 0L512 374.821l-212.839-212.84c-37.881-37.881-99.298-37.881-137.179 0s-37.881 99.298 0 137.179L374.821 512 161.982 724.84c-37.881 37.881-37.881 99.297 0 137.179 18.94 18.94 43.765 28.41 68.589 28.41 24.825 0 49.649-9.47 68.589-28.41L512 649.179l212.839 212.84c18.94 18.94 43.765 28.41 68.589 28.41s49.649-9.47 68.59-28.41c37.881-37.882 37.881-99.298 0-137.179L649.179 512z"></path></svg>',
  80. },
  81. StyleKey: 'config-panel',
  82. }
  83. // GM 存储的键
  84. static GMStorageKey = {
  85. UserConfig: 'ChatGPTWithDate-UserConfig',
  86. ConfigPanel: {
  87. Position: 'ChatGPTWithDate-ConfigPanel-Position', Size: 'ChatGPTWithDate-ConfigPanel-Size',
  88. },
  89. }
  90. }
  91.  
  92. class Utils {
  93.  
  94. /**
  95. * 按照模板格式化日期时间
  96. *
  97. * @param date Date 对象
  98. * @param template 模板,例如 '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}'
  99. * @returns string 格式化后的日期时间字符串
  100. */
  101. static formatDateTimeByDate(date, template) {
  102. const year = date.getFullYear();
  103. const month = date.getMonth() + 1;
  104. const day = date.getDate();
  105. const week = date.getDay();
  106. const hours = date.getHours();
  107. const minutes = date.getMinutes();
  108. const seconds = date.getSeconds();
  109. const milliseconds = date.getMilliseconds();
  110. const week2zh = ['', '一', '二', '三', '四', '五', '六', '日']
  111. const week2enFullName = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  112. const week2enShortName = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  113. const month2zh = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
  114. const month2enFullName = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
  115. const month2enShortName = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  116.  
  117. const getValueByKey = (key) => {
  118. switch (key) {
  119. case '{yyyy}':
  120. return year.toString();
  121. case '{yy}':
  122. return (year % 100).toString().padStart(2, '0');
  123.  
  124. case '{MM}':
  125. case '{MM:02}':
  126. return month.toString().padStart(2, '0');
  127. case '{MM:01}':
  128. return month.toString();
  129. case '{MM#name@zh}':
  130. return month2zh[month];
  131. case '{MM#name@en}':
  132. case '{MM#fullname@en}':
  133. return month2enFullName[month];
  134. case '{MM#shortname@en}':
  135. return month2enShortName[month];
  136.  
  137. case '{dd}':
  138. case '{dd:02}':
  139. return day.toString().padStart(2, '0');
  140. case '{dd:01}':
  141. return day.toString();
  142.  
  143. case '{HH}':
  144. case '{HH:02}':
  145. case '{HH#24}':
  146. case '{HH#24:02}':
  147. return hours.toString().padStart(2, '0');
  148. case '{HH:01}':
  149. case '{HH#24:01}':
  150. return hours.toString();
  151. case '{HH#12}':
  152. case '{HH#12:02}':
  153. return (hours % 12 || 12).toString().padStart(2, '0');
  154. case '{HH#12:01}':
  155. return (hours % 12 || 12).toString();
  156. case '{HH#tag}':
  157. case '{HH#tag@en}':
  158. return hours >= 12 ? 'PM' : 'AM';
  159. case '{HH#tag@zh}':
  160. return hours >= 12 ? '下午' : '上午';
  161.  
  162. case '{mm}':
  163. case '{mm:02}':
  164. return minutes.toString().padStart(2, '0');
  165. case '{mm:01}':
  166. return minutes.toString();
  167.  
  168. case '{ss}':
  169. case '{ss:02}':
  170. return seconds.toString().padStart(2, '0');
  171. case '{ss:01}':
  172. return seconds.toString();
  173.  
  174. case '{ms}':
  175. return milliseconds.toString().padStart(3, '0');
  176.  
  177.  
  178. case '{week}':
  179. case '{week:02}':
  180. return week.toString().padStart(2, '0');
  181. case '{week:01}':
  182. return week.toString();
  183. case '{week#name@zh}':
  184. return week2zh[week];
  185. case '{week#name@en}':
  186. case '{week#fullname@en}':
  187. return week2enFullName[week];
  188. case '{week#shortname@en}':
  189. return week2enShortName[week];
  190. default:
  191. return key;
  192. }
  193. }
  194. return template.replace(/\{[^}]+\}/g, match => getValueByKey(match));
  195. }
  196.  
  197. /**
  198. * 深度合并两个对象,将源对象的属性合并到目标对象中,如果属性值为对象则递归合并。
  199. *
  200. * @param target 目标对象
  201. * @param source 源对象
  202. * @returns {*}
  203. */
  204. static deepMerge(target, source) {
  205. if (!source) return target
  206. // 遍历源对象的所有属性
  207. Object.keys(source).forEach(key => {
  208. if (source[key] && typeof source[key] === 'object') {
  209. // 如果源属性是一个对象且目标中也存在同名属性且为对象,则递归合并
  210. if (target[key] && typeof target[key] === 'object') {
  211. Utils.deepMerge(target[key], source[key]);
  212. } else {
  213. // 否则直接复制(对于源中的对象,需要进行深拷贝)
  214. target[key] = JSON.parse(JSON.stringify(source[key]));
  215. }
  216. } else {
  217. // 非对象属性直接复制
  218. target[key] = source[key];
  219. }
  220. });
  221. return target;
  222. }
  223.  
  224. /**
  225. * 检查依赖关系图(有向图)是否有循环依赖,如果没有就返回一个先后顺序(即按照此顺序实例化不会出现依赖项为空的情况)。
  226. * 给定依赖关系图为此结构[{node:'ComponentClass0', dependencies:['ComponentClass2', 'ComponentClass3']}, ...]
  227. * @param dependencyGraph 依赖关系图
  228. * @returns {*[]}
  229. */
  230. static dependencyAnalysis(dependencyGraph) {
  231. // 创建一个映射每个节点到其入度的对象
  232. const inDegree = {};
  233. const graph = {};
  234. const order = [];
  235.  
  236. // 初始化图和入度表
  237. dependencyGraph.forEach(item => {
  238. const {node, dependencies} = item;
  239. if (!graph[node]) {
  240. graph[node] = [];
  241. inDegree[node] = 0;
  242. }
  243. dependencies.forEach(dependentNode => {
  244. if (!graph[dependentNode]) {
  245. graph[dependentNode] = [];
  246. inDegree[dependentNode] = 0;
  247. }
  248. graph[dependentNode].push(node);
  249. inDegree[node]++;
  250. });
  251. });
  252.  
  253. // 将所有入度为0的节点加入到队列中
  254. const queue = [];
  255. for (const node in inDegree) {
  256. if (inDegree[node] === 0) {
  257. queue.push(node);
  258. }
  259. }
  260.  
  261. // 处理队列中的节点
  262. while (queue.length) {
  263. const current = queue.shift();
  264. order.push(current);
  265. graph[current].forEach(neighbour => {
  266. inDegree[neighbour]--;
  267. if (inDegree[neighbour] === 0) {
  268. queue.push(neighbour);
  269. }
  270. });
  271. }
  272.  
  273. // 如果排序后的节点数量不等于图中的节点数量,说明存在循环依赖
  274. if (order.length !== Object.keys(graph).length) {
  275. // 找到循环依赖的节点
  276. const cycleNodes = [];
  277. for (const node in inDegree) {
  278. if (inDegree[node] !== 0) {
  279. cycleNodes.push(node);
  280. }
  281. }
  282. throw new Error("存在循环依赖的节点:" + cycleNodes.join(","));
  283. }
  284. return order;
  285. }
  286.  
  287. }
  288.  
  289. class Logger {
  290. static EnableLog = true
  291. static EnableDebug = false
  292. static EnableInfo = true
  293. static EnableWarn = true
  294. static EnableError = true
  295. static EnableTable = true
  296.  
  297. static prefix(type = 'INFO') {
  298. const timeFormat = Utils.formatDateTimeByDate(new Date(), SystemConfig.Logger.TimeFormatTemplate);
  299. return `[${timeFormat}] - [${SystemConfig.Common.ApplicationName}] - [${type}]`
  300. }
  301.  
  302. static log(...args) {
  303. if (Logger.EnableLog) {
  304. console.log(Logger.prefix('INFO'), ...args);
  305. }
  306. }
  307.  
  308. static debug(...args) {
  309. if (Logger.EnableDebug) {
  310. console.debug(Logger.prefix('DEBUG'), ...args);
  311. }
  312. }
  313.  
  314. static info(...args) {
  315. if (Logger.EnableInfo) {
  316. console.info(Logger.prefix('INFO'), ...args);
  317. }
  318. }
  319.  
  320. static warn(...args) {
  321. if (Logger.EnableWarn) {
  322. console.warn(Logger.prefix('WARN'), ...args);
  323. }
  324. }
  325.  
  326. static error(...args) {
  327. if (Logger.EnableError) {
  328. console.error(Logger.prefix('ERROR'), ...args);
  329. }
  330. }
  331.  
  332. static table(...args) {
  333. if (Logger.EnableTable) {
  334. console.table(...args);
  335. }
  336. }
  337. }
  338.  
  339. class MessageBO {
  340. /**
  341. * 消息业务对象
  342. *
  343. * @param messageId 消息ID,为消息元素的data-message-id属性值
  344. * @param role 角色
  345. * system: 表示系统消息,并不属于聊天内容。 从 API 获取
  346. * tool: 也表示系统消息。 从 API 获取
  347. * assistant: 表示 ChatGPT 回答的消息。 从 API 获取
  348. * user: 表示用户输入的消息。 从 API 获取
  349. * You: 表示用户输入的消息。 从页面实时获取
  350. * ChatGPT: 表示 ChatGPT 回答的消息。 从页面实时获取
  351. * @param timestamp 时间戳,浮点数或整数类型,单位毫秒,例如 1714398759.26881、1714398759
  352. * @param message 消息内容
  353. */
  354. constructor(messageId, role, timestamp, message = '') {
  355. this.messageId = messageId;
  356. this.role = role;
  357. this.timestamp = timestamp;
  358. this.message = message;
  359. }
  360. }
  361.  
  362. class MessageElementBO {
  363. /**
  364. * 消息元素业务对象
  365. *
  366. * @param rootEle 消息的根元素,并非是总根元素,而是 roleEle 和 messageEle 的最近公共祖先元素
  367. * @param roleEle 角色元素,例如 <div>ChatGPT</div>
  368. * @param messageEle 消息元素,包含 data-message-id 属性
  369. * 例如 <div data-message-id="123456">你好</div>
  370. */
  371. constructor(rootEle, roleEle, messageEle) {
  372. this.rootEle = rootEle;
  373. this.roleEle = roleEle;
  374. this.messageEle = messageEle;
  375. }
  376. }
  377.  
  378. class Component {
  379.  
  380. constructor() {
  381. this.dependencies = []
  382. Object.defineProperty(this, 'initDependencies', {
  383. value: function () {
  384. this.dependencies.forEach(dependency => {
  385. this[dependency.field] = ComponentLocator.get(dependency.clazz)
  386. })
  387. }, writable: false, // 防止方法被修改
  388. configurable: false // 防止属性被重新定义或删除
  389. });
  390. }
  391.  
  392. init() {
  393. }
  394. }
  395.  
  396. class ComponentLocator {
  397. /**
  398. * 组件注册器,用于注册和获取组件
  399. */
  400. static components = {};
  401.  
  402. /**
  403. * 注册组件,要求组件为 Component 的子类
  404. *
  405. * @param clazz Component 的子类
  406. * @param instance Component 的子类的实例化对象,必顧是 clazz 的实例
  407. * @returns obj 返回注册的实例化对象
  408. */
  409. static register(clazz, instance) {
  410. if (!(instance instanceof Component)) {
  411. throw new Error(`实例化对象 ${instance} 不是 Component 的实例。`);
  412. }
  413. if (!(instance instanceof clazz)) {
  414. throw new Error(`实例化对象 ${instance} 不是 ${clazz} 的实例。`);
  415. }
  416. if (ComponentLocator.components[clazz.name]) {
  417. throw new Error(`组件 ${clazz.name} 已经注册过了。`);
  418. }
  419. ComponentLocator.components[clazz.name] = instance;
  420. return instance
  421. }
  422.  
  423. /**
  424. * 获取组件,用于完成组件之间的依赖注入
  425. *
  426. * @param clazz Component 的子类
  427. * @returns {*} 返回注册的实例化对象
  428. */
  429. static get(clazz) {
  430. if (!ComponentLocator.components[clazz.name]) {
  431. throw new Error(`组件 ${clazz.name} 未注册。`);
  432. }
  433. return ComponentLocator.components[clazz.name];
  434. }
  435. }
  436.  
  437. class UserConfig extends Component {
  438.  
  439. init() {
  440. this.timeRender = {
  441. mode: 'AfterRoleLeft',
  442. format: SystemConfig.TimeRender.TimeTagTemplates[0],
  443. advanced: {
  444. enable: false,
  445. htmlTextContent: `<div class="text-tag-box">
  446. <span class="date">{yyyy}-{MM}-{dd}</span>
  447. <span class="time">{HH}:{mm}:{ss}</span>
  448. </div>`,
  449. styleTextContent: `.text-tag-box {
  450. border-radius: 8px;
  451. color: #E0E0E0;
  452. font-size: 0.9em;
  453. overflow: hidden;
  454. display: inline-block;
  455. }
  456.  
  457. .text-tag-box .date {
  458. background: #333;
  459. float: left;
  460. padding: 2px 8px 2px 10px;
  461. display: inline-block;
  462. transition: width 0.5s ease-out;
  463. white-space: nowrap;
  464. }
  465.  
  466. .text-tag-box .time {
  467. background: #606060;
  468. float: left;
  469. padding: 2px 10px 2px 8px;
  470. display: inline-block;
  471. }`,
  472. scriptTextContent: `(() => {
  473. const getNewWidth = (targetNode, text) => {
  474. // 创建一个临时元素来测量文本宽度
  475. const temp = targetNode.cloneNode();
  476. temp.style.width = 'auto'; // 自动宽度
  477. temp.style.visibility = 'hidden'; // 隐藏元素,不影响布局
  478. temp.style.position = 'absolute'; // 避免影响其他元素
  479. temp.style.whiteSpace = 'nowrap'; // 无换行
  480. temp.innerText = text;
  481. document.body.appendChild(temp);
  482. const newWidth = temp.offsetWidth;
  483. document.body.removeChild(temp);
  484. return newWidth;
  485. }
  486.  
  487. window.afterCreateTimeTag = (messageId, timeTagNode) => {
  488. const dateNode = timeTagNode.querySelector('.date');
  489. const date = dateNode.innerText;
  490. const originalWidth = getNewWidth(dateNode, date);
  491. const paddingWidth = 18;
  492. dateNode.style.width = (originalWidth + paddingWidth) + 'px';
  493.  
  494. timeTagNode.addEventListener('mouseover', () => {
  495. const now = new Date();
  496. const offset = now - new Date(date);
  497. const days = Math.floor(offset / (24 * 60 * 60 * 1000));
  498. let text = '';
  499. if (days < 1)
  500. text = '今天';
  501. else if (days < 2)
  502. text = '昨天';
  503. else if (days < 3)
  504. text = '前天';
  505. else if (days < 7)
  506. text = days + '天前';
  507. else if (days < 30)
  508. text = Math.floor(days / 7) + '周前';
  509. else if (days < 365)
  510. text = Math.floor(days / 30) + '个月前';
  511. else
  512. text = Math.floor(days / 365) + '年前';
  513. dateNode.innerText = text;
  514. dateNode.style.width = (getNewWidth(dateNode, text) + paddingWidth) + 'px';
  515. });
  516.  
  517. // 鼠标移出 timeTagNode 时恢复 dateNode 的内容为原来的日期
  518. timeTagNode.addEventListener('mouseout', () => {
  519. dateNode.innerText = date;
  520. dateNode.style.width = (originalWidth + paddingWidth) + 'px';
  521. });
  522. }
  523. })()`,
  524. }
  525. }
  526. const userConfig = this.load()
  527. if (userConfig) {
  528. Utils.deepMerge(this.timeRender, userConfig.timeRender)
  529. }
  530. }
  531.  
  532. save() {
  533. GM_setValue(SystemConfig.GMStorageKey.UserConfig, {
  534. timeRender: this.timeRender
  535. })
  536. }
  537.  
  538. load() {
  539. return GM_getValue(SystemConfig.GMStorageKey.UserConfig)
  540. }
  541.  
  542. /**
  543. * 更新配置并保存
  544. * @param newConfig 新的配置
  545. */
  546. update(newConfig) {
  547. Utils.deepMerge(this.timeRender, newConfig.timeRender)
  548. this.save()
  549. }
  550. }
  551.  
  552. class StyleService extends Component {
  553. init() {
  554. this.styles = new Map()
  555. }
  556.  
  557. /**
  558. * 更新样式
  559. *
  560. * @param key 样式的 key,字符串对象
  561. * @param styleContent 样式,字符串对象
  562. */
  563. updateStyle(key, styleContent) {
  564. this.removeStyle(key)
  565. const styleNode = document.createElement('style')
  566. styleNode.textContent = styleContent
  567. document.head.appendChild(styleNode)
  568. this.styles.set(key, styleNode)
  569. }
  570.  
  571. /**
  572. * 移除样式
  573. *
  574. * @param key 样式的 key,字符串对象
  575. */
  576. removeStyle(key) {
  577. let styleNode = this.styles.get(key)
  578. if (styleNode) {
  579. document.head.removeChild(styleNode)
  580. this.styles.delete(key)
  581. }
  582. }
  583. }
  584.  
  585. class JavaScriptService extends Component {
  586. init() {
  587. this.javaScriptNodes = new Map()
  588. }
  589.  
  590. updateJavaScript(key, textContent) {
  591. this.removeJavaScript(key)
  592. const scriptNode = document.createElement('script')
  593. scriptNode.type = 'text/javascript'
  594. scriptNode.textContent = textContent
  595. document.body.appendChild(scriptNode)
  596. this.javaScriptNodes.set(key, scriptNode)
  597. }
  598.  
  599. removeJavaScript(key) {
  600. let scriptNode = this.javaScriptNodes.get(key)
  601. if (scriptNode) {
  602. document.body.removeChild(scriptNode)
  603. this.javaScriptNodes.delete(key)
  604. }
  605. }
  606. }
  607.  
  608. class MessageService extends Component {
  609. init() {
  610. this.messages = new Map();
  611. }
  612.  
  613. /**
  614. * 解析消息元素,获取消息的所有内容。由于网页中不存在时间戳,所以时间戳使用当前时间代替。
  615. * 调用该方法只需要消息元素,一般用于从页面实时监测获取到的消息。
  616. *
  617. * @param messageDiv 消息元素,包含 data-message-id 属性 的 div 元素
  618. * @returns {MessageBO|undefined} 返回消息业务对象,如果消息元素不存在则返回 undefined
  619. */
  620. parseMessageDiv(messageDiv) {
  621. if (!messageDiv) {
  622. return;
  623. }
  624. const messageId = messageDiv.getAttribute('data-message-id');
  625. const messageElementBO = this.getMessageElement(messageId)
  626. if (!messageElementBO) {
  627. return;
  628. }
  629. let timestamp = new Date().getTime();
  630. const role = messageElementBO.roleEle.innerText;
  631. const message = messageElementBO.messageEle.innerHTML;
  632. if (!this.messages.has(messageId)) {
  633. const messageBO = new MessageBO(messageId, role, timestamp, message);
  634. this.messages.set(messageId, messageBO);
  635. }
  636. return this.messages.get(messageId);
  637. }
  638.  
  639. /**
  640. * 添加消息,主要用于添加从 API 劫持到的消息列表。
  641. * 调用该方法需要已知消息的所有内容,如果只知道消息元素则应该使用 parseMessageDiv 方法获取消息业务对象。
  642. *
  643. * @param message 消息业务对象
  644. * @param force 是否强制添加,如果为 true 则强制添加,否则如果消息已经存在则不添加
  645. * @returns {boolean} 返回是否添加成功
  646. */
  647. addMessage(message, force = false) {
  648. if (this.messages.has(message.messageId) && !force) {
  649. return false;
  650. }
  651. this.messages.set(message.messageId, message);
  652. return true
  653. }
  654.  
  655. /**
  656. * 通过消息 ID 获取消息元素业务对象
  657. *
  658. * @param messageId 消息 ID
  659. * @returns {MessageElementBO|undefined} 返回消息元素业务对象
  660. */
  661. getMessageElement(messageId) {
  662. const messageDiv = document.body.querySelector(`div[data-message-id="${messageId}"]`);
  663. if (!messageDiv) {
  664. return;
  665. }
  666. const rootDiv = messageDiv.parentElement.parentElement.parentElement;
  667. const roleDiv = rootDiv.firstChild;
  668. return new MessageElementBO(rootDiv, roleDiv, messageDiv);
  669. }
  670.  
  671. /**
  672. * 通过消息 ID 获取消息业务对象
  673. * @param messageId
  674. * @returns {any}
  675. */
  676. getMessage(messageId) {
  677. return this.messages.get(messageId);
  678. }
  679.  
  680. /**
  681. * 显示所有消息信息
  682. */
  683. showMessages() {
  684. Logger.table(Array.from(this.messages.values()));
  685. }
  686. }
  687.  
  688. class MonitorService extends Component {
  689. constructor() {
  690. super();
  691. this.messageService = null
  692. this.timeRendererService = null
  693. this.dependencies = [{field: 'messageService', clazz: MessageService}, {
  694. field: 'timeRendererService', clazz: TimeRendererService
  695. },]
  696. }
  697.  
  698. init() {
  699. this.totalTime = 0;
  700. this.originalFetch = window.fetch;
  701. this._initMonitorFetch();
  702. this._initMonitorAddedMessageNode();
  703. }
  704.  
  705. /**
  706. * 初始化劫持 fetch 方法,用于监控 ChatGPT 的消息数据
  707. *
  708. * @private
  709. */
  710. _initMonitorFetch() {
  711. const that = this;
  712. 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}$");
  713. unsafeWindow.fetch = (...args) => {
  714. return that.originalFetch.apply(this, args)
  715. .then(response => {
  716. if (urlRegex.test(response.url)) {
  717. // 克隆响应对象以便独立处理响应体
  718. const clonedResponse = response.clone();
  719. clonedResponse.json().then(data => {
  720. that._parseConversationJsonData(data);
  721. }).catch(error => Logger.error('解析响应体失败:', error));
  722. }
  723. return response;
  724. });
  725. };
  726. }
  727.  
  728. /**
  729. * 解析从 API 获取到的消息数据,该方法存在报错风险,需要在调用时捕获异常以防止中断后续操作。
  730. *
  731. * @param obj 从 API 获取到的消息数据
  732. * @private
  733. */
  734. _parseConversationJsonData(obj) {
  735. const mapping = obj.mapping
  736. const messageIds = []
  737. for (let key in mapping) {
  738. const message = mapping[key].message
  739. if (message) {
  740. const messageId = message.id
  741. const role = message.author.role
  742. const createTime = message.create_time
  743. const messageBO = new MessageBO(messageId, role, createTime * 1000)
  744. messageIds.push(messageId)
  745. this.messageService.addMessage(messageBO, true)
  746. }
  747. }
  748. this.timeRendererService.addMessageArrayToBeRendered(messageIds)
  749. this.messageService.showMessages()
  750. }
  751.  
  752. /**
  753. * 初始化监控节点变化,用于监控在使用 ChatGPT 期间实时输入的消息。
  754. * 每隔 500ms 检查一次 main 节点是否存在,如果存在则开始监控节点变化。
  755. * @private
  756. */
  757. _initMonitorAddedMessageNode() {
  758. const interval = setInterval(() => {
  759. const mainElement = document.querySelector('main');
  760. if (mainElement) {
  761. this._setupMonitorAddedMessageNode(mainElement);
  762. clearInterval(interval); // 清除定时器,停止进一步检查
  763. }
  764. }, 500);
  765. }
  766.  
  767. /**
  768. * 设置监控节点变化,用于监控在使用 ChatGPT 期间实时输入的消息。
  769. * @param supervisedNode 监控在此节点下的节点变化,确保新消息的节点在此节点下
  770. * @private
  771. */
  772. _setupMonitorAddedMessageNode(supervisedNode) {
  773. const that = this;
  774. const callback = function (mutationsList, observer) {
  775. const start = new Date().getTime();
  776. for (const mutation of mutationsList) {
  777. if (mutation.type === 'childList') {
  778. mutation.addedNodes.forEach(node => {
  779. if (node.nodeType === Node.ELEMENT_NODE) {
  780. let messageDiv = node.querySelector('div[data-message-id]');
  781. if (!messageDiv && node.hasAttribute('data-message-id')) {
  782. messageDiv = node
  783. }
  784. if (messageDiv !== null) {
  785. const messageBO = that.messageService.parseMessageDiv(messageDiv);
  786. that.timeRendererService.addMessageToBeRendered(messageBO.messageId);
  787. that.messageService.showMessages()
  788. }
  789. }
  790. });
  791. }
  792. }
  793. const end = new Date().getTime();
  794. that.totalTime += (end - start);
  795. Logger.debug(`监控到节点变化,耗时 ${end - start}ms,总耗时 ${that.totalTime}ms。`);
  796. };
  797. const observer = new MutationObserver(callback);
  798. observer.observe(supervisedNode, {childList: true, subtree: true,});
  799. }
  800. }
  801.  
  802. class TimeRendererService extends Component {
  803.  
  804. constructor() {
  805. super();
  806. this.messageService = null
  807. this.userConfig = null
  808. this.styleService = null
  809. this.javaScriptService = null
  810. this.dependencies = [
  811. {field: 'messageService', clazz: MessageService},
  812. {field: 'userConfig', clazz: UserConfig},
  813. {field: 'styleService', clazz: StyleService},
  814. {field: 'javaScriptService', clazz: JavaScriptService},
  815. ]
  816. }
  817.  
  818. init() {
  819. unsafeWindow.beforeCreateTimeTag = (messageId, timeTagHTML) => {
  820. }
  821. unsafeWindow.afterCreateTimeTag = (messageId, timeTagNode) => {
  822. }
  823. this.messageToBeRendered = []
  824. this.messageCountOfFailedToRender = new Map()
  825. this._setStyleAndJavaScript()
  826. this._initRender()
  827. }
  828.  
  829. /**
  830. * 若为高级模式则设置用户自定义的样式和脚本,否则设置默认样式
  831. *
  832. * @private
  833. */
  834. _setStyleAndJavaScript() {
  835. this.styleService.updateStyle(SystemConfig.TimeRender.BasicStyleKey, SystemConfig.TimeRender.RenderModeStyles[this.userConfig.timeRender.mode])
  836. if (this.userConfig.timeRender.advanced.enable) {
  837. this.styleService.updateStyle(SystemConfig.TimeRender.AdditionalStyleKey, this.userConfig.timeRender.advanced.styleTextContent)
  838. this.javaScriptService.updateJavaScript(SystemConfig.TimeRender.AdditionalScriptKey, this.userConfig.timeRender.advanced.scriptTextContent)
  839. } else {
  840. this.styleService.removeStyle(SystemConfig.TimeRender.AdditionalStyleKey)
  841. unsafeWindow.beforeCreateTimeTag = (messageId, timeTagHTML) => {
  842. }
  843. unsafeWindow.afterCreateTimeTag = (messageId, timeTagNode) => {
  844. }
  845. this.javaScriptService.removeJavaScript(SystemConfig.TimeRender.AdditionalScriptKey)
  846. }
  847. }
  848.  
  849. /**
  850. * 添加消息 ID 到待渲染队列
  851. * @param messageId 消息 ID
  852. */
  853. addMessageToBeRendered(messageId) {
  854. if (typeof messageId !== 'string') {
  855. return
  856. }
  857. this.messageToBeRendered.push(messageId)
  858. Logger.debug(`添加ID ${messageId} 到待渲染队列,当前队列 ${this.messageToBeRendered}`)
  859. }
  860.  
  861. /**
  862. * 添加消息 ID 到待渲染队列
  863. * @param messageIdArray 消息 ID数组
  864. */
  865. addMessageArrayToBeRendered(messageIdArray) {
  866. if (!messageIdArray || !(messageIdArray instanceof Array)) {
  867. return
  868. }
  869. this.messageToBeRendered.push(...messageIdArray)
  870. Logger.debug(`添加ID ${messageIdArray} 到待渲染队列,当前队列 ${this.messageToBeRendered}`)
  871. }
  872.  
  873. /**
  874. * 初始化渲染时间的定时器,每隔 SystemConfig.TimeRender.Interval 毫秒处理一次待渲染队列
  875. * 1. 备份待渲染队列
  876. * 2. 清空待渲染队列
  877. * 3. 遍历备份的队列,逐个渲染
  878. * 3.1 如果渲染失败则重新加入待渲染队列,失败次数加一
  879. * 3.2 如果渲染成功,清空失败次数
  880. * 4. 重复 1-3 步骤
  881. * 5. 如果失败次数超过 SystemConfig.TimeRender.RenderRetryCount 则不再尝试渲染,即不再加入待渲染队列。同时清空失败次数。
  882. *
  883. * @private
  884. */
  885. _initRender() {
  886. const that = this
  887.  
  888. async function processTimeRender() {
  889. const start = new Date().getTime();
  890. let completeCount = 0;
  891. let totalCount = that.messageToBeRendered.length;
  892. const messageToBeRenderedClone = that.messageToBeRendered.slice()
  893. that.messageToBeRendered = []
  894. let count = 0;
  895. for (let messageId of messageToBeRenderedClone) {
  896. count++;
  897. if (count <= SystemConfig.TimeRender.BatchSize && new Date().getTime() - start <= SystemConfig.TimeRender.BatchTimeout) {
  898. const result = await that._renderTime(messageId)
  899. if (!result) {
  900. let countOfFailed = that.messageCountOfFailedToRender.get(messageId)
  901. if (countOfFailed && countOfFailed >= SystemConfig.TimeRender.RenderRetryCount) {
  902. Logger.debug(`ID ${messageId} 渲染失败次数超过 ${SystemConfig.TimeRender.RenderRetryCount} 次,将不再尝试。`)
  903. that.messageCountOfFailedToRender.delete(messageId)
  904. } else {
  905. that.messageToBeRendered.push(messageId);
  906. if (countOfFailed) {
  907. that.messageCountOfFailedToRender.set(messageId, countOfFailed + 1)
  908. } else {
  909. that.messageCountOfFailedToRender.set(messageId, 1)
  910. }
  911. }
  912. } else {
  913. completeCount++
  914. that.messageCountOfFailedToRender.delete(messageId)
  915. }
  916. Logger.debug(`ID ${messageId} 渲染${result ? '成功' : '失败'},当前渲染进度 ${completeCount}/${totalCount},该批次耗时 ${new Date().getTime() - start}ms`)
  917. } else {
  918. for (let i = count; i < messageToBeRenderedClone.length; i++) {
  919. that.messageToBeRendered.push(messageToBeRenderedClone[i])
  920. }
  921. if (count > SystemConfig.TimeRender.BatchSize) {
  922. Logger.debug(`本批次渲染数量超过 ${SystemConfig.TimeRender.BatchSize},将继续下一批次渲染。`)
  923. break;
  924. }
  925. if (new Date().getTime() - start > SystemConfig.TimeRender.BatchTimeout) {
  926. Logger.debug(`本批次渲染超时,将继续下一批次渲染。`)
  927. break;
  928. }
  929. }
  930. }
  931. const end = new Date().getTime();
  932. Logger.debug(`处理当前ID队列渲染 ${messageToBeRenderedClone} 耗时 ${end - start}ms`)
  933. setTimeout(processTimeRender, SystemConfig.TimeRender.Interval);
  934. }
  935.  
  936. processTimeRender().then(r => Logger.debug('初始化渲染时间定时器完成'))
  937. }
  938.  
  939. /**
  940. * 将时间渲染到目标位置,如果检测到目标位置已经存在时间元素则更新时间,否则创建时间元素并插入到目标位置。
  941. *
  942. * @param messageId 消息 ID
  943. * @returns {Promise} 返回是否渲染成功的 Promise 对象
  944. * @private
  945. */
  946. _renderTime(messageId) {
  947. return new Promise(resolve => {
  948. const messageElementBo = this.messageService.getMessageElement(messageId);
  949. const messageBo = this.messageService.getMessage(messageId);
  950. if (!messageElementBo || !messageBo) resolve(false)
  951. const timeElement = messageElementBo.rootEle.querySelector(`.${SystemConfig.TimeRender.TimeClassName}`);
  952. const element = this._createTimeElement(messageBo.timestamp);
  953. if (!timeElement) {
  954. unsafeWindow.beforeCreateTimeTag(messageId, element.timeTagContainer)
  955. switch (this.userConfig.timeRender.mode) {
  956. case 'AfterRoleLeft':
  957. case 'AfterRoleRight':
  958. case 'BelowRole':
  959. messageElementBo.roleEle.innerHTML += element.timeTagContainer
  960. break;
  961. }
  962. unsafeWindow.afterCreateTimeTag(messageId, messageElementBo.rootEle.querySelector(`.${SystemConfig.TimeRender.TimeClassName}`))
  963. } else {
  964. timeElement.innerHTML = element.timeTagFormated
  965. }
  966. resolve(true)
  967. })
  968. }
  969.  
  970. /**
  971. * 创建时间元素,如果开启高级模式则使用用户自定义的时间格式,否则使用默认时间格式。
  972. *
  973. * @param timestamp 时间戳,浮点数或整数类型,单位毫秒,例如 1714398759.26881、1714398759
  974. * @returns {{timeTagFormated, timeTagContainer: string}} 返回格式化后的时间标签 和 包含时间标签的容器的 HTML 字符串
  975. * @private
  976. */
  977. _createTimeElement(timestamp) {
  978. let timeTagFormated = ''
  979. if (this.userConfig.timeRender.advanced.enable) {
  980. timeTagFormated = Utils.formatDateTimeByDate(new Date(timestamp), this.userConfig.timeRender.advanced.htmlTextContent)
  981. } else {
  982. timeTagFormated = Utils.formatDateTimeByDate(new Date(timestamp), this.userConfig.timeRender.format);
  983. }
  984. const timeTagContainer = `<span class="${SystemConfig.TimeRender.TimeClassName}">${timeTagFormated}</span>`;
  985. return {
  986. timeTagFormated, timeTagContainer,
  987. };
  988. }
  989.  
  990. /**
  991. * 清除所有时间元素
  992. * @private
  993. */
  994. _cleanAllTimeElements() {
  995. const timeElements = document.body.querySelectorAll(`.${SystemConfig.TimeRender.TimeClassName}`);
  996. timeElements.forEach(ele => {
  997. ele.remove()
  998. })
  999. }
  1000.  
  1001. /**
  1002. * 重新渲染时间元素,强制拉取所有消息 ID 重新渲染
  1003. */
  1004. reRender() {
  1005. this._setStyleAndJavaScript()
  1006. this._cleanAllTimeElements()
  1007. this.addMessageArrayToBeRendered(Array.from(this.messageService.messages.keys()))
  1008. }
  1009. }
  1010.  
  1011. class ConfigPanelService extends Component {
  1012.  
  1013. constructor() {
  1014. super();
  1015. this.userConfig = null
  1016. this.styleService = null
  1017. this.timeRendererService = null
  1018. this.messageService = null
  1019. this.javascriptService = null
  1020. this.dependencies = [
  1021. {field: 'userConfig', clazz: UserConfig},
  1022. {field: 'styleService', clazz: StyleService},
  1023. {field: 'timeRendererService', clazz: TimeRendererService},
  1024. {field: 'messageService', clazz: MessageService},
  1025. {field: 'javascriptService', clazz: JavaScriptService},
  1026. ]
  1027. }
  1028.  
  1029. /**
  1030. * 初始化配置面板,强调每个子初始化方法阻塞式的执行,即一个初始化方法执行完毕后再执行下一个初始化方法。
  1031. * @returns {Promise<void>}
  1032. */
  1033. async init() {
  1034. this.appID = SystemConfig.ConfigPanel.AppID
  1035. this._initVariables()
  1036. Logger.debug('开始初始化配置面板')
  1037. await this._initStyle()
  1038. Logger.debug('初始化样式完成')
  1039. await this._initExternalResources()
  1040. Logger.debug('初始化脚本完成')
  1041. await this._initPanel()
  1042. Logger.debug('初始化面板完成')
  1043. this._initVue()
  1044. Logger.debug('初始化Vue完成')
  1045. this._initConfigPanelSizeAndPosition()
  1046. this._initConfigPanelEventMonitor()
  1047. Logger.debug('初始化配置面板事件监控完成')
  1048. this._initMenuCommand()
  1049. Logger.debug('初始化菜单命令完成')
  1050. }
  1051.  
  1052. /**
  1053. * 初始化配置面板的 HTML 与 Vue 实例的配置属性。集中管理以便方便修改。
  1054. * @private
  1055. */
  1056. _initVariables() {
  1057. const that = this
  1058. const TimeTagComponent = {
  1059. props: ['html'],
  1060. render() {
  1061. return Vue.h('div', {innerHTML: this.html});
  1062. },
  1063. }
  1064. this.panelStyle = `
  1065. .v-binder-follower-container {
  1066. position: fixed;
  1067. }
  1068. #CWD-Configuration-Panel {
  1069. position: absolute;
  1070. top: 50px;
  1071. left: 50px;
  1072. width: 250px;
  1073. background-color: #FFFFFF;
  1074. border: #D7D8D9 1px solid;
  1075. border-radius: 4px;
  1076. resize: horizontal;
  1077. min-width: 200px;
  1078. overflow: auto;
  1079. color: black;
  1080. opacity: 0.95;
  1081. }
  1082. #CWD-Configuration-Panel .status-bar {
  1083. cursor: move;
  1084. background-color: #f0f0f0;
  1085. border-radius: 4px 4px 0 0;
  1086. display: flex;
  1087. }
  1088. #CWD-Configuration-Panel .status-bar .title {
  1089. display: flex;
  1090. align-items: center;
  1091. justify-content: left;
  1092. padding-left: 10px;
  1093. user-select: none;
  1094. color: #777;
  1095. flex: 1;
  1096. font-weight: bold;
  1097. }
  1098. #CWD-Configuration-Panel .status-bar .close {
  1099. cursor: pointer;
  1100. padding: 10px;
  1101. transition: color 0.3s;
  1102. }
  1103. #CWD-Configuration-Panel .status-bar .close:hover {
  1104. color: #f00;
  1105. }
  1106. #CWD-Configuration-Panel .container {
  1107. padding: 20px;
  1108. }
  1109. #CWD-Configuration-Panel .container .code-block {
  1110. padding: 10px;
  1111. border: 1px solid #d9d9d9;
  1112. border-radius: 4px;
  1113. }
  1114. #CWD-Configuration-Panel .container .button-group {
  1115. display: flex;
  1116. justify-content: center;
  1117. gap: 10px;
  1118. }
  1119. #CWD-Configuration-Panel .container .button-group > button {
  1120. width: 30%;
  1121. }`
  1122. this.panelHTML = `
  1123. <div id="${that.appID}" style="visibility: hidden">
  1124. <div class="status-bar">
  1125. <div class="title" id="${that.appID}-DraggableArea">{{title}}</div>
  1126. <n-button class="close" @click="onClose" text>
  1127. <n-icon size="20">
  1128. ${SystemConfig.ConfigPanel.Icon.Close}
  1129. </n-icon>
  1130. </n-button>
  1131. </div>
  1132. <n-scrollbar style="max-height: 80vh">
  1133. <div class="container">
  1134. <n-form :label-width="40" :model="configForm" label-placement="left">
  1135. <n-form-item v-show="!configForm.advanced.enable" label="模板" path="format">
  1136. <n-select v-model:value="configForm.format"
  1137. :disabled="configForm.advanced.enable" :render-label="renderLabel"
  1138. :options="formatOptions" @update:value="onConfigUpdate"></n-select>
  1139. </n-form-item>
  1140. <n-form-item v-show="!configForm.advanced.enable" label="预览" path="format">
  1141. <div v-html="reFormatTimeHTML(configForm.format)"></div>
  1142. </n-form-item>
  1143. <n-form-item v-show="!configForm.advanced.enable" label="代码" path="format">
  1144. <n-config-provider :hljs="hljs">
  1145. <div class="code-block">
  1146. <n-scrollbar style="max-height: 4em">
  1147. <n-code :code="configForm.format" language="html" word-wrap/>
  1148. </n-scrollbar>
  1149. </div>
  1150. </n-config-provider>
  1151. </n-form-item>
  1152. <n-form-item label="位置" path="mode">
  1153. <n-select v-model:value="configForm.mode"
  1154. :options="modeOptions" @update:value="onConfigUpdate"></n-select>
  1155. </n-form-item>
  1156. <n-form-item label="高级" path="advanced.enable">
  1157. <n-switch v-model:value="configForm.advanced.enable" @update:value="onConfigUpdate" />
  1158. </n-form-item>
  1159. <div v-show="configForm.advanced.enable">
  1160. <n-form-item label="HTML" path="advanced.htmlTextContent">
  1161. <n-input type="textarea" placeholder="请输入 HTML 代码" :autosize="{ minRows: 1, maxRows: 5 }"
  1162. @keydown.tab="insertTab" @update:value="onConfigUpdate"
  1163. v-model:value="configForm.advanced.htmlTextContent"/>
  1164. </n-form-item>
  1165. <n-form-item label="CSS" path="advanced.styleTextContent">
  1166. <n-input type="textarea" placeholder="请输入 CSS 代码" :autosize="{ minRows: 1, maxRows: 5 }"
  1167. @keydown.tab="insertTab" @update:value="onConfigUpdate"
  1168. v-model:value="configForm.advanced.styleTextContent"/>
  1169. </n-form-item>
  1170. <n-form-item label="JS" path="advanced.scriptTextContent">
  1171. <n-input type="textarea" placeholder="请输入 JavaScript 代码" :autosize="{ minRows: 1, maxRows: 5 }"
  1172. @keydown.tab="insertTab" @update:value="onConfigUpdate"
  1173. v-model:value="configForm.advanced.scriptTextContent"/>
  1174. </n-form-item>
  1175. </div>
  1176. </n-form>
  1177. <div class="button-group">
  1178. <n-button @click="onReset" :disabled="!configDirty">重置</n-button>
  1179. <n-button @click="onApply">应用</n-button>
  1180. <n-button @click="onConfirm">保存</n-button>
  1181. </div>
  1182. </div>
  1183. </n-scrollbar>
  1184. </div>`
  1185. this.appConfig = {
  1186. el: `#${that.appID}`,
  1187. data() {
  1188. return {
  1189. hljs: hljs,
  1190. date: new Date(),
  1191. title: "ChatGPTWithDate",
  1192. formatOptions: SystemConfig.TimeRender.TimeTagTemplates.map(item => {
  1193. return {label: item, value: item}
  1194. }),
  1195. modeOptions: [
  1196. {label: '角色之后(靠左)', value: 'AfterRoleLeft'},
  1197. {label: '角色之后(居右)', value: 'AfterRoleRight'},
  1198. {label: '角色之下', value: 'BelowRole'},
  1199. ],
  1200. configForm: {
  1201. format: that.userConfig.timeRender.format,
  1202. mode: that.userConfig.timeRender.mode,
  1203. advanced: {
  1204. enable: that.userConfig.timeRender.advanced.enable,
  1205. htmlTextContent: that.userConfig.timeRender.advanced.htmlTextContent,
  1206. styleTextContent: that.userConfig.timeRender.advanced.styleTextContent,
  1207. scriptTextContent: that.userConfig.timeRender.advanced.scriptTextContent,
  1208. },
  1209. },
  1210. config: {
  1211. format: that.userConfig.timeRender.format,
  1212. mode: that.userConfig.timeRender.mode,
  1213. advanced: {
  1214. enable: that.userConfig.timeRender.advanced.enable,
  1215. htmlTextContent: that.userConfig.timeRender.advanced.htmlTextContent,
  1216. styleTextContent: that.userConfig.timeRender.advanced.styleTextContent,
  1217. scriptTextContent: that.userConfig.timeRender.advanced.scriptTextContent,
  1218. },
  1219. },
  1220. configDirty: false,
  1221. configPanel: {
  1222. display: true,
  1223. },
  1224. };
  1225. },
  1226. methods: {
  1227. onApply() {
  1228. this.config = JSON.parse(JSON.stringify(this.configForm));
  1229. that.updateConfig(this.config)
  1230. this.configDirty = false
  1231. },
  1232. onConfirm() {
  1233. this.onApply()
  1234. this.onClose()
  1235. },
  1236. onReset() {
  1237. this.configForm = JSON.parse(JSON.stringify(this.config));
  1238. this.configDirty = false
  1239. },
  1240. onClose() {
  1241. that.hide()
  1242. },
  1243. onConfigUpdate() {
  1244. this.configDirty = true
  1245. },
  1246. renderLabel(option) {
  1247. return Vue.h(TimeTagComponent, {
  1248. html: option.label,
  1249. })
  1250. },
  1251. reFormatTimeHTML(html) {
  1252. return Utils.formatDateTimeByDate(this.date, html)
  1253. },
  1254. insertTab(event) {
  1255. if (!event.shiftKey) { // 确保不是 Shift + Tab 组合
  1256. event.preventDefault(); // 阻止 Tab 键的默认行为
  1257. // 尝试使用 execCommand 插入四个空格
  1258. if (document.queryCommandSupported('insertText')) {
  1259. document.execCommand('insertText', false, ' ');
  1260. } else { // 如果浏览器不支持 execCommand,回退到原始方法(不支持撤销)
  1261. const start = event.target.selectionStart;
  1262. const end = event.target.selectionEnd;
  1263. const value = event.target.value;
  1264. event.target.value = value.substring(0, start) + " " + value.substring(end);
  1265. // 移动光标位置到插入的空格后
  1266. event.target.selectionStart = event.target.selectionEnd = start + 4;
  1267. // 触发 input 事件更新 v-model
  1268. this.$nextTick(() => {
  1269. event.target.dispatchEvent(new Event('input'));
  1270. });
  1271. }
  1272. }
  1273. }
  1274. },
  1275. created() {
  1276. this.date = new Date()
  1277. this.formatOptions.forEach(item => {
  1278. item.label = this.reFormatTimeHTML(item.value)
  1279. })
  1280. },
  1281. mounted() {
  1282. this.timestampInterval = setInterval(() => {
  1283. this.date = new Date()
  1284. this.formatOptions.forEach(item => {
  1285. item.label = this.reFormatTimeHTML(item.value)
  1286. })
  1287. }, 50)
  1288. },
  1289. beforeUnmount() {
  1290. clearInterval(this.timestampInterval)
  1291. },
  1292. }
  1293. }
  1294.  
  1295. /**
  1296. * 初始化样式。
  1297. * 同时为了避免样式冲突,在 head 元素中加入一个 <meta name="naive-ui-style" /> 元素,
  1298. * naive-ui 会把所有的样式刚好插入这个元素的前面。
  1299. * 参考 https://www.naiveui.com/zh-CN/os-theme/docs/style-conflict
  1300. *
  1301. * @returns {Promise}
  1302. * @private
  1303. */
  1304. _initStyle() {
  1305. return new Promise(resolve => {
  1306. const meta = document.createElement('meta');
  1307. meta.name = 'naive-ui-style'
  1308. document.head.appendChild(meta);
  1309. this.styleService.updateStyle(SystemConfig.ConfigPanel.StyleKey, this.panelStyle)
  1310. resolve()
  1311. })
  1312. }
  1313.  
  1314. /**
  1315. * 初始化 Vue 与 Naive UI 脚本。无法使用 <script src="https://xxx"> 的方式插入,因为 ChatGPT 有 CSP 限制。
  1316. * 采用 GM_xmlhttpRequest 的方式获取 Vue 与 Naive UI 的脚本内容,然后插入 <script>脚本内容</script> 到页面中。
  1317. *
  1318. * @returns {Promise}
  1319. * @private
  1320. */
  1321. _initExternalResources() {
  1322. return new Promise(resolve => {
  1323. let completeCount = 0;
  1324. const resources = [
  1325. {type: 'js', url: 'https://unpkg.com/vue@3.4.26/dist/vue.global.js'},
  1326. {type: 'js', url: 'https://unpkg.com/naive-ui@2.38.1/dist/index.js'},
  1327. {type: 'css', url: 'https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/default.min.css'},
  1328. {type: 'js', url: 'https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js'},
  1329. ]
  1330. const addScript = (content) => {
  1331. let script = document.createElement('script');
  1332. script.textContent = content;
  1333. document.body.appendChild(script);
  1334. completeCount++;
  1335. if (completeCount === resources.length) {
  1336. resolve()
  1337. }
  1338. }
  1339. const addStyle = (content) => {
  1340. let style = document.createElement('style');
  1341. style.textContent = content;
  1342. document.head.appendChild(style);
  1343. completeCount++;
  1344. if (completeCount === resources.length) {
  1345. resolve()
  1346. }
  1347. }
  1348. resources.forEach(resource => {
  1349. GM_xmlhttpRequest({
  1350. method: "GET", url: resource.url, onload: function (response) {
  1351. if (resource.type === 'js') {
  1352. addScript(response.responseText);
  1353. } else if (resource.type === 'css') {
  1354. addStyle(response.responseText);
  1355. }
  1356. }
  1357. });
  1358. })
  1359. // 以下方法有 CSP 限制
  1360. // const naiveScript = document.createElement('script');
  1361. // naiveScript.setAttribute("type", "text/javascript");
  1362. // naiveScript.text = "https://unpkg.com/naive-ui@2.38.1/dist/index.js";
  1363. // document.documentElement.appendChild(naiveScript);
  1364. })
  1365. }
  1366.  
  1367. /**
  1368. * 初始化配置面板,插入配置面板的 HTML 到 body 中。
  1369. *
  1370. * @returns {Promise}
  1371. * @private
  1372. */
  1373. _initPanel() {
  1374. const that = this
  1375. return new Promise(resolve => {
  1376. const panelRoot = document.createElement('div');
  1377. panelRoot.innerHTML = that.panelHTML;
  1378. document.body.appendChild(panelRoot);
  1379. resolve()
  1380. })
  1381. }
  1382.  
  1383. /**
  1384. * 初始化 Vue 实例,挂载到配置面板的 HTML 元素上。
  1385. *
  1386. * @private
  1387. */
  1388. _initVue() {
  1389. const app = Vue.createApp(this.appConfig);
  1390. app.use(naive)
  1391. app.mount(`#${this.appID}`);
  1392. }
  1393.  
  1394. /**
  1395. * 初始化配置面板大小与位置
  1396. *
  1397. * @private
  1398. */
  1399. _initConfigPanelSizeAndPosition() {
  1400. const panel = document.getElementById(this.appID)
  1401.  
  1402. // 获取存储的大小
  1403. const size = GM_getValue(SystemConfig.GMStorageKey.ConfigPanel.Size, {})
  1404. if (size && size.width && !isNaN(size.width)) {
  1405. panel.style.width = size.width + 'px';
  1406. }
  1407.  
  1408. // 获取存储的位置
  1409. const position = GM_getValue(SystemConfig.GMStorageKey.ConfigPanel.Position, {})
  1410. if (position && position.left && position.top && !isNaN(position.left) && !isNaN(position.top)) {
  1411. const {left, top} = position
  1412. const {refineLeft, refineTop} = this.refinePosition(left, top, panel.offsetWidth, panel.offsetHeight)
  1413. panel.style.left = refineLeft + 'px';
  1414. panel.style.top = refineTop + 'px';
  1415. }
  1416.  
  1417. // 如果面板任何一边超出屏幕,则重置位置
  1418. // const rect = panel.getBoundingClientRect()
  1419. // const leftTop = {
  1420. // x: rect.left,
  1421. // y: rect.top
  1422. // }
  1423. // const rightBottom = {
  1424. // x: rect.left + rect.width,
  1425. // y: rect.top + rect.height
  1426. // }
  1427. // const screenWidth = window.innerWidth;
  1428. // const screenHeight = window.innerHeight;
  1429. // if (leftTop.x < 0 || leftTop.y < 0 || rightBottom.x > screenWidth || rightBottom.y > screenHeight) {
  1430. // panel.style.left = '50px';
  1431. // panel.style.top = '50px';
  1432. // }
  1433.  
  1434. }
  1435.  
  1436. /**
  1437. * 初始化配置面板事件监控,包括面板拖动、面板大小变化等事件。
  1438. *
  1439. * @private
  1440. */
  1441. _initConfigPanelEventMonitor() {
  1442. const that = this
  1443. const panel = document.getElementById(this.appID)
  1444. const draggableArea = document.getElementById(`${this.appID}-DraggableArea`)
  1445.  
  1446. // 监听面板宽度变化
  1447. const resizeObserver = new ResizeObserver(entries => {
  1448. for (let entry of entries) {
  1449. if (entry.contentRect.width) {
  1450. GM_setValue(SystemConfig.GMStorageKey.ConfigPanel.Size, {
  1451. width: entry.contentRect.width,
  1452. })
  1453. }
  1454. }
  1455. });
  1456. resizeObserver.observe(panel);
  1457.  
  1458. // 监听面板位置
  1459. draggableArea.addEventListener('mousedown', function (e) {
  1460. const offsetX = e.clientX - draggableArea.getBoundingClientRect().left;
  1461. const offsetY = e.clientY - draggableArea.getBoundingClientRect().top;
  1462.  
  1463. function mouseMoveHandler(e) {
  1464. const left = e.clientX - offsetX;
  1465. const top = e.clientY - offsetY;
  1466. const {
  1467. refineLeft, refineTop
  1468. } = that.refinePosition(left, top, panel.offsetWidth, panel.offsetHeight);
  1469. panel.style.left = refineLeft + 'px';
  1470. panel.style.top = refineTop + 'px';
  1471. GM_setValue(SystemConfig.GMStorageKey.ConfigPanel.Position, {
  1472. left: refineLeft, top: refineTop,
  1473. })
  1474. }
  1475.  
  1476. document.addEventListener('mousemove', mouseMoveHandler);
  1477. document.addEventListener('mouseup', function () {
  1478. document.removeEventListener('mousemove', mouseMoveHandler);
  1479. });
  1480. });
  1481. }
  1482.  
  1483. /**
  1484. * 限制面板位置,使其任意一部分都不超出屏幕
  1485. *
  1486. * @param left 面板左上角 x 坐标
  1487. * @param top 面板左上角 y 坐标
  1488. * @param width 面板宽度
  1489. * @param height 面板高度
  1490. * @returns {{refineLeft: number, refineTop: number}} 返回修正后的坐标
  1491. */
  1492. refinePosition(left, top, width, height) {
  1493. const screenWidth = window.innerWidth;
  1494. const screenHeight = window.innerHeight;
  1495. return {
  1496. refineLeft: Math.min(Math.max(0, left), screenWidth - width),
  1497. refineTop: Math.min(Math.max(0, top), screenHeight - height),
  1498. }
  1499. }
  1500.  
  1501. /**
  1502. * 初始化菜单命令,用于在 Tampermonkey 的菜单中添加一个配置面板的命令。
  1503. *
  1504. * @private
  1505. */
  1506. _initMenuCommand() {
  1507. let that = this
  1508. GM_registerMenuCommand("配置面板", () => {
  1509. that.show()
  1510. })
  1511. }
  1512.  
  1513. /**
  1514. * 显示配置面板
  1515. */
  1516. show() {
  1517. document.getElementById(this.appID).style.visibility = 'visible';
  1518. }
  1519.  
  1520. /**
  1521. * 隐藏配置面板
  1522. */
  1523. hide() {
  1524. document.getElementById(this.appID).style.visibility = 'hidden';
  1525. }
  1526.  
  1527. /**
  1528. * 更新配置,由 Vue 组件调用来更新配置并重新渲染时间
  1529. * @param config
  1530. */
  1531. updateConfig(config) {
  1532. this.userConfig.update({timeRender: config})
  1533. this.timeRendererService.reRender()
  1534. }
  1535. }
  1536.  
  1537. class Main {
  1538. static ComponentsConfig = [
  1539. UserConfig, StyleService, MessageService,
  1540. MonitorService, TimeRendererService, ConfigPanelService,
  1541. JavaScriptService,
  1542. ]
  1543.  
  1544. constructor() {
  1545. for (let componentClazz of Main.ComponentsConfig) {
  1546. const instance = new componentClazz();
  1547. this[componentClazz.name] = instance
  1548. ComponentLocator.register(componentClazz, instance)
  1549. }
  1550. }
  1551.  
  1552. /**
  1553. * 获取依赖关系图
  1554. * @returns {[]} 依赖关系图,例如 [{node:'ComponentClass0', dependencies:['ComponentClass2', 'ComponentClass3']}, ...]
  1555. * @private
  1556. */
  1557. _getDependencyGraph() {
  1558. const dependencyGraph = []
  1559. for (let componentClazz of Main.ComponentsConfig) {
  1560. const dependencies = this[componentClazz.name].dependencies.map(dependency => dependency.clazz.name)
  1561. dependencyGraph.push({node: componentClazz.name, dependencies})
  1562. }
  1563. return dependencyGraph
  1564. }
  1565.  
  1566. start() {
  1567. const dependencyGraph = this._getDependencyGraph()
  1568. const order = Utils.dependencyAnalysis(dependencyGraph)
  1569. Logger.debug('初始化顺序:', order.join(' -> '))
  1570. for (let componentName of order) {
  1571. this[componentName].initDependencies()
  1572. }
  1573. for (let componentName of order) {
  1574. this[componentName].init()
  1575. }
  1576. }
  1577. }
  1578.  
  1579. const main = new Main();
  1580. main.start();
  1581.  
  1582. })();