ChatGPT with Date

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

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

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