jira日志记录功能增强

一些技术支持需要用到的辅助功能, 目前包括: 日志快捷记录, 自动暂存文本框内容

// ==UserScript==
// @name         jira日志记录功能增强
// @namespace    armstrong@fanruan.com
// @version      4.4.13.2
// @description  一些技术支持需要用到的辅助功能, 目前包括: 日志快捷记录, 自动暂存文本框内容
// @author       LeoYuan&Armstrong
// @license      CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @match        https://work.fineres.com/browse/*
// @include      /^https?://work\.fineres\.com.*
// @icon         https://work.fineres.com/s/3e84z9/805005/12f785fd3d3d0d63b7c21a41e0d048b2/_/jira-favicon-hires.png
// @supportURL   https://greasyfork.org/en/scripts/415068
// @require      https://greasyfork.org/scripts/455782-fork-of-gm-config/code/fork%20of%20GM_config.js
// @grant        none
// ==/UserScript==

/* DONE:
- fix: 并没有修复看板上的记录日志也能打开日志效果
*/

/* TODO:
- 优化埋点: 本地汇总后定期发送, 减少请求量;
- 冲突检测: 同时启用了多个这个插件的话, 进行提示;
- 主动升级提醒;
*/



(function() {
  'use strict';
  console.warn('------------------------- script start -------------------------')
  var JIRA = window.JIRA || {};
  var AJS = window.AJS || {};

  // ----------------------------------------------------------------------------------------------------
  // # 一些变量
  // ### 可配置选项;
  const configOptionsFrame = document.createElement('div')
  document.body.appendChild(configOptionsFrame);
  const CONFIG = {
    "id": "JIRA_USERSCRIPT_OPTIONS",
    "title": "jira日志记录功能增强脚本 配置界面",
    "frame": configOptionsFrame,
    "fields": {
      "localization": { "type": "checkbox", "label": "自动翻译部分英文标签", "default": true, "section": ['通用'], },
      "WORKLOGOBSTACLES_type": { "type": "radio", "label": "日志记录-障碍点类型", "options": ["默认", "融合区测试1"], "default": "默认", "section": ['日志记录助手'], },
      "WORKLOGOBSTACLEHINTS_show": { "type": "checkbox", "label": "日志记录-显示障碍点参考方案提示", "default": true, },
    },
  };


  // ### 自定义日志快捷记录, 每条一个数组, 数组第一项是记录名称字符串, 第二项是时长字符串, 第三项是内容字符串. (注意需要带标签, 有人要审核标签)
  // *** (注意此配置暂时无法保存, 每次重新安装或更新后都要重新定义) ***
  const WORKLOGS = {
    ALL: [
      {name: '1m_待发布', duration: '1m', state: '修复待发布', obstacles: [], extra: '', location: ['right-panel', 'dropdown-panel']},
      {name: '我未跟进', duration: '1m', state: '', obstacles: ['未跟进'], extra: '', location: ['right-panel', 'dropdown-panel']},
      {name: '关问题', duration: '5m', state: '实施后观察效果', obstacles: [], extra: '关问题', location: ['right-panel', 'dropdown-panel']},
    ],
    filter: (location) => WORKLOGS.ALL.filter(log => log.location.includes(location)),
    content: (log) => `${TEMPLATES.status(log.state)}\n${TEMPLATES.obstacle(log.obstacles)}\n${log.extra}`,
    filterByName: (name) => WORKLOGS.ALL.filter(log => log.name.includes(name))[0] || {},
  };

  // ### 重构后的问题状态日志记录字段.  请勿随意修改! 无效的字段或格式是无法被crm处理统计的!! 事关个人绩效.请勿随意修改!
  const WORKLOGSTATES = [ '沟通和定位', '方案准备和交付', '__SPACE__', '方案待实施', '实施后观察效果', '__SPACE__', '研发处理', '修复待发布', '二开沟通阶段', ];
  const WORKLOGOBSTACLES = () => {return [ '一线给了错误信息', '文档找不到或缺失', '文档错误', '流程不清晰', '__SPACE__', '环境特殊', '内网', '需搭建环境', '__SPACE__', '未到约定沟通日期', '客户无响应', '客户配合意愿较低', '__SPACE__', '未跟进', '反复修改需求', '难还原待复现', '不知道找谁/找了进度慢', ].concat(GM_config.get("WORKLOGOBSTACLES_type")=="融合区测试1" ? ["专家处理慢", "三线处理慢"] : []) };
  const WORKLOGOBSTACLEHINTS = {
    '三线处理慢': '1、高频率催促2、识别到客户的紧急情绪或问题本身重要紧急,且自行推进不动时,反馈至组长',
    '专家处理慢': '1、专家节点停留超过24h,主动询问是否转三线处理2、专家未处理满24h,主动研究问题或寻找其他内部资源',
    '内网': '理清思路(如本地复现找到日志报错关键字),整理一份清单,一次性发给客户来获取信息。',
    '反复修改需求': '按照自己的理解,使用专业术语,复述客户的需求,让客户确认',
    '客户配合度差且无法延长确诊': '1、电话沟通,以期高效配合2、若问题不严重、不紧急,则可与客户协商有空的时间再处理',
    '文档找不到或缺失': '1、帮助文档问题,提交文档demo任务2、内部文档问题,联系专家确认;或反馈至学习委员',
    '文档错误': '1、帮助文档问题,提交文档demo任务2、内部文档问题,联系专家确认;或反馈至学习委员',
    '未跟进': '评估是否需要交接给他人处理',
    '流程不清晰': '及时反馈至师傅>组长>学习委员。',
    '环境特殊': '待补充。拨开云雾见青天,透过现象看本质。',
    '难还原待复现': '专家是否确认难还原',
    '需搭建环境': '若客户问题提出已超过24h,则先转专家协助,或组内询问是否有现成环境,后本地搭建,复盘问题',
  }
  // #### 以下两个变量用来根据问题状态/类别隐藏不需要的问题状态选项的. 逻辑是HIDEITEMRULES中的键对应html元素会带有一个"_标识_"的属性标记, 然后hideUnnecessaryItem()方法会根据问题实际的状态/类别和ISSUEINFODICT定义的状态进行"_标识_"的剔除, 剩下来的没剔除掉的, 都会设置隐藏;
  const HIDEITEMRULES = {
    '研发处理': ['_研发处理中_'],
    '修复待发布': ['_待发布_'],
    '二开沟通阶段': ['_二开中_'],
    '沟通和定位': ['_SLA待确诊_'],
    '方案准备和交付': ['_SLA待确诊_'],
    '方案待实施': ['_SLA已确诊_'],
    '实施后观察效果': ['_SLA已确诊_'],
    '一线给了错误信息': ['_SLA待确诊_', '_SLA已确诊_'],
  };
  const ISSUEINFODICT = {
    PROJECT: {
      '三线': ['REPORT', 'BI', 'DEC', 'MOBILE', 'CHART', ],
      '二开': ['JSD', 'TM', 'SLN'],
    },
    STATUS: {
      'SLA待确诊': ['技术支持小组长', '组长分配', '待恢复', '待确诊'],
      'SLA已确诊': ['待观察', '组长审核', '已解决', 'bug已解决', '协商关闭'],
      '三线处理中': ['测试处理', '研发组员问题解决中', '测试组员测试验收中', '研发组长验收', '产品确认'].concat(['待分配', '组长分配', '子模块组长分配', '测试组长分配', '架构组分配']).concat(['重复BUG待解决']),
      '三线待发布': ['验收通过待发布'].concat(['重复BUG待解决']),
      '三线已解决': ['被否决', '已解决', '创建者确认', '创建者验收'].concat(['产品确认']),
      '二开中': [''],
    }
  };

  // ### 一些预定义的html内容, 请勿随意修改! 会影响脚本正常运行!
  const HTMLCONTENT = {
    logWorkRightPanel: (issuekey) => `<tr class="quick-work-log-tr"><td class="quick-work-log-td">${WORKLOGS.filter('right-panel').map(log => `<div class="quick-work-log-btn" data-location="issueRightPanel" data-log="${log.name}" data-issuekey="${issuekey}">${log.name}</div>`).join('')}</td></tr>`,
    logWorkDropdownPanel: (issuekey) => `<ul class="aui-list-section quick-work-log-ul">${WORKLOGS.filter('dropdown-panel').map(log => {return `<li class="aui-list-item quick-work-log-li"><a class="aui-list-item-link quick-work-log-item" role="menuitem" data-location="dashboardDropdownPanel" data-log="${log.name}" data-issuekey="${issuekey}">${log.name}</a></li> \n `}).join('') }</ul> \n `,
    logWorkDialog: (issuekey) => `<div class="quick-work-log-dialog-div">${WORKLOGS.filter('log-work-dialog').map(log => {return `<div class="quick-work-log-btn" data-location="logWorkDialog" data-log="${log.name}" data-issuekey="${issuekey}">${log.name}</div>`}).join('') }</div>`,

    fieldGroup: (inner) => `<div class="field-group" id="wp-fg-status"><label for="log-work-status">Issue Status<span class="aui-icon icon-required"> Required</span></label><div class="log-work-status-pick-box">${inner}</div></div>`,
    statePanelBox: () => `<div class="log-work-status-panel state-panel" id="log-work-status-state-panel">${HTMLCONTENT.stateListHeader}<div class="log-work-status-list-scroll"><div class="aui-list log-work-status-list state-ul">${WORKLOGSTATES.map(stt => stt == '__SPACE__' ? HTMLCONTENT.spacerItem.repeat(2) : HTMLCONTENT.stateItem(stt)).join('')}</div></div></div>`,
    stateListHeader: `<div class="log-work-status-list-header list-header">当前问题状态*</div>`,
    stateItem: (state) => `<div class="log-work-status-list-item state-item" state="${state}" ${HIDEITEMRULES[state] && HIDEITEMRULES[state].join(' ') || ''} ><p class="aui-list-item-link"><span>${state}</span></p></div>`,
    obstaclePanelBox: () => `<div class="log-work-status-panel obstacle-panel" id="log-work-status-obstacle-panel">${HTMLCONTENT.obstacleListHeader}<div class="log-work-status-list-scroll"><div class="aui-list log-work-status-list obstacle-ul">${WORKLOGOBSTACLES().map(obs => obs == '__SPACE__' ? HTMLCONTENT.spacerItem : HTMLCONTENT.obstacleItem(obs)).join('')}</div></div></div>`,
    obstacleListHeader: `<div class="log-work-obstacle-list-header list-header">当前障碍点<span class="list-header-tip">(可多选)</span></div>`,
    obstacleItem: (obstacle) => `<div class="log-work-status-list-item obstacle-item" obstacle="${obstacle}" ${HIDEITEMRULES[obstacle] && HIDEITEMRULES[obstacle].join(' ') || ''} ><p class="aui-list-item-link"><span>${obstacle}</span></p></div>`,
    spacerItem: `<div  class="log-work-status-list-item spacer-item">&nbsp;</div>`,

    solutionTip: `<div class="description">没有定位到问题原因的可以写能帮助判断所属模块的原因</div>`,
    outerSolutionInvalidTip: `<div class="description" id="outerSolutionInvalidTip">对外解决方案请不要包含【原因】字段</div>`,
    autoStorageTip: (msg) => `<div class="auto-saved-tip" title="内容缓存到本地session啦,  找回内容请找 LeoYuan">~~~${msg || '内容已自动缓存'}~~~</div>`,
    fieldCompanyInfo: (el) => `${el.innerText}<a class="added-field-button" href="https://crm.finereporthelp.com/WebReport/decision/view/report?reportlet=customer/company_view.cpt&op=view&comid=${el.innerText.trim()}" target="_blank">crm客户信息</a>`,
    fieldCompanyName: (el) => `${el.innerText}<a class="added-field-button" href="https://crm.finereporthelp.com/WebReport/decision/view/report?autoQuery=true&viewlet=customer/contact_list.cpt&op=write&COMNAME=${encodeURIComponent(el.innerText.trim())}" target="_blank">crm查客户</a>`,
    fieldCustomerAcitivities: (el) => `<a class="added-field-button" href="${el.href}" target="_blank">crm客户活动</a>`,
    fieldChatWithQQ: (el) => `${el.innerText}<a class="added-field-button" href="tencent://message/?uin=${el.innerText}&Site=qq&Menu=yes" target="_blank">发起聊天</a>`,
    ModuleExpertFeedback: (el) => `${el.innerText}<a class="added-field-button" href="https://t6ixa9nyl6.jiandaoyun.com/f/624274c82280110008d32e36?ext=sla" target="_blank">意见反馈</a>`,
    fieldModuleExpert: (name, sameUser) => `<dl><dt title="专家">专家:</dt><dd><span class="user-hover" rel="${name}" id="user_cf_${name}">${name}</span>${sameUser ? '' : HTMLCONTENT.MESupportButton}${sameUser ? HTMLCONTENT.MECreateTESTaskButton : ''}</dd></dl>`,
    MESupportType: (list) => ['<div class="mesupporttype-button-list">', list.map( type => `<a href="javascript:quickFieldEdit('customfield_19449', '${type[0]}', 3)" class="added-field-button">${type[1]}</a>`).join(''), '</div>'].join(''),
    MESupportButton: `<a href="javascript:handleModuleExpertButton()" class="added-field-button">支撑</a>`,
    MECreateTESTaskButton: `<a href="javascript:createTESTask()" class="added-field-button">建学习任务</a>`,
    fieldResponsibleModuleExpert: (name) => `<dl><dt title="责任专家">责任专家:</dt><dd><span class="user-hover" rel="${name}" id="user_cf_${name}">${name}</span></dd></dl>`,
    reportSensitiveWordsButton: `<a href="https://www.jiandaoyun.com/app/59f96c4ae1ddba302aaa624a/entry/619eeea6dd59100007fc5d08" id="reportSensitiveWords" class="aui-button" target="_blank">反馈敏感词</a>`,
    diagnoseReportButton: (issuekey) => `<a href="https://report.fineres.com/webroot/decision/view/report?viewlet=%25E5%2585%25AC%25E5%2585%25B1%252Fsupport%252F%25E4%25B8%2593%25E5%25AE%25B6%252FDiagnose.cpt&op=write&工单=${issuekey}&ly=jira0" id="diagnoseReport" class="aui-button" target="_blank">诊断书</a>`,
    custRiskEvaluateButton: (mobile) => `<a href="https://crm.finereporthelp.com/WebReport/decision/view/report?viewlet=support_success%252Fcust_risk_evaluate.cpt&op=view&mobile=${mobile}" id="custRiskEvaluate" class="aui-button" target="_blank">风险评估书</a>`,
    custNLPButton: () => `<a href="javascript:handleNLPstuff();" id="NLPbutton" class="aui-button">NLP</a>`,
    configOptionLI: `<li> <a id="config_options" class="config_options" title="配置jira脚本的功能" tabindex="-1">设置jira脚本</a> </li>`,
  }

  // ### 一些文本格式模板, 请勿随意修改!
  const TEMPLATES = {
    status: (inner) => inner ? `【[-${inner}-]】` : '',
    statusRegex: /【\[-[^】]{2,20}-\]】/g,
    statusRegexMatch: /【\[-([^】]{2,30})-\]】/,
    obstacle: (inner) => inner.length ? `当前障碍点: ${JSON.stringify(inner)}` : '',
    obstacleRegex: /当前障碍点:\s*\["[^\]]{2,256}\"]/g,
    solutionRegex: /^[\s\S]*【问题原因】([\s\S]*?)【解决方案】([\s\S]*?)(?:【操作手册】([\s\S]*?))?(?:【风险规避】([\s\S]*?))?$/,
    descPrefixes: [/遇到的异常:/, /想实现的效果:/, /【原始需求】/, /问题描述-?:?/, /【现象】/],
    outerSolutionInvalidRegex: /【原因】|【问题原因】/,
  }

  // ### 一些文本性内容
  const TEXTMSG = {
    template4InnerSolution: (cause, solution) => `【问题原因】${cause || ""}\n【解决方案】${solution || ""}\n【操作手册】\n【风险规避】`,
    template4OuterSolution: (cause, solution) => `【解决方案】${solution || ""}`,
    tipOnSolution4Customer: `请注意文明用语,内容客户可见!\n请勿泄露公司内部信息,人员姓名等!\n请谨慎使用“BUG”来描述问题,以免客户误解`,
    errMsg4IssueInfo: '工单信息获取出现问题, 请联系LeoYuan或Armstrong处理',
    errMsg4logWork: (inner) => `记录失败"${inner}", 请联系LeoYuan或Armstrong处理`,
    errMsg4FindIssueInfo: () => `获取issue信息失败了, 请联系LeoYuan处理`,
    errMsg4FieldEdit: (f,v) => ` ${f} 字段设置为 ${v} 失败, 请联系LeoYuan处理`,
    successMsg4FieldEdit: (f,v) => ` ${f} 字段设置为 ${v} 成功`,
    successMsg4logWork: (inner) => `记录成功"${inner}"`,
  }

  // ### 预定义的翻译国际化, 可根据自身需求修改. lang是匹配的浏览器语言. trans是自定义的内容.
  // * selector注意尽可能唯一确定, 范围过大的话所有匹配到的内容都会翻译掉;
  // * from为空时整个元素的innerText替换为to, from不为空时执行replace操作, 元素中匹配到from的内容替换为to;
  // * 支持添加callback方法自定义翻译, 比如有些元素的文本不在innerText中, 可以自定义方法进行修改. 方法默认接受三个参数, 单个的元素, from文本, to文本;
  const LOCALIZATION = [{
    lang: ['zh', 'zh-CN'],
    trans: [
      {selector: 'label[for="log-work-issue-picker-field"]', from: '', to: '任务'},
      {selector: 'label[for="log-work-worklog-author-field"]', from: '', to: '用户'},
      {selector: 'label[for="log-work-period"]', from: '', to: '阶段'},
      {selector: 'label[for="log-work-status"]', from: '', to: '阶段'},
      {selector: 'label[for="log-work-time-logged"]', from: '', to: '花费时间'},
      {selector: 'label[for="log-work-date-logged"]', from: '', to: '工时开始时间'},
      {selector: 'label[for="wp-copy-to-issue-comments"]', from: '', to: '复制到任务的评论中'},
      {selector: 'label[for="comment"]', from: '', to: '进展'},
      {selector: '#wp-fg-estimates label', from: '', to: '剩余工作时间'},
      {selector: '[class="aui-form example"]', from: '', to: '(例如 3d 12h 30m,单位需小写)'},
      {selector: 'div#WorklogByUserPanel a#add-worklog-issue-right-panel-link', from: '', to: 'Log Work(其他情况点我记录)'},
      {selector: '#worklogpro-log-work-submit', from: '', to: '确认记录工时', callback: (el, from, to) => {el.value = to}},
    ]
  }]

  // ----------------------------------------------------------------------------------------------------
  // # 一些预定义的常用方法
  // ## 通用的一些方法
  // ### 往页面插入自定义js的方法
  window.addCss = function(cssString) {
    const head = document.getElementsByTagName('head')[0];
    const newCss = document.createElement('style');
    newCss.type = "text/css";
    newCss.innerHTML = cssString;
    head.appendChild(newCss);
  }
  // ### 修改xhr方法, 用来拦截请求, 在请求加载后执行操作
  const __send = window.XMLHttpRequest.prototype.send;
  window.XMLHttpRequest.prototype.send = function() {
    if(window.loadInterceptions) {
      this.addEventListener('loadend', e => {
        window.loadInterceptions.filter(i => typeof(i.match) == 'function' && typeof(i.handler) == 'function').forEach((intercept) => {
          if(intercept.match(e)) intercept.handler(e);
        });
        //console.log('intercepted', e);
      }, { once: true });
    }
    __send.apply(this, arguments);
  };
  // ### 全局的监听方法, 包装了一下MutationObserver, 当监听到指定选择器的元素被加载时, 执行回调
  window.waitForAddedNode = function(params) {
    if(params.immediate) {
      const matched = [];
      matched.push(...document.querySelectorAll(params.selector));
      const smatched = [...new Set(matched)]
      if(!params.urlmatcher || location.href.includes(params.urlmatcher)) {
        for (const el of smatched) {
          params.done(el);
        }
      }
    }
    const observer = new MutationObserver(mutations => {
      const matched = [];
      for (const { addedNodes }
           of mutations) {
        for (const n of addedNodes) {
          if (!n.tagName) continue;
          if (n.matches(params.selector)) {
            matched.push(n);
          } else if (n.firstElementChild) {
            matched.push(...n.querySelectorAll(params.selector));
          }
        }
      }
      const smatched = [...new Set(matched)]
      if (smatched && params.once) this.disconnect();
      if(!params.urlmatcher || location.href.includes(params.urlmatcher)) {
        for (const el of smatched) {
          params.done(el);
        }
      }
    });
    observer.observe(document.querySelector(params.parent) || document.body, {
      subtree: !!params.recursive || !params.parent,
      childList: true,
    });
  }
  // ### 功能埋点方法, 记录使用情况
  window.eventTracker = {
    log: (event, text, storage) => {
      const data = window.eventTracker.__prepareData(event, text);
      var xhr = new XMLHttpRequest();
      xhr.addEventListener("readystatechange", function() {
        if(this.readyState === 4) {
          //          console.log('-------------------------\n', event, data, storage, this.responseText);
          if(typeof(storage) == 'object' && storage.k && storage.v) localStorage.setItem(storage.k,storage.v)

        }
      });
      xhr.open("POST", "https://fr-support.fineres.com:60083/c447bb0737483295a34e14ec74c61589");
      xhr.setRequestHeader("Content-Type", "application/json");
      xhr.send(data);
    },
    __prepareData: (event, data) => JSON.stringify({
      t: 'JIRA',
      c: AJS.params.loggedInUser || 'unknown',
      e: event || 'none',
      i: JSON.stringify(data || {}),
    }),
    __installedNotify: () => {
      if(localStorage.getItem('__scriptInstalled') && localStorage.getItem('__scriptInstalled') == GM_info.script.version) return;
      const data = {
        version: GM_info.script.version,
        browser: navigator.userAgent,
        languare: navigator.language,
        time: +new Date(),
      };
      return window.eventTracker.log('scriptInstalled', data, {k:'__scriptInstalled', v:GM_info.script.version});
    },
  }
  window.et = window.eventTracker;
  // ### 自动暂存填写内容
  window.tipTimer = 0;
  window.autoSaveKeyGenerator = function (el) {
    return ['__autoSave', location.pathname, el.parentNode.attributes && el.parentNode.attributes.id && el.parentNode.attributes.id.value || '', el.attributes && el.attributes.id && el.attributes.id.value || ''].join('__');
  }
  window.autoSave = function(el) {
    const keyName = window.autoSaveKeyGenerator(el);
    el.addEventListener('keyup', function(e){
      //      var msg = '内容已自动缓存';
      clearTimeout(window.tipTimer);
      const data = {
        value: event.target.value,
        timestamp: + new Date(),
      };
      if(data.value){
        localStorage.setItem(keyName, JSON.stringify(data));
      } else {
        localStorage.removeItem(keyName);
        //        msg = '缓存内容已清除';
      }
      //      window.tipTimer = setTimeout(function(){
      //        if(!el.parentNode.querySelector('.auto-saved-tip')) el.insertAdjacentHTML('afterend', HTMLCONTENT.autoStorageTip(msg));
      //        setTimeout(function(){el.parentNode.querySelectorAll('.auto-saved-tip').forEach(el => el.remove()); }, 10000);
      //      }, 500)
    })
  };
  // ### 恢复自动暂存的内容
  window.restoreSavedValue = function(el) {
    const keyName = window.autoSaveKeyGenerator(el);
    const data = JSON.parse(localStorage.getItem(keyName));
    if(!data || !data.timestamp || !data.value) return;
    if(data.timestamp < (new Date() - 60*60*1000)) return;
    if(el.value && el.value != '无') return;
    el.value = data.value;
    el.insertAdjacentHTML('afterend', HTMLCONTENT.autoStorageTip('已恢复缓存内容. 无需恢复请清空该控件内容后刷新页面. 详询 LeoYuan'))
  };
  // ### 自动清理过早的记录
  window.cleanEarlySavedValue = function(days) {
    if(localStorage.getItem('__autoSaveLastCleanTime__') > (new Date() - 24*60*60*1000)) return ;
    const timepoint = new Date() - (days || 7)*24*60*60*1000;
    Object.keys(localStorage).filter(keyName => keyName.match(/^__autoSave__/))
      .filter(keyName => JSON.parse(localStorage.getItem(keyName)).timestamp < timepoint)
      .forEach(keyName => localStorage.removeItem(keyName));
    localStorage.setItem('__autoSaveLastCleanTime__', + new Date());
  }


  // -------------------------------------------------
  // ## 一些适用于jira页面的方法
  // ### 快捷写日志方法
  window.quickWorkLog = function(duration, message, key, target){
    window.et.log('quickWorkLog', {target: target});
    if(!duration) JIRA.Messages.showErrorMsg('未提供日志时长');
    if(!message) JIRA.Messages.showErrorMsg('未提供日志内容');

    const user = AJS.params.loggedInUser;
    const issuekey = key || document.querySelector('meta[name="ajs-issue-key"]').attributes.content.value;
    const atltoken = document.querySelector('meta[name="atlassian-token"]').attributes.content.value;
    const startDate = window.moment(new Date).format(JIRA.translateSimpleDateFormat('yyyy-MM-dd HH:mm'));
    const endDate = window.moment(new Date).format(JIRA.translateSimpleDateFormat('yyyy-MM-dd'));

    return AJS.$.post({
      url:'https://work.fineres.com/secure/WPCreateWorklogOnIssueWithWorkLogAction.jspa?atl_token=' + atltoken,
      header:{
        'content-type': 'application/x-www-form-urlencoded'
      },
      data:{
        inline: true,
        decorator: 'dialog',
        jql: '',
        navFilter: '',
        issueKey: issuekey,
        worklogAuthorUsername: user,
        period: 'none',
        timeLogged: duration,
        startDateJS: startDate,
        startDate: startDate,
        endDateJS: endDate,
        endDate: endDate,
        adjustEstimate: 'auto',
        remainingEstimateOptionsExpanded: false,
        comment: message,
        commentLevel: ''
      },
      success: function(){
        JIRA.Messages.showSuccessMsg(TEXTMSG.successMsg4logWork(message));
        if(document.querySelector('div#worklog-tabpanel')){
          document.querySelector('a#comment-tabpanel').click();
          document.querySelector('a#worklog-tabpanel').click();
        }
      },
      fail: function(){
        JIRA.Messages.showErrorMsg(TEXTMSG.errMsg4logWork(message));
        window.et.log('quickWorkLogFailed', {target: target, duration: duration, message: message, key: key, location: location.href});
      }
    })
  }

  // ### 快捷设置issue指定单个字段值的方法
  window.quickFieldEdit = function(field, value, refresh) {
    window.et.log('quickFieldEdit', {field: field, value: value});
    if(!field) JIRA.Messages.showErrorMsg('未提供字段名');
    if(!value) JIRA.Messages.showErrorMsg('未提供字段值');

    const user = AJS.params.loggedInUser;
    const issueId = window.findIssueId();
    const atltoken = document.querySelector('meta[name="atlassian-token"]').attributes.content.value;

    var data = {
      issueId: issueId,
      atl_token: atltoken,
      singleFieldEdit: true,
      fieldsToForcePresent: field
    }
    data[field] = value;

    AJS.$.post({
      url:'https://work.fineres.com/secure/AjaxIssueAction.jspa',
      header: {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
      data: data,
      success: function(res){
        let label = field;
        if(res && res.fields) res.fields.filter(f=> f.id === field).forEach(f => label = f.label);
        JIRA.Messages.showSuccessMsg(TEXTMSG.successMsg4FieldEdit(label, value));
        if(refresh) setTimeout(function(){window.location.reload()}, Number.isInteger(refresh) ? refresh * 1000 : 2000);
      },
      fail: function(){
        JIRA.Messages.showErrorMsg(TEXTMSG.errMsg4FieldEdit(field, value));
        window.et.log('quickFieldEditFailed', {field: field, value: value});
      }
    });
    return ;
  }

  // ### 获取issue信息的方法
  window.getIssueFields = function(key, fields, successCallback) {
    if(!fields) fields = 'status,customfield_14301,issuelinks';
    return AJS.$.get({
      url: 'https://work.fineres.com/rest/api/2/issue/' + key,
      data: {fields: fields},
      success: function(res){
        if(typeof(successCallback) == 'function') successCallback(res);
        return res;
      },
      fail: function(res){
        JIRA.Messages.showErrorMsg(TEXTMSG.errMsg4IssueInfo);
        window.et.log('getIssueFieldsFailed', {key: key, fields: fields, location: location.href});
      }
    })
  }

  // ### 查找当前页面的issuekey
  window.findIssuekey = function() {
    const elMetaIssueKey = document.querySelector('meta[name="ajs-issue-key"]');
    const elDockLink = document.querySelector('#worklog-issue-dock-link');
    const elTitle = document.querySelector('a#key-val');
    const issuekey = elTitle ? elTitle.innerText : (elDockLink ? elDockLink.textContent : (elMetaIssueKey ? elMetaIssueKey.attributes.content.value : ''));
    if(!issuekey) JIRA.Messages.showErrorMsg(TEXTMSG.errMsg4FindIssueInfo);
    return issuekey;
  }

  // ### 查找当前页面的issueId
  window.findIssueId = function() {
    const elNavLink = document.querySelector('div.issue-header-content a#key-val.issue-link');
    const issueId = elNavLink ? elNavLink.attributes.rel.value : '';
    if(!issueId) JIRA.Messages.showErrorMsg(TEXTMSG.errMsg4FindIssueInfo);
    return issueId;
  }

  // ----------------------------------------------------------------------------------------------------
  // ## 一些适用于issue页面的组件用的小方法
  // ### 处理日志记录的state字段的方法
  window.stateClicked = function (event){
    window.et.log('statusClicked');
    const state = (event.target.querySelector('span') || event.target.closest('span')).innerText;
    window.prependStateContent(state);
    document.querySelectorAll('div.log-work-status-list-item.state-item').forEach(li => li.classList.remove('active'));
    (event.target.querySelector('div.log-work-status-list-item') || event.target.closest('div.log-work-status-list-item')).classList.add('active');
  }
  window.obstacleClicked = function (event){
    window.et.log('obstacleClicked');
    const el = event.target.querySelector('div.log-work-status-list-item') || event.target.closest('div.log-work-status-list-item');
    const isActivated = el.classList.contains('active');
    const obstacle = el.attributes.obstacle.value;
    if(isActivated) {
      el.classList.remove('active')
    } else {
      el.classList.add('active');
      if(GM_config.get('WORKLOGOBSTACLEHINTS_show') && WORKLOGOBSTACLEHINTS[obstacle] ) AJS.messages.hint("#wp-fg-status", { title: `"${obstacle}" 参考方案`, body: `${WORKLOGOBSTACLEHINTS[obstacle]}` });
    }
    const activeObstacles = document.querySelectorAll('div.log-work-status-list-item.obstacle-item.active');
    window.prependObstacleContent([...activeObstacles].map(obs => obs.innerText));
  }
  window.prependStateContent = function(state) {
    const commentElement = document.querySelector('div.field-group#wp-fg-comment textarea#comment');
    const newComment = `${TEMPLATES.status(state)}\n${window.purgeStateText(commentElement.value)[0]}`;
    commentElement.value = newComment;
    commentElement.dispatchEvent(new Event('input'));
  }
  window.prependObstacleContent = function(obstacles) {
    const commentElement = document.querySelector('div.field-group#wp-fg-comment textarea#comment');
    const statePurged = window.purgeStateText(commentElement.value);
    const obstaclePurged = window.purgeObstacleText(statePurged[0]);
    const obstaclesText = TEMPLATES.obstacle(obstacles);
    const newComment = [...statePurged[1], obstaclesText, obstaclePurged[0]].join('\n');
    commentElement.value = newComment;
    commentElement.dispatchEvent(new Event('input'));
  }
  window.purgeStateText = function(text) {
    const matched = (text.match(TEMPLATES.statusRegex) || []).filter(m => {
      const s = m.match(TEMPLATES.statusRegexMatch);
      return s && s[1] && WORKLOGSTATES.filter(st => st == s[1]).length > 0;
    });
    matched.forEach(m => (text = text.replace(m, '')) == '');
    return [text.trim(), matched];
  }
  window.purgeObstacleText = function(text) {
    let matched = (text.match(TEMPLATES.obstacleRegex) || [])
    matched.forEach(m => (text = text.replace(m, '')) == '');
    return [text.trim(), matched];
  }
  window.translatePage = function(lang) {
    if(!GM_config.get("localization")) return;
    lang = lang || navigator.language;
    LOCALIZATION.filter(lcl => lcl.lang.includes(lang))
      .forEach(lcl => {
      lcl.trans.forEach(trs => {
        document.querySelectorAll(trs.selector).forEach(el => {
          typeof(trs.callback) == 'function' ? trs.callback(el, trs.from, trs.to) : (el.innerText = trs.from ? el.innerText.replace(trs.from, trs.to) : trs.to);
        })
      })
    })
  }
  window.determineSubmitButton = function() {
    const textarea = document.querySelector('form#log-work-lean div#wp-fg-comment textarea');
    const btn = document.querySelector('form#log-work-lean div.form-footer input#worklogpro-log-work-submit');
    textarea.addEventListener('input', event => {
      const match = window.purgeStateText(event.target.value);
      if(match[1][0]) {
        textarea.setAttribute('hasStatus','');
        btn.removeAttribute('disabled');
        btn.value = '确认记录工时';
      } else {
        textarea.removeAttribute('hasStatus');
        btn.setAttribute('disabled','');
        btn.value = '选择问题状态后才能提交';
      }
    });
    textarea.addEventListener('keydown', window.preventKeyboardSubmit);
    setTimeout(function(){textarea.dispatchEvent(new Event('input'))}, 100);
  }
  window.preventKeyboardSubmit = function(event) {
    if(event.target.hasAttribute('hasStatus')) return;
    if(!event.ctrlKey || 13 !== event.keyCode) return;
    event.preventDefault();
    event.stopPropagation();
    return false;
  }
  window.hideUnnecessaryItem = function(issuekey) {
    return window.getIssueFields(issuekey, '', function(res) {
      if(!res || !res.fields) return;
      const info = {
        key: res.key,
        project: res.key.split('-')[0],
        status: res.fields.status.name,
        links: res.fields.issuelinks.map(linking => {
          const link = linking.inwardIssue ? linking.inwardIssue : linking.outwardIssue ? linking.outwardIssue : {key:'ERROR', status:{name:'ERROR'}};
          return {
            key: link.key,
            project: link.key.split('-')[0],
            status: link.fields.status.name,
          }}),
      };
      //获取所有的隐藏属性标签, 在下面进行排除, 剩下的标签都会隐藏掉;
      const attributes = [...new Set([].concat(...Object.values(HIDEITEMRULES)))];
      if(info.links.filter(link => ISSUEINFODICT.PROJECT['三线'].includes(link.project)).filter(link => ISSUEINFODICT.STATUS['三线待发布'].includes(link.status)).length) attributes.splice(attributes.indexOf('_待发布_'), 1) && document.querySelectorAll(`div[_待发布_]`).forEach(el => (el.classList.add('_unhide_')));
      if(info.links.filter(link => ISSUEINFODICT.PROJECT['三线'].includes(link.project)).filter(link => ISSUEINFODICT.STATUS['三线处理中'].includes(link.status)).length) attributes.splice(attributes.indexOf('_研发处理中_'), 1) && document.querySelectorAll(`div[_研发处理中_]`).forEach(el => (el.classList.add('_unhide_')));
      if(info.links.filter(link => ISSUEINFODICT.PROJECT['二开'].includes(link.project)).length) attributes.splice(attributes.indexOf('_二开中_'), 1) && document.querySelectorAll(`div[_二开中_]`).forEach(el => (el.classList.add('_unhide_')));
      if(ISSUEINFODICT.STATUS['SLA已确诊'].includes(info.status)) attributes.splice(attributes.indexOf('_SLA已确诊_'), 1) && document.querySelectorAll(`div[_SLA已确诊_]`).forEach(el => (el.classList.add('_unhide_')));
      if(ISSUEINFODICT.STATUS['SLA待确诊'].includes(info.status)) attributes.splice(attributes.indexOf('_SLA待确诊_'), 1) && document.querySelectorAll(`div[_SLA待确诊_]`).forEach(el => (el.classList.add('_unhide_')));
      attributes.forEach(attr => (document.querySelectorAll(`div[${attr}]:not(._unhide_)`).forEach(el => (el.style.display = 'none')) == '') == '');
    })
  }
  window.quickWorkLogClickHandler = function(event) {
    const location = event.target.attributes['data-location'].value;
    const logName = event.target.attributes['data-log'].value;
    const issuekey = event.target.attributes['data-issuekey'].value;
    const log = WORKLOGS.filterByName(logName);
    if(!log.name) return JIRA.Messages.showErrorMsg(TEXTMSG.errMsg4logWork(' 未能获取到对应日志记录, '));
    const content = WORKLOGS.content(log);
    return window.quickWorkLog(log.duration, content, issuekey, location);
  }
  window.zhinengtuijianclicked = function(event){
    const data = {
      user: AJS.params.loggedInUser,
      title: event.target.innerText,
      href: event.target.href,
      issuekey: document.querySelector('#issue-content #key-val').innerText,
      issuestatus: document.querySelector('#issue-content #status-val').innerText,
    }
    window.et.log('zhinengtuijian', data);
  }

  window.parseDescriptionContent = function(text) {
    function parseSentence(s){
      const matchN = TEMPLATES.descPrefixes.find(reg => s.match(reg));
      if(!matchN) return false;
      return s.replace(matchN, '');
    }
    return text.split('\n').map(e => e.trim()).filter(parseSentence).map(parseSentence).join('\n');
  }
  window.parseSolutionContent = function(text) {
    const res = text.match(TEMPLATES.solutionRegex);
    if(!res || res.length < 3) return '';
    return TEXTMSG.template4OuterSolution('', res[2]);
  }

  window.validateOuterSolution = function() {
    const textarea = document.querySelector('textarea#customfield_16005[name="customfield_16005"]');
    textarea.addEventListener('input', event => {
      const match = event.target.value.match(TEMPLATES.outerSolutionInvalidRegex);
      if(match) {
        textarea.parentElement.setAttribute('invalid','');
      } else {
        textarea.parentElement.removeAttribute('invalid');
      }
    });
    textarea.addEventListener('keydown', window.preventKeyboardSubmit);
    setTimeout(function(){textarea.dispatchEvent(new Event('input'))}, 1000);
  }

  window.handleNLPstuff = function(){
    window.getIssueFields(window.findIssuekey(), 'summary,description', res => {
      const text = res && res.fields && `${res.fields.summary}\n${res.fields.description}` || '';
      navigator.clipboard.writeText(text);
      setTimeout(function(){window.open(`https://tsjupyter.ysslang.com:60443/doc/tree/0prod/_issueAsiggnAPI.ipynb`)});
    });
  }

  window.createTESTask = function(){
    const issuekey = window.findIssuekey();
    window.getIssueFields(issuekey, 'summary,customfield_10518,customfield_16601', res => {
      setTimeout(function(){window.open(`https://tss.frts.y30.xyz:54443/webroot/decision/view/report?op=write&viewlet=JIRA-TES.cpt&jirakey=${issuekey}&summary=${res.fields.summary}&assignee=${res.fields.customfield_10518.name}&reporter=${res.fields.customfield_16601.name}`)});
    });
  }

  window.isModuleExpert = function(){
    const MEList = ['Albert.Lu', 'Dolores', 'caster', 'Chenxing.Yu', 'Daisy.Zhang', 'Johnny.Xue', 'kaimi', 'Kivia', 'Larry', 'LeoYuan', 'Leslie.Zhang', 'Phyli.Zhou', 'XuYeqiang','Alyssa']
    return MEList.includes(AJS.params.loggedInUser) ? true : false
  }

  window.handleModuleExpertButton = function(){
    const user = AJS.params.loggedInUser;
    quickFieldEdit('customfield_16601', user);
    window.getIssueFields(window.findIssuekey(), 'customfield_13202', res => {
      const expert = res && res.fields && res.fields.customfield_13202 && res.fields.customfield_13202.name || '';
      if(expert != user) quickFieldEdit('customfield_13202', user);
    });
  }


  // ----------------------------------------------------------------------------------------------------
  // ----------------------------------------------------------------------------------------------------
  // # 主代码部分
  // ### 初始化配置页面, 并在用户头像下面绑定配置按钮
  GM_config.init(CONFIG);
  window.waitForAddedNode({
    selector: '#header #user-options-content #personal',
    immediate: true,
    recursive: true,
    done: function(el) {
      el.insertAdjacentHTML('beforeend', HTMLCONTENT.configOptionLI);
      AJS.$('#config_options').on('mouseup', () => GM_config.open());
    }});

  // ### bug页侧边栏添加记录日志按钮
  window.waitForAddedNode({
    selector: '#add-worklog-issue-right-panel-link',
    immediate: true,
    recursive: true,
    done: function(el) {
      const issuekey = window.findIssuekey();
      el.closest('tbody').insertAdjacentHTML('beforeend', HTMLCONTENT.logWorkRightPanel(issuekey));
      AJS.$('.quick-work-log-tr .quick-work-log-btn').off('mouseup');
      AJS.$('.quick-work-log-tr .quick-work-log-btn').on('mouseup', window.quickWorkLogClickHandler);
    }});

  // ### 看板页下拉框添加记录日志按钮
  window.waitForAddedNode({
    selector: '.aui-list-item-link.issueaction-comment-issue',
    recursive: true,
    done: function(el) {
      const issuekey = el.attributes["data-issuekey"].value;
      el.closest('.aui-last').insertAdjacentHTML('beforebegin', HTMLCONTENT.logWorkDropdownPanel(issuekey));
      AJS.$('.quick-work-log-ul .quick-work-log-li .quick-work-log-item').off('mouseup');
      AJS.$('.quick-work-log-ul .quick-work-log-li .quick-work-log-item').on('mouseup', window.quickWorkLogClickHandler);
    }});

  // ### 关bug弹窗添加记录日志按钮
  window.waitForAddedNode({
    selector: '[for="customfield_10528"]',
    recursive: true,
    done: function(el) {
      const issuekey = el.closest('.jira-dialog-content').querySelector('.dialog-title').textContent.match(/SLA-\d{1,10}/)[0];
      el.closest('form').querySelector('.buttons-container.form-footer').insertAdjacentHTML('afterbegin', HTMLCONTENT.logWorkDialog(issuekey));
      AJS.$('.quick-work-log-dialog-div .quick-work-log-btn').off('mouseup');
      AJS.$('.quick-work-log-dialog-div .quick-work-log-btn').on('mouseup', window.quickWorkLogClickHandler);
    }});

  // ### 记录日志弹窗添加状态内容
  window.waitForAddedNode({
    selector: 'div.field-group#wp-fg-comment',
    recursive: true,
    done: function(el) {
      if(!el.querySelector('textarea#comment').attributes['data-issuekey'].value.includes('SLA-')) return;
      el.insertAdjacentHTML('beforebegin', HTMLCONTENT.fieldGroup(HTMLCONTENT.statePanelBox() + HTMLCONTENT.obstaclePanelBox()));
      AJS.$('.log-work-status-list-item.state-item').on('mouseup', window.stateClicked);
      AJS.$('.log-work-status-list-item.obstacle-item').on('mouseup', window.obstacleClicked);
      window.translatePage();
      window.determineSubmitButton();
      window.hideUnnecessaryItem(el.querySelector('textarea#comment').attributes['data-issuekey'].value);
    }});

  // ### 专家字段添加展示和快捷设置按钮
  window.waitForAddedNode({
    selector: 'div#peoplemodule div#peopledetails',
    immediate: true,
    recursive: true,
    done: function(el) {
      if(!window.findIssuekey().includes('SLA-')) return;
      if(!window.isModuleExpert()) return;
      window.getIssueFields(window.findIssuekey(), 'customfield_16601,customfield_13202', res => {
        const name = res.fields.customfield_16601 ? res.fields.customfield_16601.name : '无';
        const rname = res.fields.customfield_13202 ? res.fields.customfield_13202.name : '无';
        el.insertAdjacentHTML('beforeend', HTMLCONTENT.fieldModuleExpert(name, AJS.params.loggedInUser == name));
        el.insertAdjacentHTML('beforeend', HTMLCONTENT.fieldResponsibleModuleExpert(rname));
      });
    }})
  // ### 专家合理协助选择按钮
  window.waitForAddedNode({
    selector: 'div#customfield_19449-val',
    immediate: true,
    recursive: true,
    done: function(el) {
      if(!window.findIssuekey().includes('SLA-')) return;
      if(!window.isModuleExpert()) return;
      const MESupportTypeList = [[34755, "合理协助"], [34760, "紧急支撑"], [35751, "信息缺失"], [35752, "简单咨询"], [35753, "未按路径排查"], [35754, "未做基础排查"]]
      el.insertAdjacentHTML('afterend', HTMLCONTENT.MESupportType(MESupportTypeList));
    }})

  // ### 转专家协助预填充标识文本
  window.waitForAddedNode({
    selector: 'form#issue-workflow-transition',
    immediate: true,
    recursive: true,
    done: function(el) {
      if(!el.querySelector('input[name="action"][value="781"]')) return;
      setTimeout(function(){ el.querySelector('rich-editor')._setRawContent(`【[----专家协助信息---]】\n`) }, 1000);
    }});


  // ### 解决方案字段添加方案模板
  window.waitForAddedNode({
    selector: 'textarea#customfield_10528',
    immediate: true,
    recursive: true,
    done: function(el) {
      el.insertAdjacentHTML('afterend', HTMLCONTENT.solutionTip);
      if(el.value && el.value != '无') return;
      const issuekey = el.closest('form#issue-workflow-transition').querySelector('#comment-wiki-edit [data-issuekey]').attributes['data-issuekey'].value;
      el.value = TEXTMSG.template4InnerSolution();
    }});
  // ### 对外解决方案字段添加方案模板
  window.waitForAddedNode({
    selector: 'textarea#customfield_16005',
    immediate: true,
    recursive: true,
    done: function(el) {
      window.validateOuterSolution();
      el.placeholder = TEXTMSG.tipOnSolution4Customer;
      const innerSolution = el.closest('div.form-body').querySelector('textarea#customfield_10528').value;
      const outerSolution = window.parseSolutionContent(innerSolution)
      if(!el.value) el.value = outerSolution;
      el.insertAdjacentHTML('afterend', HTMLCONTENT.outerSolutionInvalidTip);
    }});



  // ### 公司ID添加打开客户名片按钮
  window.waitForAddedNode({
    selector: 'div#customfield_10504-val',
    immediate: true,
    recursive: true,
    done: function(el) { el.innerHTML = HTMLCONTENT.fieldCompanyInfo(el); }});
  // ### 公司名称添加crm查客户按钮
  window.waitForAddedNode({
    selector: 'div#customfield_10527-val',
    immediate: true,
    recursive: true,
    done: function(el) { el.innerHTML = HTMLCONTENT.fieldCompanyName(el); }});
  // ### 客户活动链接文本替换
  window.waitForAddedNode({
    selector: 'div#customfield_11706-val',
    immediate: true,
    recursive: true,
    done: function(el) { el.innerHTML = HTMLCONTENT.fieldCustomerAcitivities(el.querySelector('a')); }});
  // ### QQ号添加直接发起聊天按钮
  window.waitForAddedNode({
    selector: 'div#customfield_10522-val',
    immediate: true,
    recursive: true,
    done: function(el) { el.innerHTML = HTMLCONTENT.fieldChatWithQQ(el); }});
  // ### 专家是否协助添加反馈按钮
  window.waitForAddedNode({
    selector: 'div#customfield_17813-val',
    immediate: true,
    recursive: true,
    done: function(el) {
      el.innerHTML = HTMLCONTENT.ModuleExpertFeedback(el);
    }});

  // ### 文档智能推荐自动点开
  window.waitForAddedNode({
    selector: 'div#customfield_18201-val div.nfeed-field[data-field-id="customfield_18201"]',
    immediate: true,
    recursive: true,
    done: function(el) { if(el.closest('div#customfield_18201-val').innerText.includes('请点击描述以获取推荐文档')) el.click(); }});
  // ### 文档智能推荐自动点关
  window.waitForAddedNode({
    selector: 'div#customfield_18201-val div.inline-edit-fields a[name="zhinengtuijian"], div#customfield_18201-val div.inline-edit-fields span.no-value-container',
    immediate: true,
    recursive: true,
    done: function(el) { el.closest('div#customfield_18201-val').querySelector('div.save-options button.submit').click(); }});
  // ### 文档智能推荐添加埋点
  window.waitForAddedNode({
    selector: 'a[name="zhinengtuijian"]',
    immediate: true,
    recursive: true,
    done: function(el) {
      el.closest('a[name="zhinengtuijian"]') && el.closest('a[name="zhinengtuijian"]').addEventListener('mouseup', window.zhinengtuijianclicked);
      el.querySelectorAll('a[name="zhinengtuijian"]').forEach(a => a.addEventListener('mouseup', window.zhinengtuijianclicked));
    }});
  // ### FR专家推荐自动点开
  window.waitForAddedNode({
    selector: 'div#customfield_18406-val span.aui-iconfont-edit',
    immediate: true,
    recursive: true,
    done: function(el) { if(el.closest('div#customfield_18406-val').innerText.includes('该问题有专家的解决思路参考,请点我获取~')) el.click(); }});
  // ### FR专家推荐自动点关
  window.waitForAddedNode({
    selector: 'div#customfield_18406-val div.inline-edit-fields .read-only-component ',
    immediate: true,
    recursive: true,
    done: function(el) { el.closest('div#customfield_18406-val').querySelector('div.save-options button.submit').click(); }});
  // ### 添加敏感词反馈按钮
  window.waitForAddedNode({
    selector: '#opsbar-jira\\.issue\\.tools',
    immediate: true,
    recursive: true,
    done: function(el) { el.insertAdjacentHTML('beforeend', HTMLCONTENT.reportSensitiveWordsButton); }});
  // ### 修改确诊参考项样式
  window.waitForAddedNode({
    selector: 'label[for^="customfield_18405"]',
    immediate: true,
    recursive: true,
    done: function(el) { el.setAttribute('v', el.innerText); }});

  // ### 添加诊断书快捷按钮
  window.waitForAddedNode({
    selector: '#opsbar-jira\\.issue\\.tools',
    immediate: true,
    recursive: true,
    done: function(el) { el.insertAdjacentHTML('beforeend', HTMLCONTENT.diagnoseReportButton(window.findIssuekey())); }});

  // ### 添加风险评估书快捷按钮
  window.waitForAddedNode({
    selector: '#opsbar-jira\\.issue\\.tools',
    immediate: true,
    recursive: true,
    done: function(el) {
      window.getIssueFields(window.findIssuekey(), 'customfield_10523', res => {
        const mobile = res && res.fields && res.fields.customfield_10523 || '';
        el.insertAdjacentHTML('beforeend', HTMLCONTENT.custRiskEvaluateButton(mobile));
      });
    }});

  // ### 添加NLP快捷按钮
  window.waitForAddedNode({
    selector: '#opsbar-jira\\.issue\\.tools',
    immediate: true,
    recursive: true,
    done: function(el) {
      if(AJS.params.loggedInUser !== 'LeoYuan') return;
      el.insertAdjacentHTML('beforeend', HTMLCONTENT.custNLPButton());
    }});

  // ### 出现textarea的时候恢复最后值并绑定自动保存
  window.waitForAddedNode({
    selector: 'textarea',
    immediate: true,
    recursive: true,
    done: function(el) {
      //      window.restoreSavedValue(el);
      window.autoSave(el);
    }});
  // ### 清理很久之前的的自动暂存的内容
  window.cleanEarlySavedValue();
  window.et.__installedNotify();
  window.translatePage();

  // ### 页面注入css样式
  window.addCss ( `
/* ### 快捷日志log work部分 */
td.quick-work-log-td {
    padding: 0;
    text-align: left;
    display: flex;
    flex-wrap: wrap;
}

div.buttons-container.form-footer {
    text-align: left;
}

div.quick-work-log-dialog-div {
    display: inline;
}

tr.quick-work-log-subtitle-tr td {
    padding-bottom: 0;
}

tr.quick-work-log-subtitle-tr h6 {
    text-align: left;
}

div.quick-work-log-btn {
    font-size: 14px;
    border-radius: 3px;
    cursor: pointer;
    background-image: none;
    background-color: rgba(9,30,66,.08);
    border: 1px solid transparent;
    color: #344563;
    text-decoration: none;
    display: inline-block;
    padding: 4px 10px;
    margin: 5px;
    white-space: nowrap;
}

div.quick-work-log-btn:hover {
    background-color: rgba(9,30,66,.2)
}

a.aui-list-item-link.quick-work-log-item {
    color: #172b4d;
}

a.aui-list-item-link.quick-work-log-item:hover {
    color: #42526e;
    background-color: #007fde;
}



/* ### 日志状态记录部分 */
div#wp-fg-estimates.field-group.wp-remaining-estimate-group label {
    display: block !important;
}

div.log-work-status-pick-box {
    border: 2px solid #dfe1e6;
    border-radius: 3px;
}

div.log-work-status-panel {
    vertical-align: top;
}

div.state-panel {
    width: 200px;
    float: left;
}

div.obstacle-panel {
    width: 100%;
    flex-wrap: wrap;
}

div.list-header {
    text-align: center;
    padding: 5px 0;
    font-weight: bold;
    overflow-y: scroll;
    border-bottom: 2px solid lightgrey;
    pointer-events: none;
    user-select: none;
}

div.log-work-status-list.obstacle-ul {
  padding-left: 5px;
}

div.log-work-status-list-scroll {
    overflow-y: scroll;
    height: 160px;
}

div.log-work-status-pick-box:hover div.log-work-status-list-scroll {
    height: 160px !important;
}

div.log-work-status-list {
    padding: 0;
    list-style-type: none;
}

div.log-work-status-list-item {
    padding: 2px 3px;
}

div.log-work-status-list-item.state-item:nth-child(even) {
    background: #f0f0ff;
}

div.log-work-status-list-item.state-item.active,
div.log-work-status-list-item.obstacle-item.active {
    background: #007fde;
    color: white;
}

div.log-work-status-list-item.state-item:hover,
div.log-work-status-list-item.obstacle-item:hover {
    background: #007fde;
    color: white;
    font-weight: bold;
    cursor: pointer;
}

div.obstacle-panel div.log-work-status-list-item {
    display: inline-block;
    white-space: nowrap;
    border: 1px solid lightgrey;
    border-radius: 5px;
    padding: 1px;
    margin: 2px;
}

div.obstacle-panel div.log-work-status-list-item span {
    white-space: normal;
    word-wrap: break-word;
}

div.state-panel div.log-work-status-list-item.spacer-item {
    height: 1px;
}
div.state-panel div.log-work-status-list-item.spacer-item:nth-child(odd) {
    border-bottom: 2px dashed lightgrey;
}

div.obstacle-panel div.log-work-status-list-item.spacer-item {
    border: 0;
    width: 10px;
}

div#wp-fg-status div {
    scrollbar-width: thin;
    scrollbar-color: darkblue lightgrey;
}

div#wp-fg-status div::-webkit-scrollbar {
    width: 10px;
}

div#wp-fg-status div::-webkit-scrollbar-track {
    background: lightgrey;
}

div#wp-fg-status div::-webkit-scrollbar-thumb {
    background-color: darkblue;
    border-radius: 20px;
    border: 3px solid lightgrey;
}

span.list-header-tip {
    font-size: x-small;
}

input#worklogpro-log-work-submit[disabled] {
    color: red;
    font-weight: bold;
}


div#outerSolutionInvalidTip.description {
    font-size: 16px;
    font-weight: bold;
    color: red;
    background: lightyellow;
}

div div#outerSolutionInvalidTip.description {
    display: none;
}

div[invalid] div#outerSolutionInvalidTip.description {
    display: block;
}


a.added-field-button {
    box-sizing: border-box;
    border-radius: 3.01px;
    cursor: pointer;
    font-size: 12px;
    background-color: lightgray;
    border: 1px solid transparent;
    color: green;
    display: inline;
    margin: 0px 10px;
    padding: 2px 4px;
    white-space: nowrap;
}

a#reportSensitiveWords,
a#diagnoseReport,
a#custRiskEvaluate {
    background: green;
    color: lightyellow;
}

label[for^="customfield_18405"][v^="报错库-"] {
    color: darkblue !important;
}
label[for^="customfield_18405"][v^="智能推荐-"] {
    color: darkred !important;
}
label[for^="customfield_18405"] {
    color: darkgreen !important;
}


/* ### 自动暂存的提示 */
div.auto-saved-tip {
    color: grey;
    font-size: 9pt;
    background: lightblue;
    display: table-row;
}


/* ### 以下内容是对原有样式的美化 */
div#wp-fg-comment textarea#comment {
    max-width: 655px;
    height: 8rem;
}

div#WorklogByUserPanel a#add-worklog-issue-right-panel-link {
    background: lightblue;
    border-radius: 8px;
    padding: 12px 24px;
    margin-left: 23px;
    display: inline-block;
}
div#WorklogByUserPanel a#add-worklog-issue-right-panel-link:hover {
    background: lightsteelblue;
}

/* 其他 */
.jira-dialog-content .form-body::-webkit-scrollbar { width: 10px; }
.jira-dialog-content .form-body::-webkit-scrollbar-track { background: lightgrey; }
.jira-dialog-content .form-body::-webkit-scrollbar-thumb { background-color: darkblue; border-radius: 20px; border: 3px solid lightgrey; }


/*配置界面*/
#JIRA_USERSCRIPT_OPTIONS {
  width: 600px !important;
  height: unset !important;
  inset: 100px 100px auto auto !important;
  padding: 20px !important;
  box-shadow: 5px 5px 20px black !important;
 }
#JIRA_USERSCRIPT_OPTIONS_wrapper {}

#JIRA_USERSCRIPT_OPTIONS .field_label{
  position: unset !important;
}

.mesupporttype-button-list {display: inline;}
.mesupporttype-button-list > .added-field-button { margin: 0 5px;}


` );


})();