
(function () {
  const DEBUG = false // true
  const IS_NODE = typeof window == 'undefined'
  const IS_BROWSER = typeof window == 'object'

  if (IS_NODE) {  // 解决在 Node 环境下缺少相关变量/常量/函数导致报错
    try {
      eval(`
        var alert = function(msg) {console.log('alert: ' + msg)};
        // var console = {log: function(msg) {}};

        var vUrl = {value: 'http://localhost:8080/get'};
        var vUrlComment = {value: ''};
        var vTransfer = {value: '', disabled: false};
        var vType = {value: 'JSON'};
        var vSend = {value: '', disabled: false};

        var vInput = {value: ''};
        var vWarning = {value: ''};
        var vComment = {value: ''};
        var vHeader = {value: ''};
        var vRandom = {value: ''};
        var vScript = {value: ''};
        var vOutput = {value: ''};

        var vAccount = {value: ''};
        var vPassword = {value: ''};
        var vVerify = {value: ''};
        var vRemember = {checked: true}

        var vRequestMarkdown = {value: ''};
        var vMarkdown = {value: ''};
        var vPage = {value: '0'};
        var vCount = {value: '100'};
        var vSearch = {value: ''};
        var vTestCasePage = {value: '0'};
        var vTestCaseCount = {value: '100'};
        var vTestCaseSearch = {value: ''};
        var vRandomPage = {value: '0'};
        var vRandomCount = {value: '100'};
        var vRandomSearch = {value: ''};
        var vRandomDisable = {checked: false}
        var vRandomSubPage = {value: '0'};
        var vRandomSubCount = {value: '100'};
        var vRandomSubSearch = {value: ''};

        var Vue = require('./vue.min'); // 某些版本不兼容 require('vue');
        var StringUtil = require('../apijson/StringUtil');
        var CodeUtil = require('../apijson/CodeUtil');
        var FileUtil = require('../apijson/FileUtil');
        var JSONObject = require('../apijson/JSONObject');
        var JSONResponse = require('../apijson/JSONResponse');
        var JSONRequest = require('../apijson/JSONRequest');
        var localforage = require('./localforage.min');
        var clipboard = require('./clipboard.min');
        var jsonlint = require('./jsonlint');
        var JSON5 = require('json5');
        // var window = {};
        // var $ = require('./jquery').jQuery;
        // var $ = {
        //   isEmptyObject: function (obj) {
        //     return obj == null || Object.keys(obj).length <= 0;
        //   }
        // };

        // var LocalStorage = require('node-localstorage').LocalStorage;
        // var localStorage = new LocalStorage('./scratch');
        var localStorage = {
          getItem: function (key) {},
          setItem: function (key, value) {}
        }
        // var difflib = require('./difflib');
        // var diffview = require('./diffview');
        // var editor = require('./editor');
        // var FileSaver = require('./FileSaver');
        // var helper = require('./helper');
        // var jquery = require('./jquery');
        // var jsonlint = require('./jsonlint');
        // var parse = require('./parse');
        // var uuid = require('./uuid');

        var axios = require('axios');
        var editormd = null;
      `)
    } catch (e) {
      console.log(e)
    }
  }

  function log(msg) {
    if (DEBUG) {
      console.log(msg)
    }
  }

  function len(s) {
    return StringUtil.length(s)
  }


  Vue.component('vue-item', {
    props: ['jsondata', 'theme', 'thiz'],
    template: '#item-template'
  })

  Vue.component('vue-outer', {
    props: ['jsondata', 'isend', 'thiz', 'path', 'theme'],
    template: '#outer-template'
  })

  Vue.component('vue-expand', {
    props: [],
    template: '#expand-template'
  })

  Vue.component('vue-val', {
    props: ['field', 'val', 'isend', 'thiz', 'path', 'theme'],
    template: '#val-template'
  })

  Vue.use({
    install: function (Vue, options) {

      // 判断数据类型
      Vue.prototype.getTyp = function (val) {
        return toString.call(val).split(']')[0].split(' ')[1]
      }

      // 判断是否是对象或者数组，以对下级进行渲染
      Vue.prototype.isObjectArr = function (val) {
        return ['Object', 'Array'].indexOf(this.getTyp(val)) > -1
      }

      // 折叠
      Vue.prototype.fold = function ($event) {
        var target = Vue.prototype.expandTarget($event)
        target.siblings('svg').show()
        target.hide().parent().siblings('.expand-view').hide()
        target.parent().siblings('.fold-view').show()
      }
      // 展开
      Vue.prototype.expand = function ($event) {
        var target = Vue.prototype.expandTarget($event)
        target.siblings('svg').show()
        target.hide().parent().siblings('.expand-view').show()
        target.parent().siblings('.fold-view').hide()
      }

      //获取展开折叠的target
      Vue.prototype.expandTarget = function ($event) {
        switch($event.target.tagName.toLowerCase()) {
          case 'use':
            return $($event.target).parent()
          case 'label':
            return $($event.target).closest('.fold-view').siblings('.expand-wraper').find('.icon-square-plus').first()
          default:
            return $($event.target)
        }
      }

      // 格式化值
      Vue.prototype.formatVal = function (val) {
        switch(Vue.prototype.getTyp(val)) {
          case 'String':
            return '"' + val + '"'
          case 'Null':
            return 'null'
          default:
            return val
        }
      }

      // 判断值是否是链接
      Vue.prototype.isaLink = function (val) {
        return /^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/.test(val)
      }

      // 计算对象的长度
      Vue.prototype.objLength = function (obj) {
        return Object.keys(obj).length
      }

      /**渲染 JSON key:value 项
       * @author TommyLemon
       * @param val
       * @param key
       * @return {boolean}
       */
      Vue.prototype.onRenderJSONItem = function (val, key, path) {
        if (isSingle || key == null) {
          return true
        }
        if (key == '_$_this_$_') {
          // return true
          return false
        }

        var method = App.getMethod();
        var isRestful = ! JSONObject.isAPIJSONPath(method);

        try {
          if (val instanceof Array) {
              // alert('onRenderJSONItem  key = ' + key + '; val = ' + JSON.stringify(val))

              var ckey = key == null ? null : key.substring(0, key.lastIndexOf('[]'));

              var aliaIndex = ckey == null ? -1 : ckey.indexOf(':');
              var objName = aliaIndex < 0 ? ckey : ckey.substring(0, aliaIndex);

              var firstIndex = objName == null ? -1 : objName.indexOf('-');
              var firstKey = firstIndex < 0 ? objName : objName.substring(0, firstIndex);

              for (var i = 0; i < val.length; i++) {
                var cPath = (StringUtil.isEmpty(path, false) ? '' : path + '/') + key;

                var vi = val[i]

                if (JSONResponse.isObject(vi) && JSONObject.isTableKey(firstKey || '', vi, isRestful)) {
                  // var newVal = parseJSON(JSON.stringify(val[i]))
                  if (vi == null) {
                    continue
                  }

                  var curPath = cPath + '/' + i;
                  var curTable = firstKey;
                  var thiz = {
                    _$_path_$_: curPath,
                    _$_table_$_: curTable
                  };

                  var newVal = {};
                  for (var k in vi) {
                    newVal[k] = vi[k]; //提升性能
                    if (App.isFullAssert) {
                        try {
                          var cri = App.currentRemoteItem || {};
                          var tr = cri.TestRecord || {};
                          var d = cri.Flow || {};
                          var standard = App.isMLEnabled ? tr.standard : tr.response;
                          var standardObj = StringUtil.isEmpty(standard, true) ? null : parseJSON(standard);
                          var tests = App.tests[String(App.currentAccountIndex)] || {};
                          var responseObj = (tests[d.id] || {})[0]

                          var pathUri = (StringUtil.isEmpty(curPath, false) ? '' : curPath + '/') + k;
                          var pathKeys = StringUtil.split(pathUri, '/');
                          var target = App.isMLEnabled ? JSONResponse.getStandardByPath(standardObj, pathKeys) : JSONResponse.getValByPath(standardObj, pathKeys);
                          var real = JSONResponse.getValByPath(responseObj, pathKeys);
                          var cmp = App.isMLEnabled ? JSONResponse.compareWithStandard(target, real, pathUri) : JSONResponse.compareWithBefore(target, real, pathUri);
//                          cmp.path = pathUri;
                          var cmpShowObj = JSONResponse.getCompareShowObj(cmp);
                          thiz[k] = [cmpShowObj.compareType, cmpShowObj.compareColor, cmpShowObj.compareMessage];
                          var countKey = '_$_' + cmpShowObj.compareColor + 'Count_$_';
                          thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                        } catch (e) {
                          thiz[k] = [JSONResponse.COMPARE_ERROR, 'red', e.message];
                          var countKey = '_$_redCount_$_';
                          thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                        }
                    }

                    delete vi[k]
                  }

                  vi._$_this_$_ = JSON.stringify(thiz)
                  for (var k in newVal) {
                    vi[k] = newVal[k]
                  }
                }
                else {
                  this.onRenderJSONItem(vi, '' + i, cPath);
                }

                // this.$children[i]._$_this_$_ = key
                // alert('this.$children[i]._$_this_$_ = ' + this.$children[i]._$_this_$_)
              }
          }
          else if (val instanceof Object) {
            var aliaIndex = key == null ? -1 : key.indexOf(':');
            var objName = aliaIndex < 0 ? key : key.substring(0, aliaIndex);

            // var newVal = parseJSON(JSON.stringify(val))

            var curPath = (StringUtil.isEmpty(path, false) ? '' : path + '/') + key;
            var curTable = JSONObject.isTableKey(objName, val, isRestful) ? objName : null;
            var thiz = {
              _$_path_$_: curPath,
              _$_table_$_: curTable
            };

            var newVal = {};
            for (var k in val) {
              newVal[k] = val[k]; //提升性能
              if (App.isFullAssert) {
                  try {
                    var cri = App.currentRemoteItem || {};
                    var tr = cri.TestRecord || {};
                    var d = cri.Flow || {};
                    var standard = App.isMLEnabled ? tr.standard : tr.response;
                    var standardObj = StringUtil.isEmpty(standard, true) ? null : parseJSON(standard);
                    var tests = App.tests[String(App.currentAccountIndex)] || {};
                    var responseObj = (tests[d.id] || {})[0]

                    var pathUri = (StringUtil.isEmpty(curPath, false) ? '' : curPath + '/') + k;
                    var pathKeys = StringUtil.split(pathUri, '/');
                    var target = App.isMLEnabled ? JSONResponse.getStandardByPath(standardObj, pathKeys) : JSONResponse.getValByPath(standardObj, pathKeys);
                    var real = JSONResponse.getValByPath(responseObj, pathKeys);
            //              c = JSONResponse.compareWithBefore(target, real, path);
                    var cmp = App.isMLEnabled ? JSONResponse.compareWithStandard(target, real, pathUri) : JSONResponse.compareWithBefore(target, real, pathUri);
//                    cmp.path = pathUri;
                    var cmpShowObj = JSONResponse.getCompareShowObj(cmp);
                    thiz[k] = [cmpShowObj.compareType, cmpShowObj.compareColor, cmpShowObj.compareMessage];
                    var countKey = '_$_' + cmpShowObj.compareColor + 'Count_$_';
                    thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                  } catch (e) {
                    thiz[k] = [JSONResponse.COMPARE_ERROR, 'red', e.message];
                    var countKey = '_$_redCount_$_';
                    thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                  }
              }

              delete val[k];
            }

            val._$_this_$_ = JSON.stringify(thiz)
            for (var k in newVal) {
              val[k] = newVal[k];
            }

            // val = Object.assign({ _$_this_$_: objName }, val) //解决多显示一个逗号 ,

            // this._$_this_$_ = key  TODO  不影响 JSON 的方式，直接在组件读写属性
            // alert('this._$_this_$_ = ' + this._$_this_$_)
          }

        } catch (e) {
          if (DEBUG) {
            alert('onRenderJSONItem  try { ... } catch (e) {\n' + e.message)
          } else {
            console.log(e)
          }
        }

        return true

      }


      /**显示 Response JSON 的注释
       * @author TommyLemon
       * @param val
       * @param key
       * @param $event
       */
      Vue.prototype.setResponseHint = function (val, key, $event, isAssert, color) {
        console.log('setResponseHint')
        this.$refs.responseKey.setAttribute('data-hint', isSingle ? '' : this.getResponseHint(val, key, $event, isAssert, color));
      }
      /**获取 Response JSON 的注释
       * 方案一：
       * 拿到父组件的 key，逐层向下传递
       * 问题：拿不到爷爷组件 "Comment[]": [ { "id": 1, "content": "content1" }, { "id": 2 }... ]
       *
       * 方案二：
       * 改写 jsonon 的 refKey 为 key0/key1/.../refKey
       * 问题：遍历，改 key；容易和特殊情况下返回的同样格式的字段冲突
       *
       * 方案三：
       * 改写 jsonon 的结构，val 里加 .path 或 $.path 之类的隐藏字段
       * 问题：遍历，改 key；容易和特殊情况下返回的同样格式的字段冲突
       *
       * @author TommyLemon
       * @param val
       * @param key
       * @param $event
       */
      Vue.prototype.getResponseHint = function (val, key, $event, isAssert, color) {
        // alert('setResponseHint  key = ' + key + '; val = ' + JSON.stringify(val))

        var s = ''

        try {
          var standardObj = null;
          try {
            var currentItem = App.isTestCaseShow ? App.remotes[App.currentDocIndex] : App.currentRemoteItem;
            standardObj = parseJSON(((currentItem || {}).TestRecord || {}).standard);
          } catch (e3) {
            log(e3)
          }

          var path = null
          var table = null
          var column = null

          var method = App.isTestCaseShow ? ((App.currentRemoteItem || {}).Flow || {}).url : App.getMethod();
          var isRestful = ! JSONObject.isAPIJSONPath(method);

          if (val instanceof Object && (val instanceof Array == false)) {

            var parent = $event.currentTarget.parentElement.parentElement
            var valString = parent.textContent

            // alert('valString = ' + valString)

            var i = valString.indexOf('"_$_this_$_":')
            if (i >= 0) {
              valString = valString.substring(i + '"_$_this_$_":'.length)
              i = valString.indexOf('}"')
              var i2 = valString.indexOf('"{')
              if (i >= 0 && i2 >= 0 && i2 < i) {
                valString = valString.substring(i2 + 1, i + 1)
                // alert('valString = ' + valString)
                var _$_this_$_ = parseJSON(valString) || {}
                path = _$_this_$_._$_path_$_
                table = _$_this_$_._$_table_$_
              }

              var aliaIndex = key == null ? -1 : key.indexOf(':');
              var objName = aliaIndex < 0 ? key : key.substring(0, aliaIndex);

              if (JSONObject.isTableKey(objName, val, isRestful)) {
                table = objName
              }
              else if (JSONObject.isTableKey(table, val, isRestful)) {
                column = key
              }

              // alert('path = ' + path + '; table = ' + table + '; column = ' + column)
            }
          }
          else {
            var parent = $event.currentTarget.parentElement.parentElement
            var valString = parent.textContent

            // alert('valString = ' + valString)

            var i = valString.indexOf('"_$_this_$_":')
            if (i >= 0) {
              valString = valString.substring(i + '"_$_this_$_":'.length)
              i = valString.indexOf('}"')
              var i2 = valString.indexOf('"{')
              if (i >= 0 && i2 >= 0 && i2 < i) {
                valString = valString.substring(i2 + 1, i + 1)
                // alert('valString = ' + valString)
                var _$_this_$_ = parseJSON(valString) || {}
                path = _$_this_$_ == null ? '' : _$_this_$_._$_path_$_
                table = _$_this_$_ == null ? '' : _$_this_$_._$_table_$_
              }
            }

            if (val instanceof Array && JSONObject.isArrayKey(key, val, isRestful)) {
              var key2 = key == null ? null : key.substring(0, key.lastIndexOf('[]'));

              var aliaIndex = key2 == null ? -1 : key2.indexOf(':');
              var objName = aliaIndex < 0 ? key2 : key2.substring(0, aliaIndex);

              var firstIndex = objName == null ? -1 : objName.indexOf('-');
              var firstKey = firstIndex < 0 ? objName : objName.substring(0, firstIndex);

              // alert('key = ' + key + '; firstKey = ' + firstKey + '; firstIndex = ' + firstIndex)
              if (JSONObject.isTableKey(firstKey || '', null, isRestful)) {
                table = firstKey

                var s0 = '';
                if (firstIndex > 0) {
                  objName = objName.substring(firstIndex + 1);
                  firstIndex = objName.indexOf('-');
                  column = firstIndex < 0 ? objName : objName.substring(0, firstIndex)

                  var pathUri = (StringUtil.isEmpty(path) ? '' : path + '/') + key;

                  var c = CodeUtil.getCommentFromDoc(docObj == null ? null : docObj['[]'], table, column, method, App.database, App.language, true, false, pathUri.split('/'), isRestful, val, true, standardObj); // this.getResponseHint({}, table, $event
                  s0 = column + (StringUtil.isEmpty(c, true) ? '' : ': ' + c)
                }

                var pathUri = (StringUtil.isEmpty(path) ? '' : path + '/') + (StringUtil.isEmpty(column) ? key : column);
                var c;
                if (isAssert) {
                    try {
                      var tr = App.currentRemoteItem.TestRecord || {};
                      var d = App.currentRemoteItem.Flow || {};
                      var standard = App.isMLEnabled ? tr.standard : tr.response;
                      var standardObj = StringUtil.isEmpty(standard, true) ? null : parseJSON(standard);
                      var tests = App.tests[String(App.currentAccountIndex)] || {};
                      var responseObj = (tests[d.id] || {})[0]

                      var pathKeys = StringUtil.split(pathUri, '/');
                      var target = App.isMLEnabled ? JSONResponse.getStandardByPath(standardObj, pathKeys) : JSONResponse.getValByPath(standardObj, pathKeys);
                      var real = JSONResponse.getValByPath(responseObj, pathKeys);
        //              c = JSONResponse.compareWithBefore(target, real, path);
                      var cmp = App.isMLEnabled ? JSONResponse.compareWithStandard(target, real, path) : JSONResponse.compareWithBefore(target, real, path);
//                      cmp.path = pathUri;
                      return JSONResponse.getCompareShowObj(cmp);
                    } catch (e) {
                      s += '\n' + e.message
                    }
                } else {
                    c = CodeUtil.getCommentFromDoc(docObj == null ? null : docObj['[]'], table, isRestful ? key : null, method, App.database, App.language, true, false, pathUri.split('/'), isRestful, val, true, standardObj);
                }

                s = (StringUtil.isEmpty(path) ? '' : path + '/') + key + ' 中 '
                  + (
                    StringUtil.isEmpty(c, true) ? '' : table + ': '
                      + c + ((StringUtil.isEmpty(s0, true) ? '' : '  -  ' + s0) )
                  );

                return s;
              }
              //导致 key[] 的 hint 显示为  key[]key[]   else {
              //   s = (StringUtil.isEmpty(path) ? '' : path + '/') + key
              // }
            }
            else {
              if (isRestful || JSONObject.isTableKey(table)) {
                column = key
              }
              // alert('path = ' + path + '; table = ' + table + '; column = ' + column)
            }
          }
          // alert('setResponseHint  table = ' + table + '; column = ' + column)

          var pathUri = (StringUtil.isEmpty(path) ? '' : path + '/') + key;
          var c;
          if (isAssert) {
            try {
              var tr = App.currentRemoteItem.TestRecord || {};
              var d = App.currentRemoteItem.Flow || {};
              var standard = App.isMLEnabled ? tr.standard : tr.response;
              var standardObj = StringUtil.isEmpty(standard, true) ? null : parseJSON(standard);
              var tests = App.tests[String(App.currentAccountIndex)] || {};
              var responseObj = (tests[d.id] || {})[0]

              var pathKeys = StringUtil.split(pathUri, '/');
              var target = App.isMLEnabled ? JSONResponse.getStandardByPath(standardObj, pathKeys) : JSONResponse.getValByPath(standardObj, pathKeys);
              var real = JSONResponse.getValByPath(responseObj, pathKeys);
//              c = JSONResponse.compareWithBefore(target, real, path);
              var cmp = App.isMLEnabled ? JSONResponse.compareWithStandard(target, real, path) : JSONResponse.compareWithBefore(target, real, path);
//              cmp.path = pathUri;
              return JSONResponse.getCompareShowObj(cmp);
            } catch (e) {
              s += '\n' + e.message
            }
          } else {
            c = CodeUtil.getCommentFromDoc(docObj == null ? null : docObj['[]'], table, isRestful ? key : column, method, App.database, App.language, true, false, pathUri.split('/'), isRestful, val, true, standardObj);
          }

          s += pathUri + (StringUtil.isEmpty(c, true) ? '' : ': ' + c)
        }
        catch (e) {
          s += '\n' + e.message
        }

        return s;
      },

      /**显示 Response JSON 的注释
       * @author TommyLemon
       * @param val
       * @param key
       * @param $event
       */
      Vue.prototype.setResponseCounts = function (val, key, $event, isAssert) {
        console.log('setResponseCounts')
        this.$refs.responseGreenAssert.setAttribute('data-content', isSingle ? '' : this.getResponseGreenCount(val, key, $event));
        this.$refs.responseBlueAssert.setAttribute('data-content', isSingle ? '' : this.getResponseBlueCount(val, key, $event));
        this.$refs.responseOrangeAssert.setAttribute('data-content', isSingle ? '' : this.getResponseOrangeCount(val, key, $event));
        this.$refs.responseRedAssert.setAttribute('data-content', isSingle ? '' : this.getResponseRedCount(val, key, $event));
      },
      /**获取 Response 断言失败 的数量
       * @author TommyLemon
       * @param val
       * @param key
       * @param $event
       */
      Vue.prototype.setResponseAllCount = function (val, key, $event) {
        return this.setResponseHint(val, key, $event, true, 'all')
      },
      /**获取 Response 断言失败 的数量
       * @author TommyLemon
       * @param val
       * @param key
       * @param $event
       */
      Vue.prototype.getResponseAllCount = function (val, key, $event) {
        return this.getResponseGreenCount(val, key, $event) + this.getResponseBlueCount(val, key, $event) + this.getResponseOrangeCount(val, key, $event) + this.getResponseRedCount(val, key, $event);
      },
      Vue.prototype.setResponseColorCount = function (val, key, $event) {
        console.log('setResponseColorCount')
        var assert = isSingle ? null : this.getResponseHint(val, key, $event, true, 'all')
        var responseAssert = $event.target; // this.$refs.responseAssert
//        responseAssert.innerText = (StringUtil.trim(assert.count) || '1');
//        $(responseAssert).attr('data-hint', assert == null ? '' : StringUtil.trim(assert.hintMessage));
//        responseAssert.setAttribute('data-content', assert == null ? '' : (StringUtil.trim(assert.count) || '1'));
        responseAssert.setAttribute('data-hint', assert == null ? '' : StringUtil.trim(assert.compareMessage));
//        $(responseAssert).attr('background', assert == null ? '' : assert.compareColor);
//        $(responseAssert).attr('visibility', assert == null || assert.code == 0 ? 'hidden' : 'show');
      },
      Vue.prototype.setResponseCount = function (val, key, $event, color) {
        console.log('setResponseCount')
//        var count = isSingle ? 0 : this.getResponseCount(val, key, $event, color)
        var assert = isSingle ? null : this.getResponseHint(val, key, $event, true, 'all')
        var responseAssert = $event.target; // this.$refs['response' + StringUtil.firstCase(color, true) + 'Assert']
//        responseAssert.innerText = (StringUtil.trim(assert.count) || '1');
        $(responseAssert).attr('data-hint', assert == null ? '' : StringUtil.trim(assert.compareMessage));
//        responseAssert.setAttribute('data-content', assert == null ? '' : (StringUtil.trim(assert.count) || '1'));
//        responseAssert.setAttribute('data-hint', assert == null ? '' : StringUtil.trim(assert.hintMessage));
//        $(responseAssert).attr('background', assert == null ? '' : assert.compareColor);
//        $(responseAssert).attr('visibility', assert == null || assert.code == 0 ? 'hidden' : 'show');
      },
      Vue.prototype.getResponseCount = function (val, key, $event, color) {
        return this.getResponseHint(val, key, $event, true, color)
      },
      Vue.prototype.setResponseGreenCount = function (val, key, $event, isAssert) {
        this.setResponseCount(val, key, $event, 'green')
      },
      Vue.prototype.getResponseGreenCount = function (val, key, $event) {
        return this.getResponseHint(val, key, $event, true, 'green')
      },
      Vue.prototype.setResponseBlueCount = function (val, key, $event, isAssert) {
        this.setResponseCount(val, key, $event, 'blue')
      },
      Vue.prototype.getResponseBlueCount = function (val, key, $event) {
        return this.getResponseHint(val, key, $event, true, 'blue')
      },
      Vue.prototype.setResponseOrangeCount = function (val, key, $event, isAssert) {
        this.setResponseCount(val, key, $event, 'orange')
      },
      Vue.prototype.getResponseOrangeCount = function (val, key, $event) {
        return this.getResponseHint(val, key, $event, true, 'orange')
      },
      Vue.prototype.setResponseRedCount = function (val, key, $event, isAssert) {
        this.setResponseCount(val, key, $event, 'red')
      },
      Vue.prototype.getResponseRedCount = function (val, key, $event) {
        return this.getResponseHint(val, key, $event, true, 'red')
      }
    }
  })


  var initJson = {}

// 主题 [key, String, Number, Boolean, Null, link-link, link-hover]
  var themes = [
    ['#92278f', '#3ab54a', '#25aae2', '#f3934e', '#f34e5c', '#717171'],
    ['rgb(19, 158, 170)', '#cf9f19', '#ec4040', '#7cc500', 'rgb(211, 118, 126)', 'rgb(15, 189, 170)'],
    ['#886', '#25aae2', '#e60fc2', '#f43041', 'rgb(180, 83, 244)', 'rgb(148, 164, 13)'],
    ['rgb(97, 97, 102)', '#cf4c74', '#20a0d5', '#cd1bc4', '#c1b8b9', 'rgb(25, 8, 174)']
  ]




// APIJSON <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  var ERR_MSG = `出现了一些问题，可以按照以下步骤解决：
1.检查网络连接是否畅通，可用浏览器打开右侧地址： https://www.baidu.com/s?wd=%22APIJSON%22
2.检查 URL 是否为一个可用的 域名/IPV4 地址，可用浏览器打开试试：正常返回结果 或 非 GET 请求返回 Whitelabel Error Page，一般都没问题
3.开启或关闭 右上方 设置>托管服务器代理，然后再试：如果开启后才通应该是 CORS 跨域问题；关闭后才通应该是用外网服务代理来访问内网导致，可退出登录后修改退关服务器地址为内网的 APIJSON 代理服务地址
4.Disable 关闭 VPN 等 电脑/手机/平板 上的网络代理软件 App 客户端，或者切换代理服务器地址，然后再试
5.按 Fn+F12 或 右键网页>Inspect 检查 查看 Network 接口调用信息和 Console 控制台日志
6.查看请求目标服务器上的日志，优先找异常报错内容
7.改用 Postman 等其它 HTTP API 接口工具测试同一个接口
8.再试一次

# 问题与解答大全(可截屏后 New issue 上报问题等待解答)：
https://github.com/TommyLemon/APIAuto/issues

如果是请求 APIJSON 后端服务，则使用以下链接：
https://github.com/Tencent/APIJSON/issues

There may be something wrong, you can follow by the steps:
1. Check whether the network connection is available, you can open the address with a browser: https://www.google.com/search?q=%22APIJSON%22
2. Check whether the URL is an available domain name/IPV4 address, try opening it with a browser: if return the result normally or return a Whitelabel Error Page for a non-GET request, generally the URL is available
3. Turn it on or off at the top right, Settings>Server Proxy, and try again: If it is enabled, it should be a CORS cross-domain problem; and if it is turned off, it should be caused by using an external network service proxy to access the intranet, You can log out and modify the logout server address to the APIJSON proxy service address of the intranet
4. Disable the network proxy software App client on the computer/phone/tablet such as VPN, or switch the proxy server address, and then try again
5. Press Fn+F12 or right-click the webpage>Inspect to view the Network interface call information and Console console log
6. Check the log on the request target server, and give priority to find the abnormal error content
7. Use other HTTP API tools such as Postman to test the same interface
8. Try again

# Questions and answers (you can report the problem after taking a screenshot and wait for the answer):
https://github.com/TommyLemon/APIAuto/issues

If you are requesting an APIJSON backend service, use the following link:
https://github.com/Tencent/APIJSON/issues



`;


  function markdownToHTML(md, isRequest) {
    if (typeof editormd == 'undefined' || editormd == null) {
      return;
    }

    if (isRequest) {
      vRequestMarkdown.innerHTML = '';
    }
    else {
      vMarkdown.innerHTML = '';
    }
    editormd.markdownToHTML(isRequest ? 'vRequestMarkdown' : "vMarkdown", {
      markdown        : md ,//+ "\r\n" + $("#append-test").text(),
      //htmlDecode      : true,       // 开启 HTML 标签解析，为了安全性，默认不开启
      htmlDecode      : "style,script,iframe",  // you can filter tags decode
      //toc             : false,
      tocm            : true,    // Using [TOCM]
      //tocContainer    : "#custom-toc-container", // 自定义 ToC 容器层
      //gfm             : false,
      tocDropdown     : true,
      // markdownSourceCode : true, // 是否保留 Markdown 源码，即是否删除保存源码的 Textarea 标签
      taskList        : true,
      tex             : true,  // 默认不解析
      flowChart       : true,  // 默认不解析
      sequenceDiagram : true,  // 默认不解析
    });
  }

  var OPERATE_TYPE_RECORD = 'RECORD'
  var OPERATE_TYPE_REVIEW = 'REVIEW'
  var OPERATE_TYPE_REPLAY = 'REPLAY'
  var OPERATE_TYPE_HTTP = 'HTTP'

  var PLATFORM_POSTMAN = 'POSTMAN'
  var PLATFORM_SWAGGER = 'SWAGGER'
  var PLATFORM_YAPI = 'YAPI'
  var PLATFORM_RAP = 'RAP'


  var REQUEST_TYPE_PARAM = 'PARAM'  // GET ?a=1&b=c&key=value
  var REQUEST_TYPE_FORM = 'FORM'  // POST x-www-form-urlencoded
  var REQUEST_TYPE_DATA = 'DATA'  // POST form-data
  var REQUEST_TYPE_JSON = 'JSON'  // POST application/json
  var REQUEST_TYPE_GRPC = 'GRPC'  // POST application/json
  var HTTP_METHOD_GET = 'GET'  // GET ?a=1&b=c&key=value
  var HTTP_METHOD_POST = 'POST'  // POST application/json
  var HTTP_METHOD_PUT = 'PUT'  // PUT
  var HTTP_METHOD_PATCH = 'PATCH'  // PATCH
  var HTTP_METHOD_DELETE = 'DELETE'  // DELETE
  var HTTP_METHOD_HEAD = 'HEAD'  // HEAD
  var HTTP_METHOD_OPTIONS = 'OPTIONS'  // OPTIONS
  var HTTP_METHOD_TRACE = 'TRACE'  // TRACE
  var HTTP_METHODS = [HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_PUT, HTTP_METHOD_PATCH, HTTP_METHOD_DELETE, HTTP_METHOD_HEAD, HTTP_METHOD_OPTIONS, HTTP_METHOD_TRACE]
  var HTTP_POST_TYPES = [HTTP_METHOD_POST, REQUEST_TYPE_JSON, REQUEST_TYPE_FORM, REQUEST_TYPE_DATA, REQUEST_TYPE_GRPC]
  var HTTP_URL_ARG_TYPES = [HTTP_METHOD_GET, REQUEST_TYPE_PARAM, REQUEST_TYPE_FORM]
  var HTTP_JSON_TYPES = [HTTP_METHOD_POST, REQUEST_TYPE_JSON, REQUEST_TYPE_GRPC]
  var HTTP_FORM_TYPES = [REQUEST_TYPE_FORM, HTTP_METHOD_PUT, HTTP_METHOD_DELETE]
  var HTTP_DATA_TYPES = [REQUEST_TYPE_DATA, HTTP_METHOD_PUT, HTTP_METHOD_DELETE]
  var HTTP_FORM_DATA_TYPES = [REQUEST_TYPE_DATA, HTTP_METHOD_PUT, HTTP_METHOD_DELETE]
  var HTTP_CONTENT_TYPES = [REQUEST_TYPE_PARAM, REQUEST_TYPE_FORM, REQUEST_TYPE_DATA, REQUEST_TYPE_JSON, REQUEST_TYPE_GRPC]

  var CONTENT_TYPE_MAP = {
    // 'PARAM': 'text/plain',
    'FORM': 'application/x-www-form-urlencoded',
    'DATA': 'multipart/form-data',
    'JSON': 'application/json',
    'GRPC': 'application/json',
  }
  var CONTENT_VALUE_TYPE_MAP = {
    'text/plain': 'JSON',
    'application/x-www-form-urlencoded': 'FORM',
    'multipart/form-data': 'DATA',
    'application/json': 'JSON'
  }

  var IGNORE_HEADERS = ['status code', 'remote address', 'referrer policy', 'connection', 'content-length'
    , 'content-type', 'date', 'keep-alive', 'proxy-connection', 'set-cookie', 'vary', 'accept', 'cache-control', 'dnt'
    , 'host', 'origin', 'pragma', 'referer', 'user-agent']

  var PRE = 'PRE' // PRE()
  var CUR = 'CUR' // CUR()
  var NEXT = 'NEXT' // NEXT()

  var PRE_REQ = 'PRE_REQ' // PRE_REQ('[]/page')
  var PRE_ARG = 'PRE_ARG' // PRE_ARG('[]/page')
  var NEXT_ARG = 'NEXT_ARG' // NEXT_ARG('[]/page')
  var PRE_RES = 'PRE_RES' // PRE_RES('[]/0/User/id')
  var PRE_DATA = 'PRE_DATA' // PRE_DATA('[]/0/User/id')
  var CTX_GET = 'CTX_GET' // CTX_GET('key')
  var CUR_REQ = 'CUR_REQ' // CUR_REQ('User/id')
  var CUR_ARG = 'CUR_ARG' // CUR_REQ('User/id')
  var CUR_RES = 'CUR_RES' // CUR_RES('[]/0/User/id')
  var CUR_DATA = 'CUR_DATA' // CUR_DATA('[]/0/User/id')
  var CTX_PUT = 'CTX_PUT' // CTX_PUT('key', val)

  function get4Path(obj, path, defaultVal, msg, isDesc) {
    var val = path == null || path == '' ? obj : JSONResponse.getValByPath(obj, StringUtil.splitPath(path, false), true, isDesc)
    if (val == null && defaultVal === undefined) {
      throw new Error('找不到 ' + path + ' 对应在 obj 中的非 null 值！' + StringUtil.get(msg))
    }
    return val
  }

  function put4Path(obj, key, val, msg) {
    if (obj == null) {
      throw new Error('obj = null ！不能 put ' + key + ' ！' + StringUtil.get(msg))
    }
    obj[key] = val
  }

  var RANDOM_DB = 'RANDOM_DB'
  var RANDOM_IN = 'RANDOM_IN'
  var RANDOM_INT = 'RANDOM_INT'
  var RANDOM_NUM = 'RANDOM_NUM'
  var RANDOM_STR = 'RANDOM_STR'
  var RANDOM_BAD = 'RANDOM_BAD'
  var RANDOM_BAD_BOOL = 'RANDOM_BAD_BOOL'
  var RANDOM_BAD_NUM = 'RANDOM_BAD_NUM'
  var RANDOM_BAD_STR = 'RANDOM_BAD_STR'
  var RANDOM_BAD_IN = 'RANDOM_BAD_IN'
  var RANDOM_BAD_ARR = 'RANDOM_BAD_ARR'
  var RANDOM_BAD_OBJ = 'RANDOM_BAD_OBJ'

  var ORDER_DB = 'ORDER_DB'
  var ORDER_IN = 'ORDER_IN'
  var ORDER_INT = 'ORDER_INT'
  var ORDER_BAD = 'ORDER_BAD'
  var ORDER_BAD_BOOL = 'ORDER_BAD_BOOL'
  var ORDER_BAD_NUM = 'ORDER_BAD_NUM'
  var ORDER_BAD_STR = 'ORDER_BAD_STR'
  var ORDER_BAD_IN = 'ORDER_BAD_IN'
  var ORDER_BAD_ARR = 'ORDER_BAD_ARR'
  var ORDER_BAD_OBJ = 'ORDER_BAD_OBJ'

  var ORDER_MAP = {}

  var BAD_BOOLS = [null, undefined, false, true, -1, 0, 1, 2, 3.14, 'null', 'undefined', 'None', 'nil', 'false', 'true',
   '-1', '0', '1', '2', '3.14', '', ' ', '\\t', '\\r', '\\n', '\\a', '\\b', '\\v', '\\f', 'a', 'dY', [], {}, '[]', '{}']
  var BAD_NUMS = BAD_BOOLS.concat([
   -2049, -1025, -13, 13, 1025, 2049, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.MIN_SAFE_INTEGER,
   Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER - 1, Number.MAX_SAFE_INTEGER + 1,
   '-2049', '-1025', '-13', '13', '1025', '2049', 'Number.NaN', 'Number.POSITIVE_INFINITY', 'Number.NEGATIVE_INFINITY',
    'Number.MIN_SAFE_INTEGER', 'Number.MAX_SAFE_INTEGER', '' + Number.MIN_SAFE_INTEGER, '' + Number.MAX_SAFE_INTEGER
  ])
  var BAD_STRS = BAD_NUMS.concat([
     '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+', '[', ']', '{', '}', ';', ':',
     "'", '\\"', ',', '.', '<', '>', '/', '?', '\\t\\r\\n\\a\\b\\v\\f', '`~!@#$%^&*()-_=+[]{};:\'\\",.<>/?',
     'qwertyuiopasdfghjklzxcvbnm', 'MNBVCZLKJHGFDSAPOIUYTREWQ', 'ä½ å¥½', 'ÄãºÃ', '浣犲ソ', '�����',
     '鐢辨湀瑕佸ソ濂藉涔犲ぉ澶╁悜涓?', '����Ҫ�¨²�ѧϰ������', 'ç”±æœˆè¦�å¥½å¥½å­¦ä¹ å¤©å¤©å�‘ä¸Š', 'ÓÉÔÂÒªºÃºÃÑ§Ï°ÌìÌìÏòÉÏ',
     '由月要好好学习天天向??', '锟斤拷锟斤拷要锟矫猴拷学习锟斤拷锟斤拷锟斤拷'
  ])
  // FIXME 打开时直接卡死崩溃
  var sl = Math.min(10, Math.floor(BAD_STRS.length/5))
  for (var i = 0; i < sl; i ++) {
     var v = BAD_STRS[i]
     for (var j = 0; j < sl; j ++) {
         BAD_STRS.push(v + BAD_STRS[j])
     }
  }
  var sl2 = Math.min(5, Math.floor(sl/5))
  for (var i = 0; i < sl2; i ++) {
      var v = BAD_STRS[i]
      for (var j = 0; j < sl2; j ++) {
          var v2 = v + BAD_STRS[j]
          for (var k = 0; k < sl2; k ++) {
            BAD_STRS.push(v2 + BAD_STRS[k])
          }
      }
  }

  var BAD_ARRS = []
  for (var i = 0; i < BAD_STRS.length; i ++) {
    BAD_ARRS.push([BAD_STRS[i]])
  }
  // FIXME 打开时直接卡死崩溃
  var al = Math.min(10, Math.floor(BAD_STRS.length/5))
  for (var i = 0; i < al; i ++) {
      var v = BAD_STRS[i]
      for (var j = 0; j < al; j ++) {
        BAD_ARRS.push([v, BAD_STRS[j]])
      }
  }
  var al2 = Math.min(3, Math.floor(al/5))
  for (var i = 0; i < al2; i ++) {
      var v = BAD_STRS[i]
      for (var j = 0; j < al2; j ++) {
          var v2 = BAD_STRS[j]
          for (var k = 0; k < al2; k ++) {
            BAD_ARRS.push([v, v2, BAD_STRS[k]])
          }
      }
  }

  var BAD_OBJS = []
  var ol = Math.min(10, Math.floor(BAD_STRS.length/5))
  for (var i = 10; i < ol; i ++) { // 太多就导致 RANDOM_BAD 基本每次随机出来都是对象
    var k = BAD_STRS[i]
    var key = k == undefined ? 'undefined' : (typeof k == 'string' ? k : JSON.stringify(k))
    for (var j = 0; j < ol; j ++) {
        BAD_OBJS.push({[key]: BAD_STRS[j]})
    }
  }
  // FIXME 打开时直接卡死崩溃
  for (var i = 0; i < ol; i ++) {
    var k = BAD_STRS[i]
    var key = k == undefined ? 'undefined' : (typeof k == 'string' ? k : JSON.stringify(k))
    for (var j = 0; j < ol; j ++) {
      var val = BAD_STRS[j]

      for (var i2 = 0; i2 < ol; i2 ++) {
        var k2 = BAD_STRS[i2]
        var key2 = k2 == undefined ? 'undefined' : (typeof k2 == 'string' ? k2 : JSON.stringify(k))
        for (var j2 = 0; j2 < ol; j2 ++) {
            BAD_OBJS.push({[key]: val, [key2]: BAD_STRS[j2]})
        }
      }
    }
  }
//  var ol = Math.min(10, Math.floor(BAD_OBJS.length/5))
//  for (var i = 0; i < ol; i ++) {
//      var v = BAD_OBJS[BAD_OBJS.length - i]
//      for (var j = 0; j < ol; j ++) {
//        BAD_OBJS.push(Object.assign(v, BAD_OBJS[BAD_OBJS.length - j]))
//      }
//  }
//  var ol2 = Math.min(2, Math.floor(ol/5))
//  for (var i = 0; i < ol2; i ++) {
//      var v = BAD_OBJS[BAD_OBJS.length - i]
//      for (var j = 0; j < ol2; j ++) {
//          var v2 = Object.assign(v, BAD_OBJS[BAD_OBJS.length - j])
//          for (var k = 0; k < ol2; k ++) {
//            BAD_OBJS.push(Object.assign(v2, BAD_OBJS[BAD_OBJS.length - k]))
//          }
//      }
//  }

  var BADS = BAD_STRS.concat(BAD_ARRS).concat(BAD_OBJS)

  var PRIME_INTS = [
  1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97
  , 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199
  , 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293
  , 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397
  , 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499
  , 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599
  , 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691
  , 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797
  , 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887
  , 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997
  , 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097
  , 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193
  , 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297
  , 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399
  , 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499
  , 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597
  , 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699
  , 1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789
  , 1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889
  , 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987, 1993, 1997, 1999
  ]
  function randomPrimeInt() {
    return PRIME_INTS[randomInt(0, PRIME_INTS.length - 1)]
  }
  function randomInt(min, max) {
    return randomNum(min, max, 0);
  }
  function randomNum(min, max, precision) {
    // 0 居然也会转成  Number.MIN_SAFE_INTEGER ！！！
    // start = start || Number.MIN_SAFE_INTEGER
    // end = end || Number.MAX_SAFE_INTEGER

    if (min == null) {
      min = 0 // Number.MIN_SAFE_INTEGER
    }
    if (max == null) {
      max = Number.MAX_SAFE_INTEGER
    }
    if (precision == null) {
      precision = 2
    }

    return + ((max - min)*Math.random() + min).toFixed(precision)
  }
  function randomStr(minLength, maxLength, availableChars) {
    return 'Ab_Cd' + randomNum()
  }
  function randomIn(...args) {
    return args == null || args.length <= 0 ? null : args[randomInt(0, args.length - 1)]
  }
  function randomBad(defaultArgs, ...args) {
    if (defaultArgs == null) {
      defaultArgs = BADS
    }

    if (args == null || args.length <= 0) {
      return defaultArgs[randomInt(0, defaultArgs.length - 1)]
    }

    args = []
    for (var i = 0; i < defaultArgs.length; i++) {
      args.push(defaultArgs[i])
    }

    return args[randomInt(0, args.length - 1)]
  }
  function randomBadBool(...args) {
    return randomBad(BAD_BOOLS, args)
  }
  function randomBadNum(...args) {
    return randomBad(BAD_NUMS, args)
  }
  function randomBadStr(...args) {
    return randomBad(BAD_STRS, args)
  }
  function randomBadArr(...args) {
    return randomBad(BAD_ARRS, args)
  }
  function randomBadObj(...args) {
    return randomBad(BAD_OBJS, args)
  }

  function orderInt(desc, index, min, max) {
    if (min == null) {
      min = 0 // Number.MIN_SAFE_INTEGER
    }
    if (max == null) {
      max = Number.MAX_SAFE_INTEGER
    }

    if (desc) {
      return max - index%(max - min + 1)
    }
    return min + index%(max - min + 1)
  }
  function orderIn(desc, index, ...args) {
    // alert('orderIn  index = ' + index + '; args = ' + JSON.stringify(args));
    index = index || 0;
    return args == null || args.length <= index ? null : args[desc ? args.length - 1 - index : index];
  }
  function orderBad(defaultArgs, desc, index, ...args) {
    // alert('orderIn  index = ' + index + '; args = ' + JSON.stringify(args));
    if (defaultArgs == null) {
      defaultArgs = BADS
    }

    index = index || 0;
    if (args == null || args.length <= 0) {
      return defaultArgs[desc ? defaultArgs.length - index : index]
    }

    args = []
    for (var i = 0; i < defaultArgs.length; i++) {
      args.push(defaultArgs[i])
    }

    return args[desc ? args.length - index : index]
  }
  function orderBadBool(desc, index, ...args) {
    return orderBad(BAD_BOOLS, desc, index, ...args)
  }
  function orderBadNum(desc, index, ...args) {
    return orderBad(BAD_NUMS, desc, index, ...args)
  }
  function orderBadStr(desc, index, ...args) {
    return orderBad(BAD_STRS, desc, index, ...args)
  }
  function orderBadArr(desc, index, ...args) {
    return orderBad(BAD_ARRS, desc, index, ...args)
  }
  function orderBadObj(desc, index, ...args) {
    return orderBad(BAD_OBJS, desc, index, ...args)
  }

  function getOrderIndex(randomId, line, argCount, step) {
    // alert('randomId = ' + randomId + '; line = ' + line + '; argCount = ' + argCount);
    // alert('ORDER_MAP = ' + JSON.stringify(ORDER_MAP, null, '  '));

    if (randomId == null) {
      randomId = 0;
    }
    if (ORDER_MAP == null) {
      ORDER_MAP = {};
    }
    if (ORDER_MAP[randomId] == null) {
      ORDER_MAP[randomId] = {};
    }

    var orderIndex = ORDER_MAP[randomId][line];
    // alert('orderIndex = ' + orderIndex)

    if (orderIndex == null || orderIndex < -1) {
      orderIndex = -1;
    }
    if (argCount == null) {
      argCount = 0;
    }
    if (step == null) {
      step = 1;
    }

    orderIndex ++
    ORDER_MAP[randomId][line] = orderIndex;
    orderIndex = argCount <= 0 ? step*orderIndex : step*orderIndex%argCount;

    // alert('orderIndex = ' + orderIndex)
    // alert('ORDER_MAP = ' + JSON.stringify(ORDER_MAP, null, '  '));
    return orderIndex;
  }
  //这些全局变量不能放在data中，否则会报undefined错误

  var BREAK_ALL = 'BREAK_ALL'
  var BREAK_LAST = 'BREAK_LAST'

  var baseUrl
  var inputted
  var handler
  var errHandler
  var docObj
  var doc
  var output

  var isSingle = true

  var currentTarget = vInput;
  var isInputValue = false;
  var isClickSelectInput = false;
  var selectionStart = 0;
  var selectionEnd = 0;

  function newDefaultScript() {
    return { // index.html 中 v-model 绑定，不能为 null
      case: {
        0: {
          pre: { // 可能有 id
            script: '' // index.html 中 v-model 绑定，不能为 null
          },
          post: {
            script: ''
          }
        },
        1560244940013: {
          pre: { // 可能有 id
            script: '' // index.html 中 v-model 绑定，不能为 null
          },
          post: {
            script: ''
          }
        }
      },
      account: {
        0: {
          pre: {
            script: ''
          },
          post: {
            script: ''
          }
        },
        82001: {
          pre: {
            script: ''
          },
          post: {
            script: ''
          }
        }
      },
      global: {
        0: {
          pre: {
            script: ''
          },
          post: {
            script: ''
          }
        }
      }
    }
  }

// APIJSON >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

  var App = {
    el: '#app',
    data: {
      baseview: 'formater',
      view: 'output',
      jsoncon: JSON.stringify(initJson),
      jsonhtml: initJson,
      compressStr: '',
      error: {},
      requestVersion: 3,
      requestCount: 1,
      urlComment: '  // 1080x2340, Android 9.0, Xiaomi MIX 3, IMEI 8698100377666021',
      selectIndex: 0,
      allowMultiple: true,
      isFullScreen: false,
      hoverIds: { before: null, diff: null, after: null },
      visiblePaths: [],
      sameIds: [],
      missTruth: {},
      compareRandomIds: null, // [],
      isEditReqLink: false,
      currentBbox: null,
      isDrawingBox: false,
      drawingBox: { startX: 0, startY: 0, endX: 0, endY: 0 },
      isLabelModalShow: false,
      labelModalPosition: { x: 0, y: 0 },
      selectedColor: {hexString: '#FF6B6B'},
      newLabelText: '',
      currentLabel: {
        text: 'object',
        color: 'blue'
      },
      boxLabels: [{
        text: 'object',
        color: 'blue'
      }],
      isColorPickerShow: false,
      colorPickerPosition: { x: 0, y: 0 },
      // 拖拉和旋转相关状态变量
      isDragging: false,        // 是否正在拖拉
      isResizing: false,        // 是否正在缩放
      isRotating: false,        // 是否正在旋转
      dragMode: '',             // 操作模式: 'move', 'resize', 'rotate'
      dragStartX: 0,            // 拖拉开始时的鼠标X坐标
      dragStartY: 0,            // 拖拉开始时的鼠标Y坐标
      originBox: null,        // 原始框数据备份
      currentCorner: '',        // 当前操作的角点: 'tl', 'tr', 'bl', 'br'
      rotationCenter: {x: 0, y: 0}, // 旋转中心点
      detection: {
        isShowNum: true,
        total: 10,
        sameIds: [],
        missTruth: {},

        afterThreshold: 35,
        afterCorrect: 9,
        afterImgCorrect: 9,
        afterAllCorrect: 9,
        afterWrong: 1,
        afterImgWrong: 1,
        afterAllWrong: 1,
        afterMiss: 1,
        afterImgMiss: 1,
        afterAllMiss: 1,
        afterRecall: 9/(9+1),
        afterImgRecall: 9/(9+1),
        afterAllRecall: 9/(9+1),
        afterRecallStr: (100*this.afterRecall).toFixed(0) + '%',
        afterImgRecallStr: (100*this.afterImgRecall).toFixed(0), // + '%',
        afterAllRecallStr: (100*this.afterAllRecall).toFixed(0), // + '%',
        afterPrecision: 9/(9+1),
        afterImgPrecision: 9/(9+1),
        afterAllPrecision: 9/(9+1),
        afterPrecisionStr: (100*this.afterPrecision).toFixed(0) + '%',
        afterImgPrecisionStr: (100*this.afterImgPrecision).toFixed(0), // + '%',
        afterAllPrecisionStr: (100*this.afterAllPrecision).toFixed(0), // + '%',
        afterF1: 2*(this.afterRecall*this.afterPrecision)/(this.afterRecall + this.afterPrecision),
        afterImgF1: 2*(this.afterImgRecall*this.afterImgPrecision)/(this.afterImgRecall + this.afterImgPrecision),
        afterAllF1: 2*(this.afterAllRecall*this.afterAllPrecision)/(this.afterAllRecall + this.afterAllPrecision),
        afterF1Str: (100*this.afterF1).toFixed(0) + '%',
        afterImgF1Str: (100*this.afterImgF1).toFixed(0), // + '%',
        afterAllF1Str: (100*this.afterAllF1).toFixed(0), // + '%',
        after: { bboxes: [] },

        beforeThreshold: 30,
        beforeCorrect: 8,
        beforeImgCorrect: 8,
        beforeAllCorrect: 8,
        beforeWrong: 3,
        beforeImgWrong: 3,
        beforeAllWrong: 3,
        beforeMiss: 2,
        beforeImgMiss: 2,
        beforeAllMiss: 2,
        beforeRecall: 8/(8+3),
        beforeImgRecall: 8/(8+3),
        beforeALlRecall: 8/(8+3),
        beforeRecallStr: (100*this.beforeRecall).toFixed(0) + '%',
        beforeImgRecallStr: (100*this.beforeImgRecall).toFixed(0), // + '%',
        beforeAllRecallStr: (100*this.beforeAllRecall).toFixed(0), // + '%',
        beforePrecision: 8/(8+2),
        beforeImgPrecision: 8/(8+2),
        beforeALlPrecision: 8/(8+2),
        beforePrecisionStr: (100*this.beforePrecision).toFixed(0) + '%',
        beforeImgPrecisionStr: (100*this.beforeImgPrecision).toFixed(0), // + '%',
        beforeAllPrecisionStr: (100*this.beforeAllPrecision).toFixed(0), // + '%',
        beforeF1: 2*(this.beforeRecall*this.beforePrecision)/(this.beforeRecall + this.beforePrecision),
        beforeImgF1: 2*(this.beforeImgRecall*this.beforeImgPrecision)/(this.beforeImgRecall + this.beforeImgPrecision),
        beforeAllF1: 2*(this.beforeAllRecall*this.beforeAllPrecision)/(this.beforeAllRecall + this.beforeAllPrecision),
        beforeF1Str: (100*this.beforeF1).toFixed(0) + '%',
        beforeImgF1Str: (100*this.beforeImgF1).toFixed(0), // + '%',
        beforeAllF1Str: (100*this.beforeAllF1).toFixed(0), // + '%',
        before: { bboxes: [] },

        diffThreshold: 90,
        diffCorrectStr: '+' + 1,
        diffImgCorrectStr: '+' + 1,
        diffAllCorrectStr: '+' + 1,
        diffWrongStr: '-' + 2,
        diffImgWrongStr: '-' + 2,
        diffAllWrongStr: '-' + 2,
        diffMissStr: '-' + 1,
        diffImgMissStr: '-' + 1,
        diffAllMissStr: '-' + 1,
        diffRecallStr: '+' + (100*(this.afterRecall - this.beforeRecall)).toFixed(0) + '%',
        diffImgRecallStr: '+' + (100*(this.afterImgRecall - this.beforeImgRecall)).toFixed(0), // + '%',
        diffAllRecallStr: '+' + (100*(this.afterAllRecall - this.beforeAllRecall)).toFixed(0), // + '%',
        diffPrecisionStr: '+' + (100*(this.afterPrecision - this.beforePrecision)).toFixed(0) + '%',
        diffImgPrecisionStr: '+' + (100*(this.afterImgPrecision - this.beforeImgPrecision)).toFixed(0), // + '%',
        diffAllPrecisionStr: '+' + (100*(this.afterAllPrecision - this.beforeAllPrecision)).toFixed(0), // + '%',
        diffF1: '+' + (100*(this.afterF1 - this.beforeF1)).toFixed(0) + '%',
        diffImgF1: '+' + (100*(this.afterImgF1 - this.beforeImgF1)).toFixed(0), // + '%',
        diffAllF1: '+' + (100*(this.afterAllF1 - this.beforeAllF1)).toFixed(0), // + '%',
        diffF1Str: '+' + (100*this.diffF1).toFixed(0) + '%',
        diffImgF1Str: '+' + (100*this.diffImgF1).toFixed(0), // + '%',
        diffAllF1Str: '+' + (100*this.diffAllF1).toFixed(0), // + '%',
        diff: { bboxes: [] },
      },
      img: '', // 'img/Screenshot_2020-11-07-16-35-27-473_apijson.demo.jpg',
      file: 'Screenshot_2020-11-07-16-35-27-473_apijson.demo.jpg',
      imgMap: {},
      canvasMap: {},
      options: [], // [{name:"id", type: "integer", comment:"主键"}, {name:"name", type: "string", comment:"用户名称"}],
      historys: [],
      history: {name: '请求0'},
      remotes: [],
      locals: [],
      chainPaths: [],
      casePaths: [],
      chainGroups: [],
      caseGroups: [],
      testCases: [],
      randoms: [],
      randomSubs: [],
      account: '13000082005',
      password: '123456',
      logoutSummary: {},
      accounts: [
        {
          'id': 82001,
          'isLoggedIn': false,
          'name': '测试账号5',
          'phone': '13000082005',
          'password': '123456'
        },
        {
          'id': 82002,
          'isLoggedIn': false,
          'name': '测试账号2',
          'phone': '13000082002',
          'password': '123456'
        },
        {
          'id': 82003,
          'isLoggedIn': false,
          'name': '测试账号3',
          'phone': '13000082003',
          'password': '123456'
        }
      ],
      otherEnvTokenMap: {},
      otherEnvCookieMap: {},
      allSummary: {},
      currentAccountIndex: 0,
      currentChainGroupIndex: -1,
      currentDocIndex: -1,
      currentRandomIndex: -1,
      currentRandomSubIndex: -1,
      tests: { '-1':{}, '0':{}, '1':{}, '2': {} },
      crossProcess: '交叉账号:已关闭',
      testProcess: '机器学习:已关闭',
      randomTestTitle: '事件 Event',
      testRandomCount: 1,
      testRandomProcess: '',
      compareColor: '#0000',
      scriptType: 'case',
      scriptBelongId: 0,
      scripts: newDefaultScript(),
      retry: 0, // 每个请求前的等待延迟
      wait: 0, // 每个请求前的等待延迟
      timeout: null, // 每个请求的超时时间
      loadingCount: 0,
      isPreScript: true,
      isRandomTest: false,
      isDelayShow: false,
      isSaveShow: false,
      isExportShow: false,
      isExportCheckShow: false,
      isExportRandom: false,
      isExportScript: false,
      isOptionListShow: false,
      isTestCaseShow: false,
      isHeaderShow: false,
      isScriptShow: false,
      isRandomShow: true,  // 默认展示
      isRandomListShow: true,
      isRandomSubListShow: false,
      isRandomEditable: false,
      isCaseGroupEditable: false,
      isLoginShow: false,
      isConfigShow: false,
      isDeleteShow: false,
      groupShowType: 0,
      caseShowType: 1,
      chainShowType: 0,
      statisticsShowType: 0,
      currentHttpResponse: {},
      currentDocItem: {},
      currentRemoteItem: {
        "Flow":  {
          "id": 1560244940013 ,
          "userId": 82001 ,
          "testAccountId": 82001 ,
          "version": 3 ,
          "name": "测试查询" ,
          "method": "POST" ,
          "type": "JSON" ,
          "url": "/get" ,
          "date": "2019-06-11 17:22:20.0",
// 导致清空文本后，在说明文档后面重叠显示这个绿色注释          "detail": `
// 以上 JSON 文本支持 JSON5 格式。清空文本内容可查看规则。
// 注释可省略。行注释前必须有两个空格；段注释必须在 JSON 下方。
//
// ## 快捷键
// Ctrl + I 或 Command + I 格式化 JSON
//
// #### 右上角设置项 > 预览请求输入框，显示对应的预览效果`
        },
        "TestRecord":  {
          "id": 1615135440014 ,
          "userId": 82001 ,
          "documentId": 1560244940013
        }
      },
      currentRandomItem: {},
      eventList: [],
      outputList: [],
      isAdminOperation: false,
      loginType: 'login',
      isExportRemote: false,
      isRegister: false,
      isCrossEnabled: false,
      isMLEnabled: false,
      isDelegateEnabled: false,
      isEnvCompareEnabled: false,
      isPreviewEnabled: false,
      isStatisticsEnabled: false,
      isEncodeEnabled: true,
      isEditResponse: false,
      isLocalShow: false,
      isChainShow: false,
      isAllowDisable: false,
      uploadTotal: 0,
      uploadDoneCount: 0,
      uploadFailCount: 0,
      uploadRandomCount: 0,
      exTxt: {
        name: 'APIJSON测试',
        label: '发布简单接口',
        button: '保存',
        index: 0
      },
      themes: themes,
      checkedTheme: 0,
      isExpand: true,
      reportId: null,
      User: {
        id: 0,
        name: '',
        head: ''
      },
      Privacy: {
        id: 0,
        balance: null //点击更新提示需要判空 0.00
      },
      isVideoFirst: false,
      operate: OPERATE_TYPE_REVIEW,
      operates: [ OPERATE_TYPE_RECORD, OPERATE_TYPE_REVIEW, OPERATE_TYPE_REPLAY, OPERATE_TYPE_HTTP ],
      method: HTTP_METHOD_POST,
      methods: null, // HTTP_METHODS,
      type: REQUEST_TYPE_JSON,
      types: [ REQUEST_TYPE_JSON, REQUEST_TYPE_PARAM, REQUEST_TYPE_FORM, REQUEST_TYPE_DATA ],
      host: 'uigo.x.UIAutoApp', // 'unitauto.test.TestUtil.',
      branch: 'countArray',
      database: 'MYSQL', // 查文档必须，除非后端提供默认配置接口  // 用后端默认的，避免用户总是没有配置就问为什么没有生成文档和注释  'MYSQL',// 'POSTGRESQL',
      schema: 'sys',  // 查文档必须，除非后端提供默认配置接口  // 用后端默认的，避免用户总是没有配置就问为什么没有生成文档和注释   'sys',
      otherEnv: 'http://localhost:8080',  // 其它环境服务地址，用来对比当前的
      server: '', // 'http://apijson.cn:8080', // 'http://localhost:8080', //  Chrome 90+ 跨域问题非常难搞，开发模式启动都不行了
      // server: 'http://47.74.39.68:9090',  // apijson.org
      projectHost: {host: 'http://192.168.31.5:8080', project: 'APIJSON'},  // apijson.cn
      thirdParty: 'SWAGGER /v2/api-docs',  //apijson.cn
      // thirdParty: 'RAP /repository/joined /repository/get',
      // thirdParty: 'YAPI /api/interface/list_menu /api/interface/get',
      projectHosts: [
        {host: 'http://192.168.31.5:8080', project: 'APIJSON'},
        {host: 'http://apijson.cn:8080', project: 'APIJSON-UIGO'},
        {host: 'http://192.168.31.5:8080', project: 'UIGOX'},
        {host: 'http://192.168.12.345:8080', project: 'UIGO'}
      ],
      language: CodeUtil.LANGUAGE_KOTLIN,
      header: {},
      page: 0,
      count: 20,
      search: '',
      chainGroupPage: 0,
      chainGroupPages: {},
      chainGroupCount: 0,
      chainGroupCounts: {},
      chainGroupSearch: '',
      chainGroupSearches: {},
      caseGroupPage: 0,
      caseGroupPages: {},
      caseGroupCount: 0,
      caseGroupCounts: {},
      caseGroupSearch: '',
      caseGroupSearches: {},
      testCasePage: 0,
      testCasePages: {},
      testCaseCount: 20,
      testCaseCounts: {},
      testCaseSearch: '',
      testCaseSearches: {},
      randomPage: 0,
      randomCount: 10,
      randomSearch: '',
      randomSubPage: 0,
      randomSubCount: 10,
      randomSubSearch: '',
      doneCount: 0,
      allCount: 0,
      deepDoneCount: 0,
      deepAllCount: 0,
      randomDoneCount: 0,
      randomAllCount: 0,
      picDelayTime: 0,
      coverage: {
        json: {},
        html: ''
      }
    },

    methods: {
      // 全部展开
      expandAll: function () {
        if (this.view != 'code') {
          alert('请先获取正确的JSON Response！')
          return
        }

        $('.icon-square-min').show()
        $('.icon-square-plus').hide()
        $('.expand-view').show()
        $('.fold-view').hide()

        this.isExpand = true;
      },

      // 全部折叠
      collapseAll: function () {
        if (this.view != 'code') {
          alert('请先获取正确的JSON Response！')
          return
        }

        $('.icon-square-min').hide()
        $('.icon-square-plus').show()
        $('.expand-view').hide()
        $('.fold-view').show()

        this.isExpand = false;
      },

      // diff
      diffTwo: function () {
        var oldJSON = {}
        var newJSON = {}
        this.view = 'code'
        try {
          oldJSON = jsonlint.parse(this.jsoncon)
        } catch (ex) {
          this.view = 'error'
          this.error = {
            msg: '原 JSON 解析错误\r\n' + ex.message
          }
          return
        }

        try {
          newJSON = jsonlint.parse(this.jsoncon)
        } catch (ex) {
          this.view = 'error'
          this.error = {
            msg: '新 JSON 解析错误\r\n' + ex.message
          }
          return
        }

        var base = difflib.stringAsLines(JSON.stringify(oldJSON, '', 4))
        var newtxt = difflib.stringAsLines(JSON.stringify(newJSON, '', 4))
        var sm = new difflib.SequenceMatcher(base, newtxt)
        var opcodes = sm.get_opcodes()
        $('#diffoutput').empty().append(diffview.buildView({
          baseTextLines: base,
          newTextLines: newtxt,
          opcodes: opcodes,
          baseTextName: '原 JSON',
          newTextName: '新 JSON',
          contextSize: 2,
          viewType: 0
        }))
      },

      baseViewToDiff: function () {
        this.baseview = 'diff'
        this.diffTwo()
      },

      // 回到格式化视图
      baseViewToFormater: function () {
        this.baseview = 'formater'
        this.view = 'code'
        this.showJsonView()
      },

      // 根据json内容变化格式化视图
      showJsonView: function () {
        if (this.baseview === 'diff') {
          return
        }
        try {
          if (this.jsoncon.trim() === '') {
            this.view = 'empty'
          } else {
            this.view = 'code'

            var ret = this.jsoncon
            try {
              ret = jsonlint.parse(this.jsoncon)
            } catch (ex) {
              log(ex)
            }

            var method = this.getMethod();  // m 已经 toUpperCase 了
            var isRestful = ! JSONObject.isAPIJSONPath(method);

            var path = null;
            var key = null;

            if (isSingle || ! JSONResponse.isObject(vi)) {
              var val = ret;
              if (isSingle != true && val instanceof Array) {
                  // alert('onRenderJSONItem  key = ' + key + '; val = ' + JSON.stringify(val))
                  var ckey = key == null ? null : key.substring(0, key.lastIndexOf('[]'));

                  var aliaIndex = ckey == null ? -1 : ckey.indexOf(':');
                  var objName = aliaIndex < 0 ? ckey : ckey.substring(0, aliaIndex);

                  var firstIndex = objName == null ? -1 : objName.indexOf('-');
                  var firstKey = firstIndex < 0 ? objName : objName.substring(0, firstIndex);

                  for (var i = 0; i < val.length; i++) {
                    var vi = val[i]

                    if (JSONResponse.isObject(vi) && JSONObject.isTableKey(firstKey || '', vi, isRestful)) {

                      var curPath = '' + i;
                      var curTable = firstKey;
                      var thiz = {
                        _$_path_$_: curPath,
                        _$_table_$_: curTable
                      };

                      var newVal = {};
                      for (var k in vi) {
                        newVal[k] = vi[k]; //提升性能
                        if (this.isFullAssert) {
                            try {
                              var cri = this.currentRemoteItem || {};
                              var tr = cri.TestRecord || {};
                              var d = cri.Flow || {};
                              var standard = this.isMLEnabled ? tr.standard : tr.response;
                              var standardObj = StringUtil.isEmpty(standard, true) ? null : parseJSON(standard);
                              var tests = this.tests[String(this.currentAccountIndex)] || {};
                              var responseObj = (tests[d.id] || {})[0]

                              var pathUri = (StringUtil.isEmpty(curPath, false) ? '' : curPath + '/') + k;
                              var pathKeys = StringUtil.split(pathUri, '/');
                              var target = this.isMLEnabled ? JSONResponse.getStandardByPath(standardObj, pathKeys) : JSONResponse.getValByPath(standardObj, pathKeys);
                              var real = JSONResponse.getValByPath(responseObj, pathKeys);
                              var cmp = this.isMLEnabled ? JSONResponse.compareWithStandard(target, real, pathUri) : JSONResponse.compareWithBefore(target, real, pathUri);
//                              cmp.path = pathUri;
                              var cmpShowObj = JSONResponse.getCompareShowObj(cmp);
                              thiz[k] = [cmpShowObj.compareType, cmpShowObj.compareColor, cmpShowObj.compareMessage];
                              var countKey = '_$_' + cmpShowObj.compareColor + 'Count_$_';
                              thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                            } catch (e) {
                              thiz[k] = [JSONResponse.COMPARE_ERROR, 'red', e.message];
                              var countKey = '_$_redCount_$_';
                              thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                            }
                        }

                        delete vi[k]
                      }

                      vi._$_this_$_ = JSON.stringify(thiz)
                      for (var k in newVal) {
                        vi[k] = newVal[k]
                      }
                    }

                  }
                }

                this.jsonhtml = val;
            }
            else {
                var thiz = {
                   _$_path_$_: null,
                   _$_table_$_: null
                };

                for (var k in ret) {
                  if (this.isFullAssert) {
                      try {
                        var cri = this.currentRemoteItem || {};
                        var tr = cri.TestRecord || {};
                        var d = cri.Flow || {};
                        var standard = this.isMLEnabled ? tr.standard : tr.response;
                        var standardObj = StringUtil.isEmpty(standard, true) ? null : parseJSON(standard);
                        var tests = this.tests[String(this.currentAccountIndex)] || {};
                        var responseObj = (tests[d.id] || {})[0]

                        var pathUri = k;
                        var pathKeys = StringUtil.split(pathUri, '/');
                        var target = this.isMLEnabled ? JSONResponse.getStandardByPath(standardObj, pathKeys) : JSONResponse.getValByPath(standardObj, pathKeys);
                        var real = JSONResponse.getValByPath(responseObj, pathKeys);
                //              c = JSONResponse.compareWithBefore(target, real, path);
                        var cmp = this.isMLEnabled ? JSONResponse.compareWithStandard(target, real, pathUri) : JSONResponse.compareWithBefore(target, real, pathUri);
//                        cmp.path = pathUri;
                        var cmpShowObj = JSONResponse.getCompareShowObj(cmp);
                        thiz[k] = [cmpShowObj.compareType, cmpShowObj.compareColor, cmpShowObj.compareMessage];
                        var countKey = '_$_' + cmpShowObj.compareColor + 'Count_$_';
                        thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                      } catch (e) {
                        thiz[k] = [JSONResponse.COMPARE_ERROR, 'red', e.message];
                        var countKey = '_$_redCount_$_';
                        thiz[countKey] = thiz[countKey] == null ? 1 : thiz[countKey] + 1;
                      }
                  }
                }

                this.jsonhtml = Object.assign({
                    _$_this_$_: JSON.stringify(thiz)
                }, ret)
            }

          }
        } catch (ex) {
          this.view = 'error'
          this.error = {
            msg: ex.message
          }
        }
      },


      showUrl: function (isAdminOperation, branchUrl) {
        if (StringUtil.isEmpty(this.host, true)) {  //显示(可编辑)URL Host
          if (isAdminOperation != true) {
            baseUrl = this.getBaseUrl(this.projectHost.host, true)
          }
          vUrl.value = (isAdminOperation ? this.server : baseUrl) + branchUrl
        }
        else {  //隐藏(固定)URL Host
          if (isAdminOperation) {
            this.host = this.server
          }
          vUrl.value = branchUrl
        }

        vUrlComment.value = isSingle || StringUtil.isEmpty(this.urlComment, true)
          ? '' : vUrl.value+ ' // ' + (this.requestVersion > 0 ? 'V' + this.requestVersion : 'V*') + this.urlComment
      },

      //设置基地址
      setBaseUrl: function () {
        if (StringUtil.isEmpty(this.host, true) != true) {
          return
        }
        // 重新拉取文档
        var bu = this.getBaseUrl(this.projectHost.host, true)
        if (baseUrl != bu) {
          baseUrl = bu
          // doc = null //这个是本地的数据库字典及非开放请求文档
          this.saveCache('', 'URL_BASE', baseUrl)

          //已换成固定的管理系统URL

          // this.remotes = []

          // var index = baseUrl.indexOf(':') //http://localhost:8080
          // this.server = (index < 0 ? baseUrl : baseUrl.substring(0, baseUrl)) + ':9090'

          var accounts = []
          if (StringUtil.isNotEmpty(bu, true)) {
            accounts = this.getCache(bu, 'accounts', [])
//            for (var i = 0; i < accounts.length; i ++) {
//               var ats = accounts[i]
//               if (ats == null || (StringUtil.isNotEmpty(ats.baseUrl, true) && ats.baseUrl != bu)) {
//
//               }
//            }
          }
          else {
              var baseUrls = this.getCache('', 'baseUrls', [])
              for (var i = 0; i < baseUrls.length; i ++) {
                var bu2 = baseUrls[i]
                var ats = this.getCache(bu2, 'accounts', [])
//                accounts.push({
//                  baseUrl: bu2,
//                  id: 0,
//                  phone: 0,
//                  name: ''
//                })
                accounts = accounts.concat(ats)
              }
          }

          if (accounts.length >= 1) {
            this.accounts = accounts
          }
        }
      },
      getUrl: function () {
        var url = StringUtil.trim(vUrl.value)
        if (! url.includes('://')) {
          url = StringUtil.noBlank(this.host) + (url.startsWith('/') ? url : '/' + url)
        }
        return StringUtil.noBlank(url)
      },
      //获取基地址
      getBaseUrl: function (url_, fixed) {
        var url = StringUtil.trim(url_ != undefined ? url_ : baseUrl || this.projectHost.host) // vUrl.value)
        var length = this.getBaseUrlLength(url)
        if (length <= 0 && url_ == undefined) {
          var account = this.getCurrentAccount()
          if (account != null) {
            return account.baseUrl || ''
          }
        }

        url = length <= 0 ? '' : url.substring(0, length)
        return url == '' ? (fixed != true ? URL_BASE : '') : url
      },
      //获取基地址长度，以://后的第一个/分割baseUrl和method
      getBaseUrlLength: function (url_) {
        var url = StringUtil.trim(url_)
        var index = url.indexOf(' ')
        if (index >= 0) {
          return index + 1
        }

        index = url.indexOf('://')
        if (index < 0 && (url.startsWith('/') || url.indexOf('.') < 0)) {
          return 0
        }

        var rest = index < 0 ? url : url.substring(index + 3)
        var ind = rest.indexOf('/')

        return ind < 0 ? url.length : (index < 0 ? 0 : index + 3) + ind
      },
      //获取操作方法
      //获取操作方法
      getMethod: function (url) {
        url = url || new String(vUrl.value).trim()
        var index = url.lastIndexOf('.')
        url = index <= 0 ? url : url.substring(index + 1)
        return StringUtil.trim(url.startsWith('.') ? url.substring(1) : url)
      },
      //获取操作方法
      getClass: function (url) {
        url = url || this.getUrl()
        var index = url.lastIndexOf('.')
        if (index <= 0) {
          throw new Error('必须要有类名！完整的 URL 必须符合格式 package.Class.method ！')
        }
        // url = url.substring(0, index)
        // index = url.lastIndexOf('.')
        var clazz = StringUtil.trim(index < 0 ? url : url.substring(index + 1))
        if (App.language == 'Java' || App.language == 'JavaScript' || App.language == 'TypeScript') {
          if (/[A-Z]{0}[A-Za-z0-9_]/.test(clazz) != true) {
            alert('类名 ' + clazz + ' 不符合规范！')
          }
        }
        return clazz
      },
      //获取操作方法
      getPackage: function (url) {
        url = url || this.getUrl()
        var index = url.lastIndexOf('.')
        if (index <= 0) {
          throw new Error('必须要有类名！完整的 URL 必须符合格式 package.Class.method ！')
        }
        // url = url.substring(0, index)
        // index = url.lastIndexOf('.')
        return StringUtil.trim(index < 0 ? '' : url.substring(0, index))
      },

      getBranchUrl: function (url) {
        var url = StringUtil.get(url == null ? vUrl.value : url).trim()
        var index = this.getBaseUrlLength(url)
        url = index <= 0 ? url : url.substring(index)
        return url.startsWith('/') ? url : '/' + url
      },
      //获取请求的tag
      getTag: function () {
        var req = null;
        try {
          req = this.getRequest(vInput.value);
        } catch (e) {
          log('main.getTag', 'try { req = this.getRequest(vInput.value); \n } catch (e) {\n' + e.message)
        }
        return req == null ? null : req.tag
      },

      getRequest: function (json, defaultValue, isRaw, isTry) {  // JSON5 兜底，减少修改范围  , isSingle) {
        if (JSONResponse.isString(json) != true) {
          return json == null ? defaultValue : json
        }
        var s = isRaw != true && isSingle ? this.switchQuote(json) : json; // this.toDoubleJSON(json, defaultValue);
        if (StringUtil.isEmpty(s, true)) {
          return defaultValue
        }

        try {
          return jsonlint.parse(s);
        } catch (e) {
          log('main.getRequest', 'try { return jsonlint.parse(s); \n } catch (e) {\n' + e.message)
          try {
            return JSON5.parse(s)  // jsonlint.parse(this.removeComment(s));
          } catch (e2) {
            console.log('main.getRequest  try jsonlint.parse(s) >> JSON5.parse(s) >> catch e = ' + e.message + '; e2 = ' + e2.message + '; s = ' + s)
            if (isTry) {
              return defaultValue
            }
            throw e2
          }
        }
      },
      getExtraComment: function(json) {
        var it = json != null ? json : StringUtil.trim(vInput.value);

        var start = it.lastIndexOf('\n/*');
        var end = it.lastIndexOf('\n*/');

        return start < 0 || end <= start ? null : it.substring(start + '\n/*'.length, end);
      },

      getHeader: function (text) {
        var header = {}
        var hs = StringUtil.isEmpty(text, true) ? null : StringUtil.split(text, '\n')

        if (hs != null && hs.length > 0) {
          var item
          for (var i = 0; i < hs.length; i++) {
            item = hs[i] || ''

            // 解决整体 trim 后第一行  // 被当成正常的 key 路径而不是注释
            var index = StringUtil.trim(item).startsWith('//') ? 0 : item.lastIndexOf(' //')  // 不加空格会导致 http:// 被截断  ('//')  //这里只支持单行注释，不用 removeComment 那种带多行的去注释方式
            var item2 = index < 0 ? item : item.substring(0, index)
            item2 = item2.trim()
            if (item2.length <= 0) {
              continue;
            }

            index = item2.indexOf(':')
            if (index <= 0) {
              throw new Error('请求头 Request Header 输入错误！请按照每行 key: value 的格式输入，不要有多余的换行或空格！'
                + '\n错误位置: 第 ' + (i + 1) + ' 行'
                + '\n错误文本: ' + item)
            }

            var val = item2.substring(index + 1, item2.length)

            var ind = val.indexOf('(')  //一定要有函数是为了避免里面是一个简短单词和 AutoUI 代码中变量冲突
            if (ind > 0 && val.indexOf(')') > ind) {  //不从 0 开始是为了保证是函数，且不是 (1) 这种单纯限制作用域的括号
              try {
                val = eval(val)
              }
              catch (e) {
                this.log("getHeader  if (hs != null && hs.length > 0) { ... if (ind > 0 && val.indexOf(')') > ind) { ... try { val = eval(val) } catch (e) = " + e.message)
              }
            }

            header[StringUtil.trim(item2.substring(0, index))] = val
          }
        }

        return header
      },

      // 分享 AutoUI 特有链接，打开即可还原分享人的 JSON 参数、设置项、搜索关键词、分页数量及页码等配置
      shareLink: function (isRandom) {
        var settingStr = null
        try {
          settingStr = JSON.stringify({
            requestVersion: this.requestVersion,
            requestCount: this.requestCount,
            isTestCaseShow: this.isTestCaseShow,
            // isHeaderShow: this.isHeaderShow,
            // isRandomShow: this.isRandomShow,
            isRandomListShow: this.isRandomShow ? this.isRandomListShow : undefined,
            isRandomSubListShow: this.isRandomListShow ? this.isRandomSubListShow : undefined,
            // isRandomEditable: this.isRandomEditable,
            isCrossEnabled: this.isCrossEnabled,
            isMLEnabled: this.isMLEnabled,
            isDelegateEnabled: this.isDelegateEnabled,
            isPreviewEnabled: this.isPreviewEnabled,
            isStatisticsEnabled: this.isStatisticsEnabled,
            isEncodeEnabled: this.isEncodeEnabled,
            isEditResponse: this.isEditResponse,
            isLocalShow: this.isTestCaseShow ? this.isLocalShow : undefined,
            page: this.page,
            count: this.count,
            testCasePage: this.testCasePage,
            testCaseCount: this.testCaseCount,
            testRandomCount: this.testRandomCount,
            randomPage: this.randomPage,
            randomCount: this.randomCount,
            randomSubPage: this.randomSubPage,
            randomSubCount: this.randomSubCount,
            host: StringUtil.isEmpty(this.host, true) ? undefined : encodeURIComponent(this.host),
            search: StringUtil.isEmpty(this.search, true) ? undefined : encodeURIComponent(this.search),
            testCaseSearch: StringUtil.isEmpty(this.testCaseSearch, true) ? undefined : this.testCaseSearch,
            randomSearch: StringUtil.isEmpty(this.randomSearch, true) ? undefined : encodeURIComponent(this.randomSearch),
            randomSubSearch: StringUtil.isEmpty(this.randomSubSearch, true) ? undefined : encodeURIComponent(this.randomSubSearch)
          })
        } catch (e) {
          log(e)
        }

        // 实测 561059 长度的 URL 都支持，只是输入框显示长度约为 2000
        window.open(this.getShareLink(
          isRandom
          , null
          , null
          , null
          , this.isTestCaseShow || StringUtil.isEmpty(vHeader.value, true) ? null : encodeURIComponent(StringUtil.trim(vHeader.value))
          // , this.isTestCaseShow || StringUtil.isEmpty(vRandom.value, true) ? null : encodeURIComponent(StringUtil.trim(vRandom.value))
          , settingStr
        ))
      },
      getShareLink: function (isRandom, json, url, type, header, random, setting) {
        var jsonStr = json == null ? null : (typeof json == 'string' ? json : JSON.stringify(json))
        if (this.isTestCaseShow != true && jsonStr == null) { // StringUtil.isEmpty(jsonStr)
          try {
            jsonStr = JSON.stringify(encode(parseJSON(vInput.value)))
          } catch (e) {  // 可能包含注释
            log(e)
            jsonStr = encode(StringUtil.trim(vInput.value))
          }
        }

        var headerStr = header

        var randomStr = random

        // URL 太长导致打不开标签
        var settingStr = setting

        var href = window.location.href || 'http://apijson.cn/api'
        var ind = href == null ? -1 : href.indexOf('?')  // url 后带参数只能 encodeURIComponent

        return (ind < 0 ? href : href.substring(0, ind))
          + (this.view != 'code' ? "?send=false" : (isRandom ? "?send=random" : "?send=true"))
          + "&type=" + StringUtil.trim(type == null ? REQUEST_TYPE_JSON : type)
          + "&url=" + encodeURIComponent(StringUtil.trim(url == null ? vUrl.value : url))
          + (jsonStr == null ? '' : "&json=" + jsonStr)
          + (headerStr == null ? '' : "&header=" + headerStr)
          + (randomStr == null ? '' : "&random=" + randomStr)
          + (settingStr == null ? '' : "&setting=" + settingStr)

      },

      onClickSelectInput: function (item, index) {
        isClickSelectInput = true;
        this.selectInput(item, index, true);
      },
      selectInput: function (item, index, isDone) { // , isValue) {
        var target = currentTarget = currentTarget || vInput; // currentTarget = target;
        var isValue = isInputValue; // isInputValue = isValue;

        // 失去焦点后拿不到有效值
        // var selectionStart = target.selectionStart;
        // var selectionEnd = target.selectionEnd;

        var text = StringUtil.get(target.value);
        var before = text.substring(0, selectionStart);
        var after = text.substring(selectionEnd);

        var name = item == null ? '' : StringUtil.get(item.name);
        target.value = text = before + name + after
        if (target == vScript) { // 不这样会自动回滚
          this.scripts[this.scriptType][this.scriptBelongId][this.isPreScript ? 'pre' : 'post'].script = text
        }
        else if (target == vInput) {
          inputted = target.value;
        }

        if (isDone) {
          this.options = [];

          target.focus();
          try {
            selectionStart = target.selectionStart = selectionEnd + (isClickSelectInput ? (name.length - (isValue ? 4 : 0)) : 0)
                + (isValue ? (after.startsWith(',') ? 1 : 0) : (target == vInput || target == vScript ? 3 : 2));
            selectionEnd = target.selectionEnd = selectionStart + (isValue ? 0 : 4)
          } catch (e) {
            console.log(e)
          }
          isClickSelectInput = false;
          // vOption.focusout()

          if (this.isChainShow && this.isTestCaseShow) {
            this.addCase2Chain(item.value)
            return
          }

          if (isInputValue != true) {
            this.showOptions(target, text, before + name + (isSingle ? "'" : '"') + ': ', after.substring(3), true);
          }
        } else {
          target.selectionStart = selectionStart;
          selectionEnd = target.selectionEnd = selectionStart + name.length;
          isClickSelectInput = false;
        }
      },

      // 显示保存弹窗
      showSave: function (show) {
        if (show) {
          if (this.isTestCaseShow) {
            alert('请先输入请求内容！')
            return
          }

          var tag = this.getTag()
          this.history.name = (this.urlComment || this.getMethod() + (StringUtil.isEmpty(tag, true) ? '' : ' ' + tag)) + ' ' + this.formatTime() //不自定义名称的都是临时的，不需要时间太详细
        }
        this.isSaveShow = show
      },

      // 显示导出弹窗
      showExport: function (show, isRemote, isRandom, isScript) {
        if (show) {
          // this.isExportCheckShow = isRemote

          if (isRemote) { //共享测试用例
            this.isExportRandom = isRandom
            this.isExportScript = isScript

            // if (isRandom != true) {  // 分享搜索关键词和分页信息也挺好 } && this.isTestCaseShow != true) {  // 没有拿到列表，没用
              // setTimeout(function () {
              //   App.shareLink(App.isRandomTest)
              // }, 1000)
            // }

            if (this.isTestCaseShow) {
              alert('请先输入请求内容！')
              return
            }

            if (this.view == 'error') {  // this.view != 'code') {
              alert('发现错误，请输入正确的内容！')  // alert('请先测试请求，确保是正确可用的！')
              return
            }
            if (isRandom) {
              // this.exTxt.isSub = this.isRandomSubListShow
              this.exTxt.name = this.isRandomListShow || this.isRandomSubListShow ? vUrl.value : (this.randomTestTitle || '随机配置 ' + this.formatDateTime())
            }
            else if (isScript) { // 避免 APIJSON 启动报错  '执行脚本 ' + this.formatDateTime()
              this.exTxt.name = this.scriptType + (this.isPreScript ? 'Pre' : 'Post') + this.getCurrentScriptBelongId()
            }
            else {
              if (this.isEditResponse) {
                this.isExportRemote = isRemote
                this.exportTxt()
                return
              }

              // var tag = this.getTag()
              this.exTxt.name = this.urlComment || ''  // 避免偷懒不输入名称  this.getMethod() + (StringUtil.isEmpty(tag, true) ? '' : ' ' + tag)
            }
          }
          else { //下载到本地
            if (this.isTestCaseShow) { //文档
              this.exTxt.name = 'APIJSON自动化文档 ' + this.formatDateTime()
            }

            else if (this.isRandomShow && this.isRandomListShow) {
              var id = this.reportId || this.getCurrentDocumentId()
              this.exTxt.name = 'CVAuto_dataset_' + id + '.zip'
              window.open(this.server + '/download/cv/report/' + id)
            }
            else if (this.view == 'markdown' || this.view == 'output') {
              var suffix
              switch (this.language) {
                case CodeUtil.LANGUAGE_KOTLIN:
                  suffix = '.kt';
                  break;
                case CodeUtil.LANGUAGE_JAVA:
                  suffix = '.java';
                  break;
                case CodeUtil.LANGUAGE_C_SHARP:
                  suffix = '.cs';
                  break;

                case CodeUtil.LANGUAGE_SWIFT:
                  suffix = '.swift';
                  break;
//                case CodeUtil.LANGUAGE_OBJECTIVE_C:
//                  suffix = '.h';
//                  break;

                case CodeUtil.LANGUAGE_GO:
                  suffix = '.go';
                  break;
                case CodeUtil.LANGUAGE_C_PLUS_PLUS:
                  suffix = '.cpp';
                  break;

                case CodeUtil.LANGUAGE_TYPE_SCRIPT:
                  suffix = '.ts';
                  break;
                case CodeUtil.LANGUAGE_JAVA_SCRIPT:
                  suffix = '.js';
                  break;

                case CodeUtil.LANGUAGE_PHP:
                  suffix = '.php';
                  break;
                case CodeUtil.LANGUAGE_PYTHON:
                  suffix = '.py';
                  break;
                default:
                  suffix = '.java';
                  break;
              }

              this.exTxt.name = 'User' + suffix
              alert('自动生成模型代码，可填类名后缀:\n'
                + 'Kotlin.kt, Java.java, Swift.swift, C#.cs, Go.go, TypeScript.ts, '
                + '\nJavaScript.js, PHP.php, Python.py, C++.cpp');
            }
            else {
              this.exTxt.name = 'APIJSON测试 ' + this.getMethod() + ' ' + this.formatDateTime()
            }
          }
        }

        this.isExportShow = show
        this.isExportRemote = isRemote
      },

      // 显示配置弹窗
      showConfig: function (show, index) {
        this.isConfigShow = false
        if (this.isTestCaseShow) {
          if (index == 3 || index == 4 || index == 5 || index == 10 || index == 13 || index == 16) {
            this.showTestCase(false, false)
          }
        }

        if (show) {
          this.exTxt.button = '切换' // index == 8 ? '上传' : '切换'
          this.exTxt.index = index
          switch (index) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 6:
            case 7:
            case 8:
            case 15:
            case 16:
              this.exTxt.name = index == 0 ? this.database : (index == 1 ? this.schema : (index == 2 ? this.language
                  : (index == 3 ? this.host : (index == 6 ? this.server : (index == 8 ? this.projectHost.host : (index == 15 ? this.otherEnv
                   : (index == 16 ? (this.methods || []).join() : (this.types || []).join())))))))
              this.isConfigShow = true

              if (index == 0) {
                alert('可填数据库:\n' + CodeUtil.DATABASE_KEYS.join())
              }
              else if (index == 2) {
                alert('自动生成代码，可填语言:\nKotlin,Java,Swift,C#,Go,TypeScript,\nJavaScript,PHP,Python,C++')
              }
              else if (index == 16) {
                alert('多个方法用 , 隔开，可填方法: ' + HTTP_METHODS.join())
              }
              else if (index == 7) {
                alert('多个类型用 , 隔开，可填类型:\nPARAM(GET ?a=1&b=c&key=value),\nJSON(POST application/json),\nFORM(POST x-www-form-urlencoded),\nDATA(POST form-data),\nGRPC(POST application/json 需要 GRPC 服务开启反射)')
              }
              else if (index == 10) {
                vInput.value = App.getCache(baseUrl, 'request4MethodList') || '{'
                  + '\n    "mock": true,  // 生成模拟参数值'
                  + '\n    "package": "' + App.getPackage() + '",  // 包名，不填默认全部'
                  + '\n    "class": "' + App.getClass() + '"  // 类名，不填默认全部'
                  + '\n}'
                App.onChange(false)
                App.requestPost(false, App.exTxt.name, App.getRequest(vInput.value), App.getHeader(vHeader.value))
              }
              else if (index == 8) {
                alert('先在 App UI 录制回放管理页启动服务，然后复制地址填到这里，例如 http://192.168.12.345:8080')
              }
              break
            case 4:
              this.isHeaderShow = show
              this.saveCache('', 'isHeaderShow', show)
              break
            case 13:
              this.isScriptShow = show
              this.saveCache('', 'isScriptShow', show)
              this.listScript()
              break
            case 5:
              this.isRandomShow = show
              this.saveCache('', 'isRandomShow', show)
              break
            case 9:
              this.isDelegateEnabled = show
              this.saveCache('', 'isDelegateEnabled', show)
              break
            case 14:
              this.isEnvCompareEnabled = show
              this.saveCache('', 'isEnvCompareEnabled', show)

              // this.enableML(false)
              break
            case 10:
              this.isPreviewEnabled = show
              this.saveCache('', 'isPreviewEnabled', show)

              this.onChange(false)
              break
            case 17:
              this.isStatisticsEnabled = show
              this.saveCache('', 'isStatisticsEnabled', show)

              this.isTestCaseShow = false
              // this.resetTestCount(this.currentAccountIndex)

              this.remotes = null
              this.reportId = 0
              this.showTestCase(true, false)
              break
            case 12:
              this.isEncodeEnabled = show
              this.saveCache('', 'isEncodeEnabled', show)
              break
            case 11:
              var did = ((this.currentRemoteItem || {}).Flow || {}).id
              if (did == null) {
                alert('请先选择一个已上传的用例！')
                return
              }

              this.isEditResponse = show
              // this.saveCache('', 'isEditResponse', show)

              vInput.value = ((this.view != 'code' || StringUtil.isEmpty(this.jsoncon, true) ? null : this.jsoncon)
                || (this.currentRemoteItem.TestRecord || {}).response) || ''

              vHeader.value = (this.currentRemoteItem.TestRecord || {}).header || ''

              this.isTestCaseShow = false
              this.onChange(false)
              break
          }
        }
        else if (index == 3) {
          // var host = StringUtil.get(this.host)
          // var branch = StringUtil.get(vUrl.value)
          // this.host = ''
          // vUrl.value = host + branch //保证 showUrl 里拿到的 baseUrl = this.host (http://apijson.cn:8080/put /balance)
          // this.setBaseUrl() //保证自动化测试等拿到的 baseUrl 是最新的
          // this.showUrl(false, branch) //没必要导致必须重新获取 Response，this.onChange(false)
        }
        else if (index == 4) {
          this.isHeaderShow = show
          this.saveCache('', 'isHeaderShow', show)
        }
        else if (index == 13) {
          this.isScriptShow = show
          this.saveCache('', 'isScriptShow', show)
        }
        else if (index == 5) {
          this.isRandomShow = show
          this.saveCache('', 'isRandomShow', show)
        }
        else if (index == 9) {
          this.isDelegateEnabled = show
          this.saveCache('', 'isDelegateEnabled', show)
        }
        else if (index == 10) {
          this.isPreviewEnabled = show
          this.saveCache('', 'isPreviewEnabled', show)
          // vRequestMarkdown.innerHTML = ''
        }
        else if (index == 17) {
          this.isStatisticsEnabled = show
          this.saveCache('', 'isStatisticsEnabled', show)
        }
        else if (index == 14) {
          this.isEnvCompareEnabled = show
          this.saveCache('', 'isEnvCompareEnabled', show)
          this.enableML(this.isMLEnabled)
        }
        else if (index == 12) {
          this.isEncodeEnabled = show
          this.saveCache('', 'isEncodeEnabled', show)
        }
        else if (index == 11) {
          this.isEditResponse = show
          // this.saveCache('', 'isEditResponse', show)

          vInput.value = (this.currentRemoteItem.Flow || {}).request || ''
          vHeader.value = (this.currentRemoteItem.Flow || {}).header || ''

          this.isTestCaseShow = false
          this.onChange(false)
        }
      },

      // 显示删除弹窗
      showDelete: function (show, item, index, isRandom, isChainGroup) {
        this.isDeleteShow = show
        this.isDeleteRandom = isRandom
        this.isDeleteChainGroup = isChainGroup
        this.exTxt.name = '请输入' + (isRandom ? '随机配置' : (isChainGroup ? '分组' : '接口')) + '名来确认'
        if (isRandom) {
          this.currentRandomItem = Object.assign(item, {
            index: index
          })
        }
        else {
          this.currentDocItem = Object.assign(item, {
            index: index
          })
        }
      },

      // 删除接口文档
      deleteDoc: function () {
        var isDeleteRandom = this.isDeleteRandom
        var isDeleteChainGroup = this.isDeleteChainGroup
        var item = (isDeleteRandom ? this.currentRandomItem : this.currentDocItem) || {}
        var doc = (isDeleteRandom ? item.Input : (isDeleteChainGroup ? item.Chain : item.Flow)) || {}

        var type = isDeleteRandom ? '随机配置' : (isDeleteChainGroup ? '分组' : '用例')
        if ((isDeleteChainGroup && doc.groupId == null) || (isDeleteChainGroup != true && doc.id == null)) {
          alert('未选择' + type + '或' + type + '不存在！')
          return
        }
        if ((isDeleteChainGroup && doc.groupName != this.exTxt.name) || (isDeleteChainGroup != true && doc.name != this.exTxt.name)) {
          alert('输入的' + type + '名和要删除的' + type + '名不匹配！')
          return
        }

        this.showDelete(false, {})

        this.isTestCaseShow = false
        this.isRandomListShow = false
        var isChainShow = this.isChainShow

        var url = this.server + '/delete'
        var req = isDeleteRandom ? {
          format: false,
          'Input': {
            'id': doc.id
          },
          'tag': 'Input'
        } : (isDeleteChainGroup || isChainShow ? {
          format: false,
          'Chain': {
            'id': isDeleteChainGroup ? null : doc.id,
            'groupId': isDeleteChainGroup ? doc.groupId : null
          },
          'tag': isDeleteChainGroup ? 'Chain-group' : 'Chain'
        } : {
          format: false,
          'Flow': {
            'id': doc.id
          },
          'tag': 'Flow'
        })
        this.adminRequest(url, req, {}, function (url, res, err) {
          App.onResponse(url, res, err)

          var data = res.data || {}

          if (isDeleteRandom) {
            if (data.Input != null && JSONResponse.isSuccess(data.Input)) {
              if (((item.Input || {}).toId || 0) <= 0) {
                App.randoms.splice(item.index, 1)
              }
              else {
                App.randomSubs.splice(item.index, 1)
              }
              // App.showRandomList(true, App.currentRemoteItem)
            }
          }
          else if (isDeleteChainGroup) {
            App.chainGroups.splice(item.index, 1)
            App.selectChainGroup(App.currentChainGroupIndex, null)
          }
          else {
            if (data.Flow != null && JSONResponse.isSuccess(data.Flow)) {
              App.remotes.splice(item.index, 1)
              App.showTestCase(true, App.isLocalShow)
            }
          }
        })
      },

      // 保存当前的JSON
      save: function () {
        if (this.history.name.trim() === '') {
          Helper.alert('名称不能为空！', 'danger')
          return
        }
        var val = {
          name: this.history.name,
          detail: this.history.name,
          type: this.type,
          package: this.getPackage(),
          class: this.getClass(),
          method: this.getMethod(),
          url: '/' + this.getMethod(),
          request: inputted,
          response: this.jsoncon,
          header: vHeader.value,
          random: vRandom.value,
          scripts: this.scripts
        }
        var key = String(Date.now())
        localforage.setItem(key, val, function (err, value) {
          Helper.alert('保存成功！', 'success')
          App.showSave(false)
          val.key = key
          App.historys.push(val)
        })
      },

      // 清空本地历史
      clearLocal: function () {
        this.locals.splice(0, this.locals.length) //UI无反应 this.locals = []
        this.saveCache('', 'locals', [])
      },

      // 删除已保存的
      remove: function (item, index, isRemote, isRandom, isProject, isChainGroup) {
        if (isRemote == null || isRemote == false) { //null != false
          if (isProject) {
            this.projectHosts.splice(index, 1)
            this.saveCache('', 'projectHosts', this.projectHosts)
            return
          }

          localforage.removeItem(item.key, function () {
            App.historys.splice(index, 1)
          })
        } else {
          if (this.isLocalShow) {
            this.locals.splice(index, 1)
            this.saveCache('', 'locals', this.locals)
            return
          }

          if (isRandom && (((item || {}).Input || {}).id || 0) <= 0) {
            this.randomSubs.splice(index, 1)
            return
          }

          this.showDelete(true, item, index, isRandom, isChainGroup)
        }
      },

      // 根据事件配置用例恢复数据
      restoreRandom: function (index, item, isSub) {
        if (this.isRandomSubListShow) {
          this.currentRandomSubIndex = index
        } else if (this.isRandomListShow) {
          this.currentRandomIndex = index
          this.currentRandomItem = item
          this.currentRandomSubIndex = -1
        }
        this.isRandomListShow = false
        this.isRandomSubListShow = false

        var random = (isSub ? (item || {}).Random : (item || {}).Input) || {}
        this.randomTestTitle = random.name
        this.testRandomCount = random.count
        vRandom.value = StringUtil.get(random.config)

        if (isSub) {
          return
        }

        var response = ((item || {}).TestRecord || {}).response
        if (StringUtil.isNotEmpty(response)) {
            this.jsoncon = StringUtil.trim(response)
            this.view = 'code'
        }

        var isHttp = random.type == InputUtil.EVENT_TYPE_HTTP
        if (isHttp && StringUtil.isEmpty(this.prevOperate)) {
          this.prevOperate = (this.operate != OPERATE_TYPE_HTTP ? this.operate : (StringUtil.isEmpty(this.randoms) ? OPERATE_TYPE_RECORD : OPERATE_TYPE_REPLAY))
        }
        this.operate = isHttp ? OPERATE_TYPE_HTTP : this.prevOperate || this.operate

        var currentResponse = parseJSON(response, {}, true)
        var imgUrl = StringUtil.trim(JSONResponse.isObject(currentResponse) ? currentResponse.screenshot : null)
        vAfter.src = StringUtil.isEmpty(imgUrl) ? vAfter.src : (imgUrl.indexOf('://') >= 0 ? '' : baseUrl) + '/download?filePath=' + encodeURI(imgUrl)

        var beforeUrl = StringUtil.trim(((item || {}).TestRecord || {}).screenshot)
        vBefore.src = StringUtil.isEmpty(beforeUrl) ? vBefore.src : (beforeUrl.indexOf('://') >= 0 ? '' : App.server) + '/download?filePath=' + encodeURI(beforeUrl)

        this.method = random.method
        var format = random.format
        this.type = HTTP_CONTENT_TYPES.includes(format) ? format : REQUEST_TYPE_PARAM
        vUrl.value = isHttp ? random.url || vUrl.value : vUrl.value
        vInput.value = isHttp ? random.request || vInput.value : vInput.value

        this.onChange(false)
      },
      // 根据测试用例/历史记录恢复数据
      restoreRemoteAndTest: function (index, item) {
        this.restoreRemote(index, item, true, true)
      },
      // 根据测试用例/历史记录恢复数据
      restoreRemote: function (index, item, test, showRandom) {
        if (this.currentDocIndex != index) {
          this.currentDocIndex = index
          this.currentRandomIndex = -1
          this.hoverIds = {}
          this.visiblePaths = []
          this.missTruth = {}
          this.sameIds = []
          this.reportId = null
        }

        this.currentRemoteItem = item
        if (showRandom != null) {
           this.isRandomShow = showRandom
           this.isRandomListShow = showRandom
        }
        this.restore(item, ((item || {}).Flow || {}).log, true, test) // FIXME Output.log
      },
      // 根据历史恢复数据
      restore: function (item, response, isRemote, test, isHttp) {
        this.isEditResponse = false
        this.operate = isHttp ? OPERATE_TYPE_HTTP : OPERATE_TYPE_REPLAY

        item = item || {}
        var doc = item.Flow || item
        var device = item.Device || {}
        var system = item.System || {}
        var docId = doc.id || 0

        var scripts = item.scripts
        if (isRemote) {
            this.randoms = []
            if (this.operate != OPERATE_TYPE_RECORD) {
              this.showRandomList(true, doc)
            }

            if (doc.logUrl != null && doc.logUrl.indexOf('://') > 0) {
              // App.request(false, REQUEST_TYPE_PARAM, item.logUrl, null, {'Accept:': 'text/plain;charset=UTF-8'}, function (url, res, err) {
              //   output = res.data || ''
              //   vOutput.value = output
              //   App.view = 'output'
              // })

              axios({
                url: (this.isDelegateEnabled ? this.server + '/delegate?$_delegate_url=' : '') + StringUtil.noBlank(item.logUrl),
                method: 'GET',
                responseType: 'text', // important
                withCredentials: true,
                header: {
                  'Accept': 'text/plain;charset=UFT8'
                //   'Content-Type': 'text/plain;charset=GBK'
                }
              }).then(function(res) {
                  output = res.data || ''
                  vOutput.value = output
                  App.view = 'output'
              }).catch(function(err) {
                  App.onResponse(item.logUrl, {}, err)
              });
            }

        
          var originItem = item
          item.random = (originItem.Input || originItem.Random || {}).config

          doc = item.Flow || {}
          docId = doc.id || 0

          var pre = Object.assign({
            'script': ''
          }, item['Script:pre'] || {})
          var post = Object.assign({
            'script': ''
          }, item['Script:post'] || {})

          var preId = pre.id
          var postId = post.id
          if (docId > 0 && (preId == null || postId == null)) {
            // var accountId = this.getCurrentAccountId();
            const cri = this.currentRemoteItem || {}
            const chain = cri.Chain || {}
            const cId = chain.id || 0

            this.adminRequest('/get', {
              'Script:pre': preId != null ? undefined : {
                'ahead': 1,
                // 'testAccountId': 0,
                'chainId': cId,
                'documentId': docId,
                '@order': 'date-'
              },
              'Script:post': postId != null ? undefined : {
                'ahead': 0,
                // 'testAccountId': 0,
                'chainId': cId,
                'documentId': docId,
                '@order': 'date-'
              }
            }, {}, function (url, res, err) {
              var data = res.data
              if (JSONResponse.isSuccess(data) != true) {
                App.log(err != null ? err : (data == null ? '' : data.msg))
                return
              }

              // var scripts = item.scripts || {}
              var scripts = originItem.scripts || {}
              // var ss = scripts.case
              // if (ss == null) {
              //   scripts.case = ss = {}
              // }

              // var bs = ss[docId]
              // if (bs == null) {
              //   ss[docId] = bs = {}
              // }

              var bs = scripts

              var pre = data['Script:pre']
              if (pre != null && pre.script != null) {
                bs.pre = originItem['Script:pre'] = data['Script:pre']
              }

              var post = data['Script:post']
              if (post != null && post.script != null) {
                bs.post = originItem['Script:post'] = data['Script:post']
              }

              originItem.scripts = scripts

              App.changeScriptType(App.scriptType)
              App.scripts.case[docId] = scripts
            })
          }

          if (scripts == null) {
            scripts = {
              pre: pre,
              post: post
            }
          }
          item.scripts = scripts

          // item.brand = device.brand
          // item.model = device.model
          // item.width = device.width
          // item.height = device.height
          item.urlComment = StringUtil.trim(StringUtil.trim(device.brand) + ' ' + StringUtil.trim(device.model)) + ' ' + (device.width || 0) + 'x' + (device.height || 0) + ' ' + StringUtil.trim(StringUtil.trim(system.brand) + ' ' + StringUtil.trim(system.versionName))
          // item = doc
          this.scripts.case[docId] = scripts
        }
        else {
          this.scripts = scripts
        }

        // localforage.getItem(item.key || '', function (err, value) {
        //   var branch = StringUtil.get(item.url || '/get')
        //   if (branch.startsWith('/') == false) {
        //     branch = '/' + branch
        //   }

          this.method = item.method;
          this.type = item.type;
          this.urlComment = item.urlComment || StringUtil.trim(StringUtil.trim(device.brand) + ' ' + StringUtil.trim(device.model)) + ' ' + (device.width || 0) + 'x' + (device.height || 0) + ' ' + StringUtil.trim(StringUtil.trim(system.brand) + ' ' + StringUtil.trim(system.versionName))
          this.requestVersion = item.versionCode;
          // this.showUrl(false, branch)

          vUrl.value = StringUtil.get(item.name || doc.name)
          vInput.value = StringUtil.get(item.request || doc.request)
          vHeader.value = StringUtil.get(item.header || doc.header)
          vRandom.value = StringUtil.get(item.random || doc.random)
          this.showTestCase(false, this.isLocalShow)
          this.changeScriptType(this.scriptType)

          this.onChange(false)

          if (isRemote) {
            this.randoms = []
            this.showRandomList(this.isRandomListShow, doc || item)
          }

          if (test) {
            this.send(false)
          }
          else {
            if (StringUtil.isEmpty(response, true) == false) {
              setTimeout(function () {
                App.jsoncon = StringUtil.trim(response)
                App.view = 'code'
              }, 500)
            }
          }

        // })
      },

      onClickHost: function(index, item) {
        this.projectHost = item = item || {host: baseUrl}
        this.isTestCaseShow = false
        baseUrl = item.host || baseUrl

        // this.host = ''
        // var bu = this.getBranchUrl()

        // vUrl.value = item.host + bu
        // this.showUrl(false, bu)

        this.saveCache('', 'URL_BASE', baseUrl)
        this.saveCache('', 'projectHost', this.projectHost)
      },

      listProjectHost: function() {
        var req = {
            'TestRecord[]': {
                'count': 0,
                'TestRecord': {
                    '@column': 'DISTINCT host,project',
                    '@from@': {
                        'join': '&/Flow',
                        'TestRecord': {
                            '@column': 'host,documentId',
                            '@group': 'host,documentId',
                            'host{}': 'length(host)>2'
                        },
                        'Flow': {
                            'id@': '/TestRecord/documentId',
                            '@column': "ifnull(project,''):project",
                            '@group': 'project',
//                            'project{}': 'length(project)>0'
                        }
                    }
                }
            }
        }

        this.adminRequest('/get', req, {}, function (url, res, err) {
          var data = res.data
          if (JSONResponse.isSuccess(data) != true) {
            App.log(err != null ? err : (data == null ? '' : data.msg))
            return
          }

          var projectHosts = App.getCache('', 'projectHosts', [])
          var list = data['TestRecord[]'] || []
//          var phs = []
//          for (var i = 0; i < list.length; i ++) {
//            var item = list[i] || {}
//            var host = (item.TestRecord || {}).host
//            if (StringUtil.isEmpty(host, true)) {
//              continue
//            }
//
//            phs.push({ 'host': host, 'project': (item.Flow || {}).project })
//          }

          App.projectHosts = projectHosts.concat(list)
        })
      },

      syncProjectHost: function(index, item) {
        var project = item == null ? null : item.project

        var list = this.testCases
        var count = list == null ? 0 : list.length
        if (count <= 0) {
          alert('没有可操作的用例！请先查询用例，保证有至少一个显示！')
          return
        }

        var ids = []
        var rawProjects = [null, '']
        for (var i = 0; i < count; i ++) {
           var item = list[i]
           var doc = item == null ? null : item.Flow
           var id = doc == null ? null : doc.id
           if (id == null || id <= 0) {
             continue
           }

           ids.push(id)
          if (! rawProjects.includes(doc.project)) {
            rawProjects.push(doc.project)
          }
        }

        var req = {
          'Flow': {
            'id{}': ids,
            'project{}': rawProjects,
            'project': project || ''
          },
          'tag': 'Flow-project[]'
        }

        this.adminRequest('/put', req, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var data = res.data
          if (JSONResponse.isSuccess(data)) {
             App.listProjectHost()
          }
        })
      },

      // 获取所有保存的json
      listHistory: function () {
        localforage.iterate(function (value, key, iterationNumber) {
          if (key[0] !== '#') {
            value.key = key
            App.historys.push(value)
          }
          if (key === '#theme') {
            // 设置默认主题
            App.checkedTheme = value
          }
        })
      },

      // 导出文本
      exportTxt: function (btnIndex) {
        if (btnIndex == null) {
          btnIndex = 0
        }

        if (btnIndex == 1 && this.isExportRandom != true) {
          this.shareLink(this.isRandomTest)
          return
        }

        this.isExportShow = false

        if (this.isExportRemote == false) { //下载到本地

          if (this.isTestCaseShow) { //文档
            saveTextAs('# ' + this.exTxt.name + '\n主页: https://github.com/Tencent/APIJSON'
              + '\n\nBASE_URL: ' + this.getBaseUrl()
              + '\n\n\n## 测试用例(Markdown格式，可用工具预览) \n\n' + this.getDoc4TestCase()
              + '\n\n\n\n\n\n\n\n## 文档(Markdown格式，可用工具预览) \n\n' + doc
              , this.exTxt.name + '.txt')
          }
          else if (this.isRandomShow && this.isRandomListShow) {
            var id = this.reportId || this.getCurrentDocumentId()
            window.open(this.server + '/download/dataset/' + id + "?ratio=20")
          }
          else if (this.view == 'markdown' || this.view == 'output') { //model
            var clazz = StringUtil.trim(this.exTxt.name)

            var txt = '' //配合下面 +=，实现注释判断，一次全生成，方便测试
            if (clazz.endsWith('.py')) {
              txt += CodeUtil.parsePythonEntity(docObj, clazz.substring(0, clazz.length - 3), this.database)
            }
            else {
              alert('请正确输入对应语言的类名后缀！')
            }

            if (StringUtil.isEmpty(txt, true)) {
              alert('找不到 ' + clazz + ' 对应的表！请检查数据库中是否存在！\n如果不存在，请重新输入存在的表；\n如果存在，请刷新网页后重试。')
              return
            }
            saveTextAs(txt, clazz)
          }
          else {
            var res = parseJSON(this.jsoncon)
            res = this.removeDebugInfo(res)

            var s = ''
            switch (this.language) {
              case CodeUtil.LANGUAGE_PYTHON:
                var isML = this.isMLEnabled
                var tr = (this.currentRemoteItem || {}).TestRecord || {}
                var stddObj = isML ? JSONResponse.updateFullStandard(parseJSON(tr.standard), res, isML) : null
                var resObj = isML ? stddObj : (res || parseJSON(tr.response))
                s += '(Python):\n\n' + CodeUtil.parsePythonResponse('', resObj, 0, ! isSingle, isML)
                break;
              default:
                s += ':\n没有生成代码，可能生成代码(封装,解析)的语言配置错误。 \n';
                break;
            }

            saveTextAs('# ' + this.exTxt.name + '\n主页: https://github.com/Tencent/APIJSON'
              + '\n\n\nURL: ' + StringUtil.get(vUrl.value)
              + '\n\n\nHeader:\n' + StringUtil.get(vHeader.value)
              + '\n\n\nRequest:\n' + StringUtil.get(vInput.value)
              + '\n\n\nResponse:\n' + StringUtil.get(this.jsoncon)
              + '\n\n\n## 解析 Response 的代码' + s
              , this.exTxt.name + '.txt')
          }
        }
        else { //上传到远程服务器
          var id = this.User == null ? null : this.User.id
          if (id == null || id <= 0) {
            alert('请先登录！')
            return
          }

          const project = (this.projectHost || {}).project

          const isExportRandom = this.isExportRandom
          const isExportScript = this.isExportScript

          const cri = this.currentRemoteItem || {}
          const chain = cri.Chain || {}
          const currentAccountId = this.getCurrentAccountId()
          const doc = cri.Flow || {}
          const tr = cri.TestRecord || {}
          const cgId = chain.groupId || 0
          const cId = chain.id || 0
          const did = isExportRandom && btnIndex == 1 ? null : doc.id

          if (isExportScript) {
            const extName = this.exTxt.name;
            const scriptType = this.scriptType
            const script = ((this.scripts[scriptType] || {})[this.getCurrentScriptBelongId()] || {})[this.isPreScript ? 'pre' : 'post'] || {};
            const sid = script.id
            const url = sid == null ? '/post' : '/put'
            const req = {
              format: false,
              'Script': Object.assign({
                'id': sid == null ? undefined : sid,
                'simple': 1,
                'ahead': this.isPreScript ? 1 : 0,
                'chainGroupId': cgId,
                'chainId': cId,
                'documentId': did == null || scriptType != 'case' ? 0 : did,
                'testAccountId': scriptType != 'account' ? 0 : currentAccountId,
                'name': extName,
                'script': vScript.value
              }, script),
              'tag': 'Script'
            }

            this.adminRequest(url, req, {}, function (url, res, err) {
              App.onResponse(url, res, err)

              var data = res.data || {}
              var isPut = url.indexOf('/put') >= 0
              var ok = JSONResponse.isSuccess(data)
              alert((isPut ? '修改' : '上传') + (ok ? '成功' : '失败！\n' + StringUtil.get(err != null ? err.message : data.msg)))

              if (ok && ! isPut) {
                script.id = (data.Script || {}).id
              }
            })

            return
          }

          const isEditResponse = this.isEditResponse

          const path = this.getMethod();

          if ((isExportRandom != true || btnIndex == 1) && StringUtil.isEmpty(this.exTxt.name, true)) {
            alert('请输入接口名！')
            return
          }

          if (isExportRandom && btnIndex <= 0 && did == null) {
            alert('请先上传测试用例！')
            return
          }

          this.isTestCaseShow = false
          const isRandomSubListShow = this.isRandomSubListShow
          
          const currentResponse = this.view != 'code' || StringUtil.isEmpty(this.jsoncon, true) ? {} : this.removeDebugInfo(parseJSON(this.jsoncon));

          const after = isSingle ? this.switchQuote(inputted) : inputted;  // this.toDoubleJSON(inputted);
          const inputObj = this.getRequest(after, {});

          var commentObj = null;
          if (isExportRandom != true) {
            var commentStddObj = null
            try {
              commentStddObj = parseJSON(isEditResponse ? tr.standard : doc.standard);
            }
            catch(e) {
              log(e)
            }

            var code_ = inputObj[JSONResponse.KEY_CODE]
            if (isEditResponse) {
              inputObj[JSONResponse.KEY_CODE] = typeof code_ == 'undefined' ? code_ : null  // delete inputObj.code
            }

            commentObj = JSONResponse.updateStandard(commentStddObj, inputObj);
            CodeUtil.parseComment(after, docObj == null ? null : docObj['[]'], path, this.database, this.language, isEditResponse != true, commentObj, true);

            if (isEditResponse) {
              inputObj[JSONResponse.KEY_CODE] = code_
            }
          } else if (this.isRandomShow && this.isRandomListShow) {
            this.exportChainApiCase(this.exTxt.name, isRandomSubListShow, isRandomSubListShow ? this.randomSubs : this.randoms)
            return
          }

          var rawRspStr = JSON.stringify(currentResponse || {})
          var rsp = currentResponse // parseJSON(rawRspStr)
          var stddObj = isML ? JSONResponse.updateFullStandard({}, rsp, isML) : {};

          const subIndex = this.currentRandomSubIndex
          const userId = this.User.id;
          const extName = this.exTxt.name;
          const isSub = subIndex != null && subIndex >= 0
          const input = ((isSub ? this.randomSubs[this.currentRandomSubIndex] : this.currentRandomItem) || {}).Input || {}
          const config = vRandom.value;
          const methods = this.methods;
          const method = this.isShowMethod() ? this.method : null;
          const baseUrl = this.getBaseUrl();
          const bbox = this.currentBbox || {}
          if (isSub) {
            var isReq = this.isEditReqLink
            var key = isReq ? 'reqLinkConfigs' : 'resLinkConfigs'
            var links = bbox[key] || {}
            links[extName] = config
          }

          var callback = function (randomName, constConfig, constJson) {
            const randomId = input.id || 0;
            var isPost = isExportRandom && isSub ? randomId <= 0 : (randomId <= 0 || ! isExportRandom) && (isEditResponse || did == null)
            const url = isPost ? '/post' : '/put';
            const table = isSub ? 'Random' : 'Input'
            const req = isExportRandom && btnIndex <= 0 ? {
              format: false,
              [table]: {
                // userId: userId,
                id: randomId <= 0 ? undefined : randomId,
                isRes: isSub ? (App.isEditReqLink ? 0 : 1) : null,
                toId: isSub ? ((App.currentRandomItem || {}).Input || {}).id : 0,
                chainGroupId: cgId,
                chainId: cId,
                flowId: isSub ? undefined : did,
                documentId: isSub ? did : undefined,
                count: isSub ? 1 : App.requestCount,
                name: extName,
                path: bbox.assertPath || bbox.viewPath,
                config: config
              },
              'TestRecord': isPost ? {
                // 'userId': userId,
                'documentId': did,
                'host': StringUtil.isEmpty(baseUrl, true) ? null : baseUrl,
                'chainGroupId': cgId,
                'chainId': cId,
                'response': rawRspStr,
                'standard': isML ? JSON.stringify(stddObj) : null
              } : null,
              'tag': table
            } : {
              format: false,
              'Flow': {
                'project': project,
                'name': App.getMethod(),
                  // 'userId': userId,
  //                'testAccountId': currentAccountId,
  //                'chainGroupId': cgId,
                'detail': extName,
                'systemId': 1,
                  // 'userId': userId,
  //                'chainGroupId': cgId,
                'deviceId': 1,
                'imei': 1234,
                'img': "http://test.url",
                'log': (currentResponse.type || App.type) || null,
                 // 没必要，直接都在请求中说明，查看也方便 'detail': (isEditResponse ? App.getExtraComment() : null) || ((App.currentRemoteItem || {}).TestRecord || {}).detail,
              },
              'tag': 'Flow'
            }

            App.adminRequest(url, req, {}, function (url, res, err) {
              App.onResponse(url, res, err)

              var data = res.data || {}

              if (isExportRandom && btnIndex <= 0) {
                if (JSONResponse.isSuccess(data)) {
                  input.config = config
                  App.randoms = isPost ? [] : App.randoms
                  App.showRandomList(true, (App.currentRemoteItem || {}).Flow)
                }
              }
              else {
                if (JSONResponse.isSuccess(data) != true) {
                  if (! isPost) {  // 修改失败就转为新增
                    App.currentRemoteItem = null;
                    alert('修改失败，请重试(自动转为新增)！' + StringUtil.trim(data.msg))
                  }
                }
                else {
                  App.remotes = []
                  App.showTestCase(true, false)

                  if (isPut) {  // 修改失败就转为新增
                    alert('修改成功')
                    return
                  }

                  //自动生成随机配置（遍历 JSON，对所有可变值生成配置，排除 @key, key@, key() 等固定值）
                  const isGenerate = StringUtil.isEmpty(config, true);
                  var req = isGenerate != true ? null : App.getRequest(vInput.value, {})
                  if (StringUtil.isEmpty(req)) {
                    return
                  }
                  App.newAndUploadRandomConfig(baseUrl, req, (data.Flow || {}).id, config, App.requestCount, function (url, res, err) {
                    if (res.data != null && res.data.Input != null && JSONResponse.isSuccess(res.data.Input)) {
                      if (StringUtil.isNotEmpty(config)) {
                        alert('已' + (isGenerate ? '自动生成并' : '') + '上传随机配置:\n' + config)
                      }
                      App.isRandomListShow = true
                    } else {
                      if (StringUtil.isNotEmpty(config)) {
                        alert((isGenerate ? '已自动生成，但' : '') + '上传以下随机配置失败:\n' + config)
                      }
                      vRandom.value = config
                    }
                    App.onResponse(url, res, err)
                  })
                }
              }
            })
          };

          if (btnIndex == 1) {
            // this.parseRandom(inputObj, header, config, null, true, true, false, callback)
            callback(null, null, inputObj)
          }
          else {
            callback(null, null, inputObj)
          }

        }
      },

      exportChainApiCase: function (groupName, isSub, randoms) {
        groupName = StringUtil.isEmpty(groupName) ? this.exTxt.name : groupName
        isSub = isSub == null ? this.isRandomSubListShow : isSub
        randoms = randoms != null ? randoms : (isSub ? this.randomSubs : this.randoms)
        var isAdd = true // TODO 暂时只添加，后续支持更新
        var groupId = new Date().getTime()
        //修改 Chain
        this.adminRequest((isAdd ? '/post' : '/put'), {
          Chain: {
            'groupName': groupName,
            'groupId': isAdd ? groupId : null,
            'groupId{}': isAdd ? null : [groupId]
          },
          tag: 'Chain-group'
        }, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var data = res.data || {}
          var isOk = JSONResponse.isSuccess(data)

          var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get(data.msg))
          if (err != null) {
            msg += '\nerr: ' + err.msg
          }

          if (! isOk) {
            alert((isAdd ? '新增' : '修改') + (isOk ? '成功' : '失败') + (isAdd ? '! \n' : '！\ngroupId: ' + groupId) + '\ngroupName: ' + groupName + '\n' + msg)
            return
          }

          var chain = data.Chain || {}
          App.addApiCase2Chain({id: chain.id, groupId: chain.groupId || groupId, name: chain.name || groupName}, randoms, 0, [])
        })

      },

      addApiCase2Chain: function (group, list, index, chains, preIndex, preChain) {
        const groupId = group == null ? null : group.groupId || group.id
        if (groupId == null || groupId <= 0) {
          alert('请选择有效的用例！')
          return
        }

        const groupName = group.name
        if (index >= list.length) {
          alert('已完成导出场景接口用例：' + groupName + ' \nid: ' + groupId)
          var settingStr = encodeURIComponent(JSON.stringify({
            isChainShow: true, chainGroupPage: 0, chainGroupCount: 10, testCaseCount: 10, chainGroupSearch: groupName
          }))
          window.open(this.server + '/api?reportId=0&setting=' + settingStr)
          window.open(this.server + '/api/index.html?reportId=0&setting=' + settingStr)
          return
        }

        const item = list[index]
        const input = item == null ? null : item.Input
        const inputUrl = input == null ? null : input.url
        var bizUrl_ = this.getBranchUrl(inputUrl || '');
        var locate_ = ''
        var qry_ = input == null ? null : StringUtil.trim(input.query)
        var query_ = ''
        if (JSONResponse.isString(bizUrl_)) {
          var ind = bizUrl_.indexOf('?')
          query_ = ind < 0 ? '' : bizUrl_.substring(ind + 1)
          bizUrl_ = ind < 0 ? bizUrl_ : bizUrl_.substring(0, ind)

          ind = bizUrl_.indexOf('#')
          locate_ = ind < 0 ? '' : bizUrl_.substring(ind + 1)
          bizUrl_ = ind < 0 ? bizUrl_ : bizUrl_.substring(0, ind)

          if (bizUrl_.endsWith('/')) {
            bizUrl_ = bizUrl_.substring(0, bizUrl_.length - 1);
          }
          if (! bizUrl_.startsWith('/')) {
            bizUrl_ = '/' + bizUrl_;
          }
        }

        const bizUrl = bizUrl_;
        if (! StringUtil.isPath(bizUrl, true)) {
          this.addApiCase2Chain(group, list, index + 1, chains, preIndex, preChain)
          return
        }

        const currentResponse = parseJSON(input.response);
        if (StringUtil.isEmpty(currentResponse, true)) {
          this.addApiCase2Chain(group, list, index + 1, chains, preIndex, preChain)
          return
        }

        chains = chains || []

        const projectHost = this.projectHost || {}
        const project = StringUtil.isEmpty(projectHost.project) ? null : projectHost.project

        var accountInfo = this.getCurrentAccount() || {}
        var currentAccountId = accountInfo.id
        var account = accountInfo.account || accountInfo.phone || accountInfo.email

        const baseUrl = this.getBaseUrl(inputUrl || '', true) || input.host;
        const query = StringUtil.isEmpty(query_) ? qry_ : (StringUtil.isEmpty(qry_) ? query_ : query_ + '&' + qry_);
        const request_ = input.request
        var inputObj_;
        try {
          inputObj_ = this.getRequest(request_, {});
        } catch (e) {
          inputObj_ = getRequestFromURL('?' + request_, true)
        }
        const queryObj = getRequestFromURL('?' + query, true)
        if (StringUtil.isEmpty(inputObj_) && StringUtil.isNotEmpty(queryObj)) {
          inputObj_ = queryObj
        }

        const inputObj = inputObj_
        const rawInputStr = StringUtil.trim(inputObj)
        var nowStr = this.formatDateTime();
        const userId = this.User.id
        var accountInfo = this.getCurrentAccount() || {}
        var currentAccountId = accountInfo.id

        const method = HTTP_METHODS.indexOf(input.method) >= 0 ? input.method : null;
        const format = input.format;
        const type = HTTP_CONTENT_TYPES.indexOf(format) >= 0 ? format : (HTTP_JSON_TYPES.indexOf(method) >= 0
            ? "JSON" : (HTTP_DATA_TYPES.indexOf(format) >= 0 ? "DATA" : (HTTP_URL_ARG_TYPES.indexOf(method) >= 0
                    ? "PARAM" : (HTTP_FORM_TYPES.indexOf(format) >= 0 ? "FORM" : null))
        ))

        var commentObj = null;
        // if (isExportRandom != true) {
          var commentStddObj = null
          try {
            commentStddObj = {} // parseJSON(isEditResponse ? tr.standard : doc.standard);
          }
          catch(e) {
            log(e)
          }

          // var code_ = JSONResponse.isObject(inputObj) ? inputObj[JSONResponse.KEY_CODE] : null
          // if (isEditResponse) {
          //   inputObj[JSONResponse.KEY_CODE] = typeof code_ == 'undefined' ? undefined : null  // delete inputObj.code
          // }

          commentObj = JSONResponse.updateStandard(commentStddObj, inputObj);

          // if (isEditResponse) {
          //   inputObj[JSONResponse.KEY_CODE] = code_
          // }
        // }

        var rawRspStr = StringUtil.trim(currentResponse)
        var isResObj = JSONResponse.isObject(currentResponse);
        const code = isResObj ? currentResponse[JSONResponse.KEY_CODE] : undefined;
        const thrw = isResObj ? currentResponse.throw : null;
        if (isResObj) {
          currentResponse[JSONResponse.KEY_CODE] = typeof code == 'undefined' ? undefined : null; // delete currentResponse.code; // currentResponse.code = null; //code必须一致
          delete currentResponse.throw; // currentResponse.throw = null; // throw必须一致
        }

        const isML = this.isMLEnabled;
        const stddObj = isML ? JSONResponse.updateStandard({}, currentResponse) : {};
        stddObj.status = StringUtil.isNumber(input.status) ? +input.status : 200;
        stddObj.code = code || 0;
        stddObj.throw = thrw;
        if (isResObj) {
          currentResponse[JSONResponse.KEY_CODE] = code;
          currentResponse.throw = thrw;
        }

        var config = input.config;
        var cgId = groupId; // FIXME 已有 Document，需要修改
        var cId = null; // FIXME 已有 Document，需要修改
        const reqObj = inputObj

        const curChain = {
          groupId: groupId,
          groupName: groupName,
          testAccountId: currentAccountId,
          account: account,
          method: method,
          type: type,
          host: baseUrl,
          url: bizUrl,
          request: inputObj,
          response: currentResponse
        }

        var callback = function (randomName, constConfig, constJson, doc, req_, url, res, err) {
          var did = doc == null ? null : doc.id
          if (did == null || did <= 0) {
            App.addApiCase2Chain(group, list, index + 1, chains, preIndex, preChain)
            alert('新增 Document 单接口用例失败！' + data.msg + '; ' + (err || {}).message + '; req = \n' + JSON.stringify(req))
            return
          }

          const isAdd = true

          const chainReq = {
            'rank': nowStr,
            'groupName': groupName,
            // 'userId': userId,
            'groupId': groupId,
            'documentId': did,
            'documentName': doc.name
          }
          const randomReq = {
            // userId: userId,
            toId: 0,
            chainGroupId: cgId,
            chainId: cId,
            documentId: did,
            count: 1,
            name: '[Record] 参数传递 ' + nowStr,
            config: constConfig
          }

          curChain.documentId = chainReq.documentId
          curChain.documentName = chainReq.documentName
          curChain.randomName = randomReq.name
          // curChain.Chain = chainReq
          // curChain.Random = randomReq

          const req = {
            format: false,
            'Chain': chainReq,
            'Random': StringUtil.isEmpty(constConfig) ? null : randomReq,
            'tag': StringUtil.isEmpty(constConfig) ? 'Chain' : 'Chain+Random'
          }

          App.adminRequest('/post', req, {}, function (url, res, err) {
            App.onResponse(url, res, err)
            var data = res.data || {}
            var isOk = JSONResponse.isSuccess(data)
            if (isOk != true) {
              alert((isAdd ? '新增 Chain ' : '修改 Chain ') + (isOk ? '成功' : '失败') + (isAdd ? '! \n' :'！\ngroupId: ' + groupId) + '\ngroupName: ' + groupName + '\n' + data.msg)
            }
            else if (StringUtil.isNotEmpty(chains)) {
              const chain = data.Chain || {}
              const random = data.Random || {}
              curChain.chainId = chainReq.id = chain.id
              curChain.randomId = randomReq.id = random.id

              const docReqObj = parseJSON(doc.request, {}, true)
              const reqObj = JSONResponse.deepMerge(docReqObj, JSONResponse.deepMerge(queryObj, inputObj))

              //自动生成随机配置（遍历 JSON，对所有可变值生成配置，排除 @key, key@, key() 等固定值）
              const isGenerate = StringUtil.isNotEmpty(reqObj);
              chain.request = reqObj
              App.newAndUploadRandomConfig(baseUrl, reqObj, chain.documentId || doc.id, config, 1, function (url, res, err) {
                if (res.data != null && res.data.Random != null && JSONResponse.isSuccess(res.data.Random)) {
                  console.log('已' + (isGenerate ? '自动生成并' : '') + '上传随机配置:\n' + config)
                  App.isRandomListShow = true
                } else {
                  console.log((isGenerate ? '已自动生成，但' : '') + '上传以下随机配置失败:\n' + config)
                  vRandom.value = config
                }
                App.onResponse(url, res, err)
              }, true, chain.id, chain.groupId || groupId, bizUrl, chains)
            }

            chains.push(curChain)
            App.addApiCase2Chain(group, list, index + 1, chains, index, curChain)
          })

        };

        this.adminRequest('/get', {
          Document: {
            'method': input.method,
            'url': bizUrl,
          }
        }, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var data = res.data
          var doc = data == null ? null : data.Document
          var did = doc == null ? null : doc.id
          var isAdd = did == null || did <= 0
          if (! isAdd) {
            // curChain.Document = doc
            callback(null, config, inputObj, doc, {}, url, res, err)
            return
          }

          const docReq = {
            'userId': userId,
            'project': StringUtil.isEmpty(projectHost.project, true) ? null : projectHost.project,
            'operation': CodeUtil.getOperation(bizUrl, reqObj),
            'name': '[Record] ' + index + '. ' + groupName + ' ' + nowStr,
            'method': method,
            'type': type,
            'url': bizUrl,
            'request': StringUtil.trim(rawInputStr),
            'standard': commentObj == null ? null : JSON.stringify(commentObj, null, '    '),
            'header': input.reqHeader || input.header
          }
          const trReq = {
            'userId': userId,
            'chainGroupId': cgId,
            'chainId': cId,
            // 'documentId': did,
            'randomId': 0,
            'host': baseUrl,
            'testAccountId': currentAccountId,
            'response': rawRspStr,
            'header': input.resHeader || input.header,
            'standard': isML ? JSON.stringify(stddObj) : undefined,
          }

          curChain.method = method
          curChain.type = type
          curChain.host = baseUrl
          curChain.url = bizUrl
          // curChain.Document = docReq
          // curChain.TestRecord = trReq
          const req = {
            format: false,
            'Document': docReq,
            'TestRecord': trReq,
            'tag': 'Document'
          }

          App.adminRequest('/post', req, {}, function (url, res, err) {
            App.onResponse(url, res, err)
            var data = res.data || {}
            var doc = data.Document
            var tr = data.TestRecord
            var did = doc == null ? null : doc.id

            docReq.id = did
            trReq.id = tr == null ? null : tr.id

            if (did == null || did <= 0) {
              App.addApiCase2Chain(group, list, index + 1, chains, preIndex, preChain)
              alert('新增 Document 单接口用例失败！' + data.msg + '; ' + (err || {}).message + '; req = \n' + JSON.stringify(req))
              return
            }

            callback(null, config, inputObj, doc, req, url, res, err)
          })

        })

      },

      newAndUploadRandomConfig: function(baseUrl, req, documentId, config, count, callback, isExportApi, chainId, chainGroupId, url, chains) {
        if (documentId == null) {
          return
        }

        console.log('newAndUploadRandomConfig documentId = ' + documentId + '; count = ' + count
            + '; baseUrl = ' + baseUrl + '; url = ' + url + '; config = \n' + config
            + '; req = \n' + StringUtil.get(req) + '; chains = \n' + StringUtil.get(chains))

        const table = isExportApi ? 'Random' : 'Input';
        const isGenerate = StringUtil.isEmpty(config, true);
        var configs = isGenerate ? [] : [config]
        if (isGenerate) {
          config = StringUtil.trim(this.newRandomConfig(null, '', req, false, null, null, null, null, chains, url))
          if (StringUtil.isEmpty(config, true)) {
            return;
          }

          configs.push(config)
          var config2 = StringUtil.trim(this.newRandomConfig(null, '', req, ! isExportApi, null, null, isExportApi, null, null, url))
          if (StringUtil.isNotEmpty(config2, true)) {
            configs.push(config2)
          }
        }

        for (var i = 0; i < configs.length; i ++) {
          const cfg = configs[i]
          console.log('newAndUploadRandomConfig uploading cfg = \n' + StringUtil.get(cfg))

          this.adminRequest(this.server + '/post', {
            format: false,
            [table]: {
              chainGroupId: i > 0 ? null : chainGroupId,
              chainId: i > 0 ? null : chainId,
              documentId: documentId,
              count: count,
              name: '默认配置' + (isGenerate ? '(上传测试用例时自动生成)' : ''),
              config: cfg
            },
            TestRecord: {
              host: baseUrl,
              response: ''
            },
            tag: table
          }, {}, callback)
        }
      },

      onClickAddRandom: function (randomIndex, randomSubIndex) {
         if (this.isRandomListShow || this.isRandomSubListShow) {
           this.showExport(true, true, true)
         } else if (StringUtil.isEmpty(vRandom.value, true)) {
            var req = this.getRequest(vInput.value, {})
            vRandom.value = StringUtil.trim(this.newRandomConfig(null, '', req, Math.random() >= 0.5, Math.random() >= 0.3, Math.random() >= 0.8))
         } else {
            this.showExport(true, true, true)
         }

         // this.currentRandomIndex = randomIndex;
         // this.currentRandomSubIndex = randomSubIndex;
         // this.allowMultiple = randomIndex == null || randomIndex < 0;
         // const fileInput = document.getElementById('imageInput');
         // fileInput.click();
      },

      newRandomConfig: function (path, key, value, isRand, isBad, noDeep, isConst, chainPath, chains, url, maxLen) {
        maxLen = maxLen == null ? 4500 : maxLen
        if (maxLen <= 0 || key == null || key.indexOf('/') >= 0 || key.indexOf('.') >= 0) {
          return ''
        }
        var isAPIJSON = JSONObject.isAPIJSONPath(url)
        const IGNORE_KEYS = JSONResponse.IGNORE_LINK_KEYS || [];
        var lowerKey = key.toLowerCase()
        if (IGNORE_KEYS.indexOf(lowerKey) >= 0 || (isAPIJSON && path == '' && ['tag', 'version', 'format'].indexOf(key) >= 0)) {
          return ''
        }

        var isValueEmpty = StringUtil.isEmpty(value)
        var isChainShow = chains != null || this.isChainShow;
        url = url || this.getMethod()
        var isRestful = ! JSONObject.isAPIJSONPath(url);

        var config = ''
        var childPath = path == null || path == '' ? key : path + '/' + key
        var prefix = childPath + ': '

        var isPositive = Math.random() >= 0.3
        var offset = (isPositive ? '+' : '') + (isPositive ? 1 : -1)*randomPrimeInt()

        if (value instanceof Array) {
          if (isConst) {
            config += prefix + '[]'
            for (var i = 0; i < value.length; i ++) {
              var cfg = this.newRandomConfig(childPath, '' + i, value[i], isRand, isBad, noDeep, isConst, chainPath, chains, url, maxLen - config.length)
              config += '\n' + (StringUtil.isEmpty(cfg, true) ? 'null' : StringUtil.trim(cfg))
            }
            return config
          }
          if (isBad && noDeep && StringUtil.isNotEmpty(childPath, true)) {
            return prefix + (isRand ? 'RANDOM_BAD_ARR' : 'ORDER_BAD_ARR' + offset) + '()'
          }

          if (Math.random() >= 7) {
            var val
            if (value.length <= 0) {
              val = ''
            }
            else {
              if (value.length <= 1) {
                val = ', ' + JSON.stringify(value)
              }
              else if (value.length <= 2) {
                val = ', ' + JSON.stringify([value[0]]) + ', ' + JSON.stringify([value[1]]) + ', ' + JSON.stringify(value)
              }
              else {
                val = ', ' + JSON.stringify([value[0]]) + ', ' + JSON.stringify([value[value.length - 1]]) + ', ' + JSON.stringify([value[Math.floor(value.length / 2)]]) + ', ' + JSON.stringify(value)
              }
            }

            config += prefix + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null, false, true, -1025, 0, [], {}, 1, 3.14, "null", "undefined", Number.MAX_SAFE_INTEGER, "-1025", "0", "" + Number.MAX_SAFE_INTEGER, "[", "]", "{", "}", "1", "3.14", "true", "false"' + val + ')'
          }
          else {
            config += prefix + '[]'

            var l = randomInt(0, 13)
            for (var i = 0; i < l; i ++) {
              var cfg = this.newRandomConfig(childPath, '' + i, value[i], isRand, isBad, noDeep, isConst, chainPath, chains, url, maxLen - config.length)
              if (StringUtil.isEmpty(cfg, true)) {
                break
              }

              config += '\n' + StringUtil.trim(cfg)
            }
          }

          return config
        }
        else if (value instanceof Object) {
          if (isBad && noDeep && StringUtil.isNotEmpty(childPath, true)) {
            return prefix + (isRand ? 'RANDOM_BAD_OBJ' : 'ORDER_BAD_OBJ' + offset) + '()'
          }

          for (var k in value) {
            var v = value[k]

            var isAPIJSONArray = isConst == false && v instanceof Object && v instanceof Array == false
                && k.startsWith('@') == false && (k.endsWith('[]') || k.endsWith('@'))

            if (isAPIJSONArray) {
              if (k.endsWith('@')) {
                delete v.from
                delete v.range
              }

              prefix = '\n' + (childPath == null || childPath == '' ? '' : childPath + '/') + k + '/'
              if (v.hasOwnProperty('page')) {
                config += prefix + 'page: ' + (isRand ? 'RANDOM_INT' : 'ORDER_INT') + '(0, 10)'
                delete v.page
              }
              if (v.hasOwnProperty('count')) {
                config += prefix + 'count: ' + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null, 0, 1, 5, 10, 20'
                    + ([0, 1, 5, 10, 20].indexOf(v.count) >= 0 ? ')' : ', ' + v.count + ')')
                delete v.count
              }
              if (v.hasOwnProperty('query')) {
                config += prefix + 'query: ' + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null, 0, 1, 2)'
                delete v.query
              }
            }

            var cfg = this.newRandomConfig(childPath, k, v, isRand, isBad, noDeep, isConst, chainPath, chains, url, maxLen - config.length)
            if (StringUtil.isNotEmpty(cfg, true)) {
              if (k != null && k.toLowerCase() == 'id') {
                return cfg
              }
              config += '\n' + cfg
            }
          }

          return config
        }
        else {
          if (isConst) {
            return prefix + JSON.stringify(value) // 会自动给 String 加 ""
          }
          //自定义关键词
          if (key.startsWith('@')) {
            return config
          }

          // FIXME 似乎加了自动生成场景传参配置后，空配置时点击 + 添加配置后，切换用例列表会卡死
          if (isChainShow && StringUtil.isNotEmpty(key, true) && (typeof value != 'string' || ! key.endsWith('@'))) {
            var keyPath = StringUtil.isEmpty(chainPath) ? key : chainPath + '/' + StringUtil.get(key)
            var isStr = JSONResponse.isString(value)
            var isNum = JSONResponse.isNumber(value) || StringUtil.isNumber(value)
            var isUnique = (isStr && value.length >= 3) || (isNum && Math.abs(+value) > 100)

            if (chains instanceof Array) {
              function findConfig(folder, chain, k, v, ctxVar, maxLen) {
                if (StringUtil.isEmpty(chain)) {
                  return ''
                }
                if (k == null || k.indexOf('/') >= 0 || k.indexOf('.') >= 0 || IGNORE_KEYS.indexOf(k.toLowerCase()) >= 0) {
                  return ''
                }

                var route = ', "' + StringUtil.trim(chain.account) + '@' + StringUtil.trim(chain.host) + '/-1"'
                var isFolderEmpty = StringUtil.isEmpty(folder)
                var cp = isFolderEmpty ? (StringUtil.isEmpty(k) ? '' : k) : folder + (StringUtil.isEmpty(k) ? '' : '/' + k);
                var isCpEmpty = StringUtil.isEmpty(cp)
                ctxVar = ctxVar || 'data'
                var pfx = prefix + 'PRE_' + ctxVar.toUpperCase() + '("'
                var sfx = ', "' + StringUtil.trim(chain.method) + ' ' + StringUtil.trim(chain.url) + '"'

                if (v instanceof Array) {
                  for (var i = 0; i < v.length; i ++) {
                    // var chs = [{method: chain.method, url: chain.url, response: v[0]}]
                    var ccp = isCpEmpty ? '/' : cp + '/'
                    // var cfg = App.newRandomConfig(path, key, value, isRand, isBad, noDeep, isConst, (cp || '') + '/', chs, url, maxLen - config.length)
                    var cfg = findConfig(ccp, chain, '', v[i], ctxVar, maxLen)
                    if (StringUtil.isNotEmpty(cfg, true) && config.indexOf(cfg) < 0) {
                      return cfg
                    }
                  }
                }
                else if (v instanceof Object) {
                  var cfg2 = ''
                  var v2 = v[key]
                  if (key.length >= 3 && typeof v2 != 'undefined' && ! JSONResponse.isObject(v2)) {
                    var ccp = isFolderEmpty ? key : cp + '/' + key
                    cfg2 = (StringUtil.isEmpty(config) ? '' : '\n// 可替代上面的 ') + pfx + ccp + '", null' + sfx + ') // key 同名';
                    cfg2 += '\n// 可替代上面的 ' + pfx + ccp + '", undefined' + sfx + route + ') // key 同名';
                    var isValMatch = v2 === value && (isStr || isNum) && (! isValueEmpty) && [0, 1, -1, 'true', 'false', 'null', 'undefined', '0', '1', '-1'].indexOf(v2) < 0
                    if (isValMatch) {
                      cfg2 += ' + value 相等：' + StringUtil.limitLength(v2, 50);
                      return cfg2;
                    }
                  }

                  for (var k2 in v) {
                      if (k2 == key) {
                        continue
                      }

                      v2 = v[k2]
                      // var chs = [{method: chain.method, url: chain.url, request: v2}]
                      // var cfg = App.newRandomConfig(path, key, value, isRand, isBad, noDeep, isConst, (cp || '') + k2 + '/', chs, url, maxLen - config.length)
                      var cfg = findConfig(cp, chain, k2, v2, ctxVar, maxLen - cfg2.length)
                      if (StringUtil.isEmpty(cfg, true) || config.indexOf(cfg) >= 0 || cfg2.indexOf(cfg) >= 0) {
                        continue
                      }
                      cfg2 += '\n' + cfg
                      if (cfg2.length > maxLen - 200 || k2.length >= 3) {
                        return cfg2
                      }
                  }

                  return cfg2
                }
                else {
                  var isKeyMatch = StringUtil.isNotEmpty(k) && k.toLowerCase().endsWith(lowerKey) || lowerKey.endsWith(k)
                  var isValMatch = v === value && (isStr || isNum) && (! isValueEmpty) && [0, 1, -1, 'true', 'false', 'null', 'undefined', '0', '1', '-1'].indexOf(v) < 0
                  if (isKeyMatch || isValMatch) { // FIXME StringUtil.endsWith(a, b, ignoreCase)
                    var sfx2 = ') // ' + (isKeyMatch ? 'key 相似' : '') + (isValMatch ? (isKeyMatch ? ' + ' : '') + 'value 相等：' + StringUtil.limitLength(v, 50) : '');
                    var ccp = cp // isFolderEmpty ? StringUtil.get(k) : folder + '/' + StringUtil.get(k) // '' 代表数组 i = cp
                    var cfg = (StringUtil.isEmpty(config) ? '' : '\n// 可替代上面的 ') + pfx + ccp + '", null' + sfx + sfx2;
                    cfg += '\n// 可替代上面的 ' + pfx + ccp + '", undefined' + sfx + route + sfx2;
                    if (isValMatch && (isKeyMatch || isUnique)) {
                      return cfg
                    }
                  }
                }

                return config
              }

              const last = chains.length - 1
              for (var i = last; i >= 0; i --) {
                var chain = chains[i] || {}
                var req = chain.request
                // var res = chain.response
                var cfg = findConfig(chainPath, chain,'', req, 'arg')
                if (StringUtil.isEmpty(cfg) || config.indexOf(cfg) >= 0) {
                  continue
                }
                config += '\n' + cfg
                if (config.length > maxLen - 200) {
                  return config
                }
              }

              for (var i = last; i >= 0; i --) {
                var chain = chains[i] || {}
                // var req = chain.request
                var res = chain.response
                var data = res
                if (JSONResponse.isObject(res) && StringUtil.isEmpty(chainPath) && res[JSONResponse.KEY_DATA] != null) {
                  data = res[JSONResponse.KEY_DATA];
                  chainPath = JSONResponse.KEY_DATA;
                }

                var cfg = findConfig(chainPath, chain, '', data, 'data')
                if (StringUtil.isEmpty(cfg) || config.indexOf(cfg) >= 0) {
                  continue
                }
                config += '\n' + cfg
                if (config.length > maxLen - 200) {
                  return config
                }
              }
            }

            if (StringUtil.isNotEmpty(chainPath) || config.length > maxLen - 200) {
              return config
            }

            var keys = StringUtil.split(path, '/');
            var table = keys == null ? '' : keys[keys.length - 1];
            var isAPIJSONArray = childPath.indexOf('[]') >= 0;
            var isId = key.toLowerCase() == 'id';
            var isList = isAPIJSONArray || childPath.toLowerCase().indexOf('list') >= 0;

            var tbl = StringUtil.getTableName(key)
            tbl = StringUtil.firstCase(tbl, true)
            var col = StringUtil.getColumnName(key)
            col = StringUtil.firstCase(col, false)

            var ks = keys == null ? [] : keys.slice(0, keys.length - 1)
            ks.push(tbl)
            var p = ks.join('/')
            var cp = StringUtil.isNotEmpty(tbl) ? childPath : (StringUtil.isEmpty(p) ? '' : p + '/') + col
            var kp = StringUtil.isEmpty(p) ? path : p;
            var fd = StringUtil.isEmpty(kp) ? '' : kp + '/';

            var ctxKey = StringUtil.isNotEmpty(tbl) ? key : StringUtil.firstCase(table, false) + StringUtil.firstCase(key, true)
            var ctxPutPfx = ctxKey + ': ';
            var tbl_ = tbl || table;
            var isTableEmpty = StringUtil.isEmpty(tbl_)
            var camelIdKey = isTableEmpty ? '' : StringUtil.firstCase(tbl_ + 'Id');
            var snakeIdKey = isTableEmpty ? '' : tbl_.toLowerCase() + '_id';

            if (isList) {
              if (isId) {
                config += isTableEmpty ? '' : prefix + 'PRE_ARG("' + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + snakeIdKey + '")';
                config += (isTableEmpty ? '' : '\n// 可替代上面的 ') + prefix + 'PRE_DATA("' + cp + '")';
                if (isRestful) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
                  if (key != cp) {
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + key + '")';
                  }
                }
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + snakeIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + snakeIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + snakeIdKey + '")';
                if (isRestful && ! (isAPIJSONArray || isTableEmpty)) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + camelIdKey + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + snakeIdKey + '")';
                }
              }
              else if (StringUtil.isIdKey(key)) {
                config += prefix + 'PRE_ARG("' + key + '")';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("id")';
                if (key != cp) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + cp + '")';
                }
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + cp + '")';

                var idKey = key.toLowerCase().startsWith(table.toLowerCase()) ? key : "id";
                if (isRestful && ! isAPIJSONArray) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
                  if (key != cp) {
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + key + '")';
                  }

                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + key + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/' + key + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + key + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("list/0/' + key + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("list/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + (tbl || table) + '/' + (col || key) + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/' + (tbl || table) + '/' + (col || key) + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + (tbl || table) + '/' + (col || key) + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + (tbl || table) + '/' + (col || key) + '")';
                } else {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("[]/0/' + key + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("[]/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + fd + (col || key) + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + (path || '') + '[]/0/' + key + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + (path || '') + '[]/0/id")';
                }
              }
              else {
                config += StringUtil.isEmpty(cp) ? '' : prefix + 'PRE_DATA("' + cp + '", get4Path(req,"' + childPath + '",null))';
                config += (StringUtil.isEmpty(cp) ? '' : '\n// 可替代上面的 ') + prefix + 'PRE_DATA("' + cp + '", get4Path(req,"' + childPath + '"))';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + cp + '")';
                if (isRestful && ! isAPIJSONArray) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
                  if (key != cp) {
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + key + '")';
                    if (StringUtil.isNotEmpty(col)) {
                      config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + (StringUtil.isEmpty(tbl) ? '' : tbl + '/') + col + '")';
                      config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + col + '")';
                    }
                  }
                }
              }

              config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + childPath + '")';
              config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + cp + '")';
              config += '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + ctxKey + '")';
              if (key != childPath) {
                config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + key + '")';
                config += '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + ctxKey + '")';
              }
            } else {
              if (isId) {
                if (isRestful) {
                  config += isTableEmpty ? '' : prefix + 'PRE_DATA("' + 'data/list/0/' + camelIdKey + '")';
                  config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + 'data/0/' + snakeIdKey + '")';
                  config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + 'list/0/' + camelIdKey + '")';
                } else {
                  config += isTableEmpty ? '' : prefix + 'PRE_DATA("' + kp + '[]/0/' + camelIdKey + '")';
                  config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + kp + '[]/0/' + snakeIdKey + '")';
                }

                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + snakeIdKey + '")';
                config += (isTableEmpty ? '' : '\n// 可替代上面的 ') + prefix + 'PRE_DATA("' + cp + '")';
                if (isRestful) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
                  if (key != cp) {
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + key + '")';
                  }
                }
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + snakeIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + (StringUtil.isEmpty(path) ? '' : path + '/') + camelIdKey + '")';
                config += isTableEmpty ? '' : '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + (StringUtil.isEmpty(path) ? '' : path + '/') + snakeIdKey + '")';
              }
              else if (StringUtil.isIdKey(key)) {
                if (isRestful) {
                  config += prefix + 'PRE_DATA("data/list/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("list/0/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + fd + 'id")';
                } else {
                  config += prefix + 'PRE_DATA("[]/0/' + (StringUtil.isEmpty(p) ? '' : kp + '/') + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("[]/0/' + fd + (col || key) + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("[]/0/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + kp + '[]/0/' + (col || key) + '")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + kp + '[]/0/id")';
                }
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + cp + '")';
                if (isRestful) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
                  if (key != cp) {
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + (col || key) + '")';
                  }
                }
                config += '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + ctxKey + '")';
                if (isRestful) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/id")';
                }
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("id")';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + fd + 'id")';
                if (isRestful) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + fd + 'id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/id")';
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + fd + 'id")';
                }
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + fd + 'id")';
              }
              else {
                config += prefix + 'PRE_DATA("' + cp + '", get4Path(req,"' + key + '",null))';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + cp + '")';
                if (isRestful) {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
                  if (key != cp) {
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + key + '")';
                    config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + key + '")';
                  }
                } else {
                  config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + key + '")';
                }
              }

              config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + childPath + '")';
              if (key != childPath) {
                config += '\n// 可替代上面的 ' + prefix + 'PRE_ARG("' + key + '")';
              }

              if (isRestful) {
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/' + cp + '")';
              }
              config += '\n// 可替代上面的 ' + prefix + 'CTX_GET("' + ctxKey + '")';
              if (isRestful) {
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/list/0/' + cp + '")';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("data/0/' + cp + '")';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("list/0/' + cp + '")';
                config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("[]/0/' + cp + '", "PRE_DATA")';
                config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("data/0/' + cp + '", "PRE_DATA")';
                config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("list/0'/ + cp + '", "PRE_DATA")';
              } else {
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("[]/0/' + cp + '")';
                config += '\n// 可替代上面的 ' + prefix + 'PRE_DATA("' + kp + '[]/0/' + (col || key) + '")';
                config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("' + kp + '[]/0/' + (col || key) + '", "PRE_DATA")';
                config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("[]/0/' + cp + '", "PRE_DATA")';
              }
            }

            config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("' + key + '", App.getCurrentAccount())';
            config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("' + cp + '", "PRE_DATA")';
            config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("' + childPath + '", "PRE_ARG")';
            if (key != childPath) {
              config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("' + key + '", "PRE_DATA")';
              config += '\n// 可替代上面的 ' + ctxPutPfx + 'CTX_PUT("' + key + '", "PRE_ARG")';
            }
          }
          else if (typeof value == 'boolean') {
            if (isBad) {
              return prefix + (isRand ? 'RANDOM_BAD_BOOL' : 'ORDER_BAD_BOOL' + offset) + '()'
            }

            config += prefix + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null, false, true)'
          }
          else if (typeof value == 'number') {
            if (isBad) {
              return prefix + (isRand ? 'RANDOM_BAD_NUM' : 'ORDER_BAD_NUM' + offset) + '()'
            }

            var isId = key == 'id' || key.endsWith('Id') || key.endsWith('_id') || key.endsWith('_ID')
            if (isId) {
              config += prefix + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null, ' + value + ')'
              if (value >= 1000000000) { //PHP 等语言默认精确到秒 1000000000000) {
                config += '\n// 可替代上面的 ' + prefix + 'RANDOM_INT(' + Math.round(0.9 * value) + ', ' + Math.round(1.1 * value) + ')'
              }
              else {
                config += '\n// 可替代上面的 ' + prefix + 'RANDOM_INT(1, ' + (10 * value) + ')'
              }
            }
            else {
              var valStr = String(value)
              var dotIndex = valStr.indexOf('.')
              var hasDot = dotIndex >= 0
              var keep = dotIndex < 0 ? 2 : valStr.length - dotIndex - 1

              if (value < 0) {
                config += prefix + (hasDot ? 'RANDOM_NUM' : 'RANDOM_INT') + '(' + (100 * value) + (hasDot ? ', 0, ' + keep + ')' : ', 0)')
              }
              else if (value > 0 && value < 1) {  // 0-1 比例
                config += prefix + 'RANDOM_NUM(0, 1, ' + keep + ')'
              }
              else if ((hasDot && value > 0 && value <= 100) || (hasDot != true && value > 5 && value <= 100)) {  // 10% 百分比
                config += prefix + (hasDot ? 'RANDOM_NUM(0, 100, ' + keep + ')' : 'RANDOM_INT(0, 100)')
              }
              else {
                config += prefix + (dotIndex < 0 && value <= 10
                        ? (isRand ? 'RANDOM_INT' : 'ORDER_INT') + '(0, 10)'
                        : ((hasDot ? 'RANDOM_NUM' : 'RANDOM_INT') + '(0, ' + 100 * value + (hasDot ? ', ' + keep + ')' : ')'))
                )
                var hasDot = String(value).indexOf('.') >= 0

                if (value < 0) {
                  config += '\n// 可替代上面的 ' + prefix + (hasDot ? 'RANDOM_NUM' : 'RANDOM_INT') + '(' + (100 * value) + ', 0)'
                }
                else if (value > 0 && value < 1) { // 0-1 比例
                  config += '\n// 可替代上面的 ' + prefix + 'RANDOM_NUM(0, 1)'
                }
                else if (value >= 0 && value <= 100) { // 10% 百分比
                  config += '\n// 可替代上面的 ' + prefix + 'RANDOM_INT(0, 100)'
                }
                else {
                  config += '\n// 可替代上面的 ' + prefix + (hasDot != true && value < 10 ? (isRand ? 'RANDOM_INT' : 'ORDER_INT') + '(0, 9)' : ((hasDot ? 'RANDOM_NUM' : 'RANDOM_INT') + '(0, ' + 100 * value + ')'))
                }
              }
            }
          }
          else if (typeof value == 'string') {
            if (isBad) {
              return prefix + (isRand ? 'RANDOM_BAD_STR' : 'ORDER_BAD_STR' + offset) + '()'
            }

            //引用赋值 || 远程函数 || 匹配条件范围
            if (key.endsWith('@') || key.endsWith('()') || key.endsWith('{}')) {
              return config
            }

            config += prefix + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null, ""' + (value == '' ? ')' : ', "' + value + '")')
          }
          else {
            if (isBad) {
              return prefix + (isRand ? 'RANDOM_BAD' : 'ORDER_BAD' + offset) + '()'
            }

            config += prefix + (isRand ? 'RANDOM_IN' : 'ORDER_IN') + '(undefined, null' + (value == null ? ')' : ', ' + JSON.stringify(value) + ')')
          }

        }

        return config
      },


      // 保存配置
      saveConfig: function () {
        this.isConfigShow = false // this.exTxt.index == 8

        switch (this.exTxt.index) {
          case 0:
            this.database = CodeUtil.database = this.exTxt.name
            this.saveCache('', 'database', this.database)

            doc = null
            var item = this.accounts[this.currentAccountIndex] || {}
            item.isLoggedIn = false
            this.onClickAccount(this.currentAccountIndex, item)
            break
          case 1:
            this.schema = CodeUtil.schema = this.exTxt.name
            this.saveCache('', 'schema', this.schema)

            doc = null
            var item = this.accounts[this.currentAccountIndex] || {}
            item.isLoggedIn = false
            this.onClickAccount(this.currentAccountIndex, item)
            break
          case 2:
            this.language = CodeUtil.language = this.exTxt.name
            this.saveCache('', 'language', this.language)

            doc = null
            this.onChange(false)
            break
          case 3:
            this.host = this.exTxt.name
            this.saveCache('', 'host', this.host)
            break
          case 6:
            this.server = this.exTxt.name
            this.saveCache('', 'server', this.server)
            this.logout(true)
            break
          case 16:
            this.methods = StringUtil.split(this.exTxt.name, ',', true)
            this.saveCache('', 'methods', this.methods)
            break
          case 7:
            this.types = StringUtil.split(this.exTxt.name, ',', true)
            this.saveCache('', 'types', this.types)
            break
          case 15:
            this.otherEnv = StringUtil.get(this.exTxt.name)
            this.saveCache('', 'otherEnv', this.otherEnv)
            break
          case 8:
            baseUrl = this.exTxt.name || baseUrl

            var projectHosts = this.projectHosts = this.projectHosts || []
            var find = null
            for (var i = 0; i < projectHosts.length; i ++) {
              var ph = projectHosts[i]
              if (ph != null && ph.host == baseUrl) {
                find = ph
                break
              }
            }

            if (find == null) {
              this.projectHost = {host: baseUrl}
              projectHosts.push(this.projectHost)
              this.saveCache('', 'projectHosts', this.projectHosts)
            }
            this.saveCache('', 'projectHost', this.projectHost)

            var c = this.currentAccountIndex == null ? -1 : this.currentAccountIndex
            var item = this.accounts == null ? null : this.accounts[c]
            if (item != null) {
              item.isLoggedIn = ! item.isLoggedIn
              this.onClickAccount(c, item)
            }
            break
        }
      },
      resetUploading: function() {
        App.uploadTotal = 0 // apis.length || 0
        App.uploadDoneCount = 0
        App.uploadFailCount = 0
        App.uploadRandomCount = 0
      },

      generateValue: function (t, n, isSQL) {
        if (t == 'boolean') {
          return true
        }
        if (t == 'integer') {
          return n == 'pageSize' ? 10 : 1
        }
        if (t == 'number') {
          return n == 'pageSize' ? 10 : 1
        }
        if (t == 'string') {  // TODO
          return ''
        }
        if (t == 'object') {
          return {}
        }
        if (t == 'array') {
          return []
        }
        var suffix = n != null && n.length >= 3 ? n.substring(n.length - 3).toLowerCase() : null
        if (suffix == 'dto') {
          return {}
        }

        return null
      },

      // 切换主题
      switchTheme: function (index) {
        this.checkedTheme = index
        localforage.setItem('#theme', index)
      },


      // APIJSON <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

      //格式化日期
      formatDate: function (date) {
        if (date == null) {
          date = new Date()
        }
        return date.getFullYear() + '-' + this.fillZero(date.getMonth() + 1) + '-' + this.fillZero(date.getDate())
      },
      //格式化时间
      formatTime: function (date) {
        if (date == null) {
          date = new Date()
        }
        return this.fillZero(date.getHours()) + ':' + this.fillZero(date.getMinutes()) + ':' + this.fillZero(date.getSeconds())
      },
      formatDateTime: function (date) {
        if (date == null) {
          date = new Date()
        }
        return this.formatDate(date) + ' ' + this.formatTime(date)
      },
      //填充0
      fillZero: function (num, n) {
        if (num == null) {
          num = 0
        }
        if (n == null || n <= 0) {
          n = 2
        }
        var len = num.toString().length;
        while(len < n) {
          num = "0" + num;
          len++;
        }
        return num;
      },


      saveAccounts: function() {
          baseUrl = this.getBaseUrl()
          this.saveCache(baseUrl, 'currentAccountIndex', this.currentAccountIndex)

          var accounts = this.accounts || []
          var accountMap = {}
          for (var i = 0; i < accounts.length; i ++) {
              var item = accounts[i]
              var account = JSONResponse.getAccount(item);
              if (item == null || StringUtil.isEmpty(account)) {
                continue
              }

              var bu = item.baseUrl || baseUrl
              var list = accountMap[bu] || []

              var find = false
              for (var j = 0; j < list.length; j ++) {
                  var act = list[j]
                  if (act == null) {
                      continue
                  }

                  if (act.baseUrl == bu && JSONResponse.getAccount(act) != account) {
                      find = true
                      break
                  }
              }

              if (find != true) {
                   list.push(item)
                   accountMap[bu] = list
              }
          }

          for (var k in accountMap) {
              this.saveCache(k, 'accounts', accountMap[k])
          }
      },

      onClickAccount: function (index, item, callback) {
        var accounts = this.accounts
        var num = accounts == null ? 0 : accounts.length
        if (index < 0 || index >= num) {
          item = this.getCurrentAccount()
          if (item != null && item.isLoggedIn) {
            //logout FIXME 没法自定义退出，浏览器默认根据url来管理session的
            this.logout(false, function (url, res, err) {
              App.onResponse(url, res, err)

              item.isLoggedIn = false
              App.saveAccounts()

              // App.changeScriptType(App.scriptType)

              if (callback != null) {
                callback(false, index, err)
              }
            });
          } else {
            if (callback != null) {
              callback(false, index)
            }
          }

          this.currentAccountIndex = index
          // this.changeScriptType(App.scriptType)
          return
        }

        if (this.currentAccountIndex == index) {
          if (item == null) {
            if (callback != null) {
              callback(false, index)
            }
          }
          else {
            this.setRememberLogin(item.remember)
            this.account = item.phone
            this.password = item.password

            if (item.isLoggedIn) {
              //logout FIXME 没法自定义退出，浏览器默认根据url来管理session的
              this.logout(false, function (url, res, err) {
                App.onResponse(url, res, err)

                item.isLoggedIn = false
                App.saveAccounts()
                // App.changeScriptType(App.scriptType)

                if (callback != null) {
                  callback(false, index, err)
                }
              });

              this.currentAccountIndex = -1
              // this.changeScriptType(App.scriptType)
            }
            else {
              //login
              this.login(false, function (url, res, err) {
                App.onResponse(url, res, err)

                var resData = res.data || {}
                var data = parseJSON(resData['return'] || JSONResponse.getValByPath(resData, StringUtil.split('methodArgs/3/value/call(){}/onHttpResponse(int,String,Throwable)/0/methodArgs/1/value', '/'))) || resData
                var user = data.user || data.userObj || data.userObject || data.userRsp || data.userResp || data.userBean || data.userData || data.data || data.User || data.Data || data
                if (! JSONResponse.isObject(user)) {
                  if (callback != null) {
                    callback(false, index, err)
                  }
                }
                else {
                  var headers = res.headers || {}
                  baseUrl = App.getBaseUrl(App.projectHost.host)

                  item.baseUrl = item.baseUrl || baseUrl
                  item.id = JSONResponse.getId(user)  // TODO 工具函数直接遍历 key 判断可能的名称
                  item.name = JSONResponse.getName(user)
//                  item.phone = JSONResponse.getPhone(user)
                  item.remember = data.remember
                  item.isLoggedIn = true
                  item.token = headers.token || headers.Token || data.token || data.Token || user.token || user.Token || user.accessToken || user.access_token || data.accessToken || data.access_token
                  item.cookie = res.cookie || headers.cookie || headers.Cookie || headers['set-cookie'] || headers['Set-Cookie']

                  App.accounts[App.currentAccountIndex] = item
                  App.saveAccounts()

                  // App.changeScriptType(App.scriptType)

                  if (callback != null) {
                      callback(true, index, err)
                  }
                }
              }, true);
            }

          }

          return;
        }

        //退出当前账号
        var c = this.currentAccountIndex
        var it = c == null || this.accounts == null ? null : this.accounts[c];
        if (it != null) { //切换 BASE_URL后 it = undefined 导致UI操作无法继续
          it.isLoggedIn = false  //异步导致账号错位 this.onClickAccount(c, this.accounts[c])
        }

        //切换到这个tab
        this.currentAccountIndex = index
        // this.changeScriptType(App.scriptType)

        //目前还没做到同一标签页下测试账号切换后，session也跟着切换，所以干脆每次切换tab就重新登录
        if (item != null) {
          item.isLoggedIn = false
          this.onClickAccount(index, item, callback)
        }
        else {
          if (callback != null) {
              callback(false, index)
          }
        }
      },

      removeAccountTab: function () {
        if (this.accounts.length <= 1) {
          alert('至少要 1 个测试账号！')
          return
        }

        this.accounts.splice(this.currentAccountIndex, 1)
        if (this.currentAccountIndex >= this.accounts.length) {
          this.currentAccountIndex = this.accounts.length - 1
        }

        this.saveAccounts()
      },
      addAccountTab: function () {
        this.showLogin(true, false)
      },

      showCompare4TestCaseList: function (show) {
        var testCases = show ? App.testCases : null
        var allCount = testCases == null ? 0 : testCases.length
        App.allCount = allCount
        if (allCount > 0) {

          var accountIndex = (this.accounts[this.currentAccountIndex] || {}).isLoggedIn ? this.currentAccountIndex : -1
          this.currentAccountIndex = accountIndex  //解决 onTestResponse 用 -1 存进去， handleTest 用 currentAccountIndex 取出来为空
//          var account = this.accounts[accountIndex]
//          baseUrl = this.getBaseUrl()

          var reportId = this.reportId
          if (reportId == null || Number.isNaN(reportId)) {
            reportId = null
          }

          var tests = this.tests[String(accountIndex)] // FIXME  account.phone + '@' + (account.baseUrl || baseUrl)]
          if ((reportId != null && reportId >= 0) || (tests != null && JSONObject.isEmpty(tests) != true)) {
            for (var i = 0; i < allCount; i++) {
              var item = testCases[i]
              var d = item == null ? null : item.Flow
              if (d == null || d.id == null) {
                continue
              }

              if (reportId != null && reportId >= 0) {
                var tr = item.TestRecord || {}
                var rsp = parseJSON(tr.response)
                tests[d.id] = [rsp]

                var cmp = parseJSON(tr.compare)
                if (cmp == null || Object.keys(cmp).length <= 0) {
                  cmp = JSONResponse.compareWithBefore(null, null)
                }

                this.onTestResponse(null, allCount, testCases, i, item, d, item.Input, tr, rsp, cmp, false, accountIndex, true);
                continue
              }

              this.compareResponse(null, allCount, testCases, i, item, (tests[d.id] || {})[0], false, accountIndex, true)
            }
          }
        }
      },

      onClickChainPath: function (index, path) {
        var chainPaths = this.chainPaths
        this.chainPaths = chainPaths.slice(0, index)
        this.selectChainGroup(0, path)
      },
      isChainGroupShow: function () {
        return this.chainShowType != 1 && (this.chainGroups.length > 0) // || this.chainPaths.length <= 0)
      },
      isChainItemShow: function () {
        return this.chainShowType != 2 || (this.chainGroups.length <= 0 && this.chainPaths.length > 0)
      },
      selectChainGroup: function (index, group) {
        this.currentChainGroupIndex = index
        this.currentDocIndex = -1
        this.isCaseGroupEditable = false

        if (group == null) {
          if (index == null) {
            index = this.chainPaths.length - 1
            group = this.chainPaths[index]
          } else {
            this.chainPaths = []
          }
        } else {
          this.chainPaths = [group] // .push(group)
        }

       this.casePaths = this.chainPaths

        var groupId = group == null ? 0 : group.groupId
        if (groupId != null && groupId > 0) { // group != null && groupId == 0) {
//          this.chainGroups = []
          this.remotes = this.testCases = (this.chainGroups[index] || {})['[]'] || []
//          this.showTestCase(true, false, null)
          return
        }

        var isMLEnabled = this.isMLEnabled
        var userId = this.User.id
        var project = this.projectHost.project
        baseUrl = this.getBaseUrl(this.projectHost.host, true)

        var key = groupId + ''
        var page = this.chainGroupPage = this.chainGroupPages[key] || 0
        var count = this.chainGroupCount = this.chainGroupCounts[key] || 0
        var search = this.chainGroupSearch = this.chainGroupSearches[key] || ''

        search = StringUtil.isEmpty(search, true) ? null : '%' + StringUtil.trim(search).replaceAll('_', '\\_').replaceAll('%', '\\%') + '%'
        var req = {
          format: false,
          '[]': {
            'count': count || 20,
            'page': page || 0,
            'Chain': {
              'userId': userId,
              'toGroupId': groupId,
              'groupName$': search,
//              '@raw': '@column',
              '@column': "groupId;any_value(groupName):groupName;count(*):count",
              '@group': 'groupId',
              '@order': 'groupId-',
//              'documentId>': 0
            },
            '[]': {
              'count': this.testCaseCount || 20, //200 条测试直接卡死 0,
              'page': 0,
              'join': '&/Flow', // ,@/Input,@/TestRecord,@/Script:pre,@/Script:post',
              'Chain': {
                // TODO 后续再支持嵌套子组合 'toGroupId': groupId,
                'userId': userId,
                'groupId@': '[]/Chain/groupId',
                '@column': "id,groupId,documentId,randomId,rank",
                '@order': 'rank+,id+',
                'documentId>': 0
              },
              'Flow': {
                'id@': '/Chain/documentId',
                // '@column': 'id,userId,version,date,name,operation,method,type,url,request,apijson,standard', // ;substr(url,' + (StringUtil.length(groupUrl) + 2) + '):substr',
                '@order': 'versionCode-,time-',
                'userId': userId,
                'project': StringUtil.isEmpty(project, true) ? null : project,
                'name$': search,
                'operation$': search,
                'url$': search,
                // 'group{}': group == null || StringUtil.isNotEmpty(groupUrl) ? null : 'length(group)<=0',
                // 'group{}': group == null ? null : (group.groupName == null ? "=null" : [group.groupName]),
                '@combine': search == null ? null : 'name$,operation$,url$',
//                'method{}': methods == null || methods.length <= 0 ? null : methods,
//                'type{}': types == null || types.length <= 0 ? null : types,
                '@null': 'sqlauto', //'sqlauto{}': '=null'
                // '@having': StringUtil.isEmpty(groupUrl) ? null : "substring_index(substr,'/',1)<0"
              },
              'Input': {
//                'id@': '/Chain/randomId',
                'toId': 0,
                'chainId@': '/Chain/id',
                'flowId@': '/Flow/id',
                'userId': userId,
                '@column': 'count(*):count;sum(disable):disableCount',
                // '@raw': '@column',
                '@order': 'time-'
              },
              'Script:pre': {
                'ahead': 1,
                // 'testAccountId': 0,
                'chainId@': '/Chain/id',
                'documentId@': '/Flow/id',
                '@order': 'date-'
              },
              'Script:post': {
                'ahead': 0,
                // 'testAccountId': 0,
                'chainId@': '/Chain/id',
                'documentId@': '/Flow/id',
                '@order': 'date-'
              },
              'TestRecord': {
                'chainId@': '/Chain/id',
                'documentId@': '/Flow/id',
                'userId': userId,
                'host': StringUtil.isEmpty(baseUrl, true) ? null : baseUrl,
                'screenshot[>': 0,
                '@order': 'randomId+,date-',
                '@column': 'screenshot'
              }
            },
          },
          '@role': IS_NODE ? null : 'LOGIN',
          key: IS_NODE ? this.key : undefined  // 突破常规查询数量限制
        }

        if (IS_BROWSER) {
          this.onChange(false)
        }

        this.adminRequest('/get', req, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var data = res.data
          if (JSONResponse.isSuccess(data) == false) {
            alert('获取场景用例分组失败！\n' + (err != null ? err.message : (data || '').msg))
            if (IS_BROWSER) { // 解决一旦错了，就只能清缓存
              App.chainGroupCount = 50
              App.chainGroupPage = 0
              App.chainGroupSearch = ''
              App.chainGroupCounts = {}
              App.chainGroupPages = {}
              App.chainGroupSearches = {}

              App.saveCache(App.server, 'chainGroupCount', App.chainGroupCount)
              App.saveCache(App.server, 'chainGroupPage', App.chainGroupPage)
              App.saveCache(App.server, 'chainGroupSearch', App.chainGroupSearch)
              App.saveCache(App.server, 'chainGroupCounts', App.chainGroupCounts)
              App.saveCache(App.server, 'chainGroupPages', App.chainGroupPages)
              App.saveCache(App.server, 'chainGroupSearches', App.chainGroupSearches)
            }
            return
          }

          var chainGroups = App.chainGroups = data['[]'] || []
          var count = chainGroups.length
          var index = App.currentChainGroupIndex
          if (index == null || index < 0 || index >= count) {
             App.currentChainGroupIndex = index = 0
          }

          var item = chainGroups[index] || {}
          if (item.Chain != null) {
            App.chainPaths.push(item.Chain)
          }
          App.remotes = App.testCases = item['[]'] || []
          App.isTestCaseShow = true
//          App.showTestCase(true, false, null)
        })
      },

      addCase2Chain: function (item) {
        var id = item == null ? null : item.id
        if (id == null || id <= 0) {
          alert('请选择有效的用例！')
          return
        }

        var group = (this.chainGroups[this.currentChainGroupIndex] || {}).Chain
        if (group == null) {
          var index = this.chainPaths.length - 1
          group = this.chainPaths[index]
        }

        var groupId = group == null ? 0 : group.groupId
        if (groupId == null || groupId <= 0) {
          alert('请选择有效的场景串联用例分组！')
          return
        }

        var nextIndex = this.currentDocIndex
        var nextChain = nextIndex == null || nextIndex < 0 ? null : (this.testCases[nextIndex] || {}).Chain
        var nextRank = nextChain == null ? null : nextChain.rank

        var groupName = group.groupName
        var isAdd = true
        this.adminRequest('/post', {
          Chain: {
            'rank': this.formatDateTime(StringUtil.isEmpty(nextRank, true) ? null : new Date(new Date(nextRank).getTime() - 10)),
            'groupName': groupName,
            'groupId': groupId,
            'documentId': item.id,
            'documentName': item.name
          },
          tag: 'Chain+Random'
        }, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var isOk = JSONResponse.isSuccess(res.data)

          var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((res.data || {}).msg))
          if (err != null) {
            msg += '\nerr: ' + err.msg
          }
          alert((isAdd ? '新增' : '修改') + (isOk ? '成功' : '失败') + (isAdd ? '! \n' :'！\ngroupId: ' + groupId) + '\ngroupName: ' + groupName + msg)

          App.isCaseGroupEditable = ! isOk
          if (isOk) {
//          App.remotes = App.testCases = []
//          App.showTestCase(true, false, null)
            App.selectChainGroup(App.currentChainGroupIndex, null)
          }
        })
      },

      onClickPath: function (index, path) {
        if (this.isChainShow) {
          this.onClickChainPath(index, path)
          return
        }

        var paths = this.casePaths
        this.casePaths = paths.slice(0, index)
        this.selectCaseGroup(0, path)
      },
      isCaseGroupShow: function () {
        if (this.isChainShow) {
          return this.isChainGroupShow()
        }

        return this.caseShowType != 1 && (this.caseGroups.length > 0 || this.casePaths.length <= 0)
      },
      isCaseItemShow: function () {
        if (this.isChainShow) {
          return this.isChainItemShow()
        }

        return this.caseShowType != 2 || (this.caseGroups.length <= 0 && this.casePaths.length > 0)
      },
      getCaseGroupShowName: function(index, item) {
        if (StringUtil.isNotEmpty(item.groupName, true)) {
          return item.groupName
        }
        if (StringUtil.isEmpty(item.groupUrl, true)) {
          return '-'
        }

        var prev = index <= 0 ? null : (this.casePaths[index-1] || {}).groupUrl
        return item.groupUrl.substring(prev == null ? 1 : prev.length + 1)
      },
      selectCaseGroup: function (index, group) {
        if (this.isChainShow) {
          this.selectChainGroup(index, group)
          return
        }

        this.isCaseGroupEditable = false

        if (group == null) {
          if (index == null) {
            index = this.casePaths.length - 1
            group = this.casePaths[index]
          } else {
            this.casePaths = []
          }
        } else {
          this.casePaths.push(group)
        }

        var groupUrl = group == null ? '' : (group.groupUrl || '')
        if (group != null && StringUtil.isEmpty(groupUrl)) {
          this.caseGroups = []
          this.remotes = App.testCases = []
          this.showTestCase(true, false, null)
          return
        }

        var project = (this.projectHost || {}).project

        var page = this.caseGroupPage = this.caseGroupPages[groupUrl] || 0
        var count = this.caseGroupCount = this.caseGroupCounts[groupUrl] || 0
        var search = this.caseGroupSearch = this.caseGroupSearches[groupUrl] || ''

        search = StringUtil.isEmpty(search, true) ? null : '%' + StringUtil.trim(search).replaceAll('_', '\\_').replaceAll('%', '\\%') + '%'
        var req = {
          format: false,
          'Flow[]': {
            'count': count || 0,
            'page': page || 0,
            'Flow': {
              // '@from@': {
              //   'Flow': {
              //     '@raw': '@column',
              //     '@column': "substr(url,1,length(url)-length(substring_index(url,'/',-1))-1):groupUrl;group:groupName", // (CASE WHEN length(`group`) > 0 THEN `group` ELSE '-' END):name",
                  'userId': this.User.id,
                  'project': StringUtil.isEmpty(project, true) ? null : project,
              //     'group$': search,
              //     'url$': search,
              //     // 'url&$': StringUtil.isEmpty(groupUrl) ? null : [groupUrl.replaceAll('_', '\\_').replaceAll('%', '\\%') + '/%'],
              //     '@combine': search == null ? null : 'group$,url$',
              //     '@null': 'sqlauto', //'sqlauto{}': '=null',
              //     'url{}': 'length(url)>0',
              //     'url&$': StringUtil.isEmpty(groupUrl) ? null : [groupUrl.replaceAll('_', '\\_').replaceAll('%', '\\%') + '/%']
              //     // 'group{}': group == null || StringUtil.isNotEmpty(groupUrl) ? null : 'length(group)<=0' // SQL WHERE 条件不用别名
              //     // '@having': "length(url)>0" //  StringUtil.isEmpty(groupUrl) ? "length(url)>0" : "(url = '" + groupUrl.replaceAll("'", "\\'") + "')"
              //   }
              // },
              // 'groupUrl&$': StringUtil.isEmpty(groupUrl) ? null : [groupUrl.replaceAll('_', '\\_').replaceAll('%', '\\%') + '/%'],
              // 'groupName$': search,
              // 'groupUrl$': search,
              // '@combine': search == null ? null : 'groupName$,groupUrl$',
              // '@column': "groupName,groupUrl;any_value(groupName):rawName;length(groupName):groupNameLen;length(groupUrl):groupUrlLen;count(*):count",
              // '@group': 'groupName,groupUrl',
              // '@order': 'groupNameLen+,groupName-,groupUrlLen+,groupUrl+',
              '@order': 'time-',
            }
          },
          '@role': IS_NODE ? null : 'LOGIN',
          key: IS_NODE ? this.key : undefined  // 突破常规查询数量限制
        }

        if (IS_BROWSER) {
          this.onChange(false)
        }

        this.adminRequest('/get', req, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var data = res.data
          if (JSONResponse.isSuccess(data) == false) {
            alert('获取用例分组失败！\n' + (err != null ? err.message : (data || '').msg))
            if (IS_BROWSER) { // 解决一旦错了，就只能清缓存
              App.caseGroupCount = 50
              App.caseGroupPage = 0
              App.caseGroupSearch = ''
              App.caseGroupCounts = {}
              App.caseGroupPages = {}
              App.caseGroupSearches = {}

              App.saveCache(App.server, 'caseGroupCount', App.caseGroupCount)
              App.saveCache(App.server, 'caseGroupPage', App.caseGroupPage)
              App.saveCache(App.server, 'caseGroupSearch', App.caseGroupSearch)
              App.saveCache(App.server, 'caseGroupCounts', App.caseGroupCounts)
              App.saveCache(App.server, 'caseGroupPages', App.caseGroupPages)
              App.saveCache(App.server, 'caseGroupSearches', App.caseGroupSearches)
            }
            return
          }

          App.caseGroups = data['Flow[]'] || []
          App.remotes = App.testCases = []
          App.showTestCase(true, false, null)
        })
      },

      switchCaseShowType: function () {
        if (this.isLocalShow) {
          alert('只有远程用例才能切换！')
          return
        }

        if (this.isChainShow) {
            this.chainShowType = (this.chainShowType + 1)%3
            if (this.chainShowType != 1 && this.chainPaths.length <= 0 && this.chainGroups.length <= 0) {
              this.selectChainGroup(-1, null)
            }
            return
        }

        this.caseShowType = (this.caseShowType + 1)%3
        if (this.caseShowType != 1 && this.casePaths.length <= 0 && this.caseGroups.length <= 0) {
          this.selectCaseGroup(-1, null)
        }
      },

      onClickPathRoot: function () {
        var isChainShow = this.isChainShow
//        var paths = isChainShow ? this.chainPaths : this.casePaths
        var paths = isChainShow ? [] : this.casePaths
        if (paths.length <= 0) {
          var type = this.groupShowType = (this.groupShowType + 1)%3
          this.isChainShow = type == 1
          this.isLocalShow = type == 2

          this.testCases = this.remotes = []
          if (type == 1 && this.chainGroups.length <= 0) {
            this.selectChainGroup(-1, null)
          }
          else if (type == 0 && this.caseGroups.length <= 0) {
            this.selectCaseGroup(-1, null)
          }
          else {
            this.showTestCase(true, this.isLocalShow)
          }
        }
        else if (isChainShow) {
          this.selectChainGroup(-1, null)
        }
        else {
          this.selectCaseGroup(-1, null)
        }
      },

      getCaseCountStr: function() {
        var isChainShow = this.isChainShow
        var isCaseGroupShow = this.isCaseGroupShow()
        var isLocalShow = this.isLocalShow
        var caseShowType = this.caseShowType
        var caseGroups = (isChainShow ? this.chainGroups : this.caseGroups) || []
        var testCases = this.testCases || []

        if (isLocalShow) {
          return '(' + testCases.length + ')'
        }

        return '(' + (isCaseGroupShow ? caseGroups.length : '')
            + (caseShowType == 0 && isCaseGroupShow ? '|' : '')
            + (caseShowType == 2 && (isCaseGroupShow) ? '' : testCases.length) + ')';

        // 以下代码不知道为啥结果显示不对
        // var isCaseGroupShow = this.isCaseGroupShow()
        // var isCaseItemShow = this.isCaseItemShow()
        // var caseGroups = (this.isChainShow ? this.chainGroups : this.caseGroups) || []
        //
        // return '(' + (isCaseGroupShow ? caseGroups.length : '')
        //     + (isCaseGroupShow && isCaseItemShow ? '|' : '')
        //     + (isCaseItemShow ? '' : testCases.length) + ')';
      },

      //显示远程的测试用例文档
      showTestCase: function (show, isLocal, callback, group) {
        this.isTestCaseShow = show
        this.isLocalShow = isLocal

        if (IS_BROWSER) {
          vOutput.value = show ? '' : (output || '')
          this.showDoc()
        }

        if (isLocal) {
          this.testCases = this.locals || []
          return
        }

        this.testCases = this.remotes || []
        this.getCurrentSummary().summaryType = 'total' // this.onClickSummary('total', true)

        if (show) {
          var testCases = this.testCases
          var allCount = testCases == null ? 0 : testCases.length
          App.allCount = allCount
          if (allCount > 0) {
            if (! (this.isAllSummaryShow() || this.isCurrentSummaryShow())) {
              this.showCompare4TestCaseList(show)
            }
            return;
          }

          var project = (this.projectHost || {}).project

          // this.isTestCaseShow = false
          var reportId = this.reportId

          var methods = this.methods
          var types = this.types

          var isChainShow = this.isChainShow
          var paths = isChainShow ? this.chainPaths : this.casePaths
          var index = paths.length - 1
          group = group != null ? group : paths[index]
          var groupId = group == null ? 0 : (group.groupId || 0)
          var groupUrl = group == null ? '' : (group.groupUrl || '')
          var groupKey = isChainShow ? groupId + '' : groupUrl

          var page = this.testCasePage = this.testCasePages[groupKey] || 0
          var count = this.testCaseCount = this.testCaseCounts[groupKey] || 100
          var search = this.testCaseSearch = this.testCaseSearches[groupKey] || ''

          search = StringUtil.isEmpty(search, true) ? null : '%' + StringUtil.trim(search).replaceAll('_', '\\_').replaceAll('%', '\\%') + '%'

          var url = this.server + '/get'
          var userId = this.User.id

          this.coverage = {}
          this.view = 'markdown'
          var req = {
            format: false,
            '[]': {
              'count': count || 100, //200 条测试直接卡死 0,
              'page': page || 0,
              'join': isChainShow ? '&/Flow,@/Input,@/TestRecord' : '@/Device,@/System,@/Script:pre,@/Script:post,@/TestRecord',
              'Chain': isChainShow ? {
                // TODO 后续再支持嵌套子组合 'toGroupId': groupId,
                'groupId': groupId,
                '@column': "id,groupId,documentId,randomId,documentName,randomName,rank", // ;unix_timestamp(rank):rank",
                '@order': 'rank+,id+',
                'documentId>': 0
              } : null,
              'Flow': {
                'id@': isChainShow ? '/Chain/documentId' : null,
                // '@column': 'id,userId,versionCode,date,name,operation,method,type,url,request,apijson,standard', // ;substr(url,' + (StringUtil.length(groupUrl) + 2) + '):substr',
                '@order': 'versionCode-,time-',
                'userId': userId,
                'project': StringUtil.isEmpty(project, true) ? null : project,
                'name$': search,
                'detail$': search,
                // 'group{}': group == null || StringUtil.isNotEmpty(groupUrl) ? null : 'length(group)<=0',
                // 'group{}': group == null ? null : (group.groupName == null ? "=null" : [group.groupName]),
                '@combine': StringUtil.isEmpty(search) ? null : 'name$,detail$'
                // '@having': StringUtil.isEmpty(groupUrl) ? null : "substring_index(substr,'/',1)<0"
              },
              'Device': {
                'id@': '/Flow/deviceId'
              },
              'System': {
                'id@': '/Flow/systemId'
              },
              'Input': {
                // 'id@': '/Chain/randomId',
                'toId': 0, // isChainShow ? 0 : null,
                'chainId@': isChainShow ? '/Chain/id' : null,
                'flowId@': '/Flow/id',
                'userId': userId,
                '@column': 'count(*):count;sum(disable):disableCount',
                // '@raw': '@column',
                '@order': 'time-'
              },
              'Script:pre': {
                'ahead': 1,
                // 'testAccountId': 0,
                'chainId@': isChainShow ? '/Chain/id' : null,
                'chainId': isChainShow ? null : 0,
                'documentId@': '/Flow/id',
                '@order': 'date-'
              },
              'Script:post': {
                'ahead': 0,
                // 'testAccountId': 0,
                'chainId@': isChainShow ? '/Chain/id' : null,
                'chainId': isChainShow ? null : 0,
                'documentId@': '/Flow/id',
                '@order': 'date-'
              },
              'TestRecord': {
                'chainId@': isChainShow ? '/Chain/id' : null,
                'documentId@': '/Flow/id',
                'userId': userId,
                'host': StringUtil.isEmpty(baseUrl, true) ? null : baseUrl,
                'screenshot[>': 0,
                '@order': 'randomId+,date-',
                '@column': 'screenshot'
              }
            },
            '@role': IS_NODE ? null : 'LOGIN',
            key: IS_NODE ? this.key : undefined  // 突破常规查询数量限制
          }

          if (IS_BROWSER) {
            this.onChange(false)
          }

          this.adminRequest(url, req, {}, function (url, res, err) {
            App.isTestCaseShow = false
            if (callback) {
              callback(url, res, err)
              return
            }

            App.onTestCaseListResponse(show, url, res, err)
          })
        } else if (callback != null) {
          callback(null, {}, null)
        }
      },

      lastCaseReqTime: 0,
      onTestCaseListResponse: function(show, url, res, err) {
        var time = res == null || res.config == null || res.config.metadata == null ? 0 : (res.config.metadata.startTime || 0)
        if (time < this.lastCaseReqTime) {
          return
        }

        this.lastCaseReqTime = time;

        this.onResponse(url, res, err)

        var data = res.data

        if (JSONResponse.isSuccess(data)) {
          this.isTestCaseShow = true
          this.isLocalShow = false
          this.testCases = App.remotes = data['[]']
          this.getCurrentRandomSummary().summaryType = 'total' // App.onClickSummary('total', true)
          if (this.isChainShow && this.currentChainGroupIndex >= 0) {
            var chain = this.chainGroups[this.currentChainGroupIndex]
            if (chain != null) {
              chain['[]'] = this.testCases
            }
          }

          if (IS_BROWSER) {
            vOutput.value = show ? '' : (output || '')
            this.showDoc()
          }

          this.showCompare4TestCaseList(show)

          //this.onChange(false)
        } else if (IS_BROWSER) { // 解决一旦错了，就只能清缓存
          this.testCaseCount = 50
          this.testCasePage = 0
          this.testCaseSearch = ''
          this.testCasePages = {}
          this.testCaseCounts = {}
          this.testCaseSearches = {}

          this.saveCache(this.server, 'testCasePage', this.testCasePage)
          this.saveCache(this.server, 'testCaseCount', this.testCaseCount)
          this.saveCache(this.server, 'testCaseSearch', this.testCaseSearch)
          this.saveCache(this.server, 'testCasePages', null)
          this.saveCache(this.server, 'testCaseCounts', null)
          this.saveCache(this.server, 'testCaseSearches', null)
        }
      },

      onClickLogoutSummary: function (color) {
        this.onClickSummary(color, false, -1)
      },
      onClickAllSummary: function (color) {
        this.onClickSummary(color, false, this.accounts.length) // this.currentAccountIndex)
      },
      onClickCurrentSummary: function (color) {
        this.onClickSummary(color, false, this.currentAccountIndex)
      },
      onClickSummary: function (color, isRandom, accountIndex) {
        var isCur = this.currentAccountIndex == accountIndex
        if (! isCur) {
          this.onClickAccount(accountIndex, accountIndex < 0 ? this.logoutSummary : this.accounts[accountIndex])
        }
        // this.currentAccountIndex = accountIndex
        // this.isTestCaseShow = false

        var isSub = this.isRandomSubListShow
        var arr = isRandom ? (isSub ? this.currentRandomItem.subs : this.currentRemoteItem.randoms) : this.remotes;
        var list = []
        if (color == null || color == 'total') {
          list = arr
          if (isCur) {
            this.statisticsShowType = (this.statisticsShowType + 1)%3;
          }
        } else if (arr != null) {
          for (var i = 0; i < arr.length; i++) {
            var obj = arr[i]
            if (obj == null) {
              continue
            }

            var count = isRandom && obj.Input != null ? obj.Input.count : (isRandom ? null : obj.totalCount)
            if (count != null && count > (isRandom ? 1 : 0)) {
              var sum = obj[color + 'Count']
              if (sum != null && sum > 0) {
                list.push(obj)
              }
              continue
            }

            if (obj.compareColor == color) {
              list.push(obj)
            }
          }
        }

        if (isRandom) {
          if (isSub) {
            this.currentRandomItem.summaryType = color
            this.randomSubs = list
          } else {
            this.currentRemoteItem.summaryType = color
            this.randoms = list
          }
        } else {
          var summary = this.getSummary(accountIndex) || {}
          summary.summaryType = color
          this.testCases = list
          this.isTestCaseShow = true
          // this.showTestCase(true, false)
        }
      },

      showCompare4RandomList: function (show, isSub) {
        this.getCurrentRandomSummary().summaryType = 'total'

        var randoms = show ? (isSub ? this.randomSubs : this.randoms) : null
        var randomCount = randoms == null ? 0 : randoms.length
        if (randomCount > 0) {
          var accountIndex = (this.accounts[this.currentAccountIndex] || {}).isLoggedIn ? this.currentAccountIndex : -1
          this.currentAccountIndex = accountIndex  //解决 onTestResponse 用 -1 存进去， handleTest 用 currentAccountIndex 取出来为空
          var docId = ((this.currentRemoteItem || {}).Flow || {}).id

          var tests = (this.tests[String(accountIndex)] || {})[docId]
          if (tests != null && JSONObject.isEmpty(tests) != true) {
            // if (! isSub) {
            //   this.resetCount(this.currentRemoteItem, true, isSub, accountIndex)
            // }

            for (var i = 0; i < randomCount; i++) {
              var item = randoms[i]
              var r = item == null ? null : item.Input
              if (r == null || r.id == null) {
                continue
              }

              this.resetCount(item, true, isSub, accountIndex)

              var subCount = r.count || 0
              if (subCount == 1) {
                this.compareResponse(null, randomCount, randoms, i, item, tests[r.id], true, accountIndex, true)
              }
              else if (subCount > 1) {
                var subRandoms = item['[]'] || []
                var subSize = Math.min(subRandoms.length, subCount)
                for (var j = 0; j < subSize; j++) {
                  var subItem = subRandoms[j]
                  var sr = subItem == null ? null : subItem.Input
                  if (sr == null || sr.id == null) {
                    continue
                  }

                  this.compareResponse(null, subSize, subRandoms, j, subItem, tests[sr.id > 0 ? sr.id : (sr.toId + '' + sr.id)], true, accountIndex, true)
                }
              }
            }
          }
        }
      },

      //显示远程的随机配置文档
      showRandomList: function (show, item, isSub, callback) {
        this.operate = show && this.operate == OPERATE_TYPE_HTTP ? OPERATE_TYPE_REPLAY : this.operate
        this.isHeaderShow = show ? false : this.isHeaderShow
        this.isRandomEditable = false
        this.isRandomListShow = show && ! isSub
        this.isRandomSubListShow = show && isSub
        if (! isSub) {
          this.randomSubs = []
        }

        if (IS_BROWSER) {
          vOutput.value = show ? '' : (output || '')
          this.showDoc()
        }

        var randoms = []
        if (this.randomPage == 0 && ! isSub) {
          randoms = (this.currentRemoteItem || {}).randoms || []
        }
        else if (this.randomSubPage == 0 && isSub) {
          randoms = (this.currentRandomItem || {}).subs || []
        }

        if (isSub) {
          this.randomSubs = randoms
        }
        else {
          this.randoms = randoms
        }

        this.getCurrentRandomSummary().summaryType = 'total' // this.onClickSummary('total', true)
        if (! this.isRandomSummaryShow()) {
          this.showCompare4RandomList(show, isSub)
        }

        if (show && this.isRandomShow && randoms.length <= 0 && item != null && item.id != null) {
          this.isRandomListShow = false

          var subSearch = StringUtil.isEmpty(this.randomSubSearch, true)
            ? null : '%' + StringUtil.trim(this.randomSubSearch) + '%'
          var search = isSub ? subSearch : (StringUtil.isEmpty(this.randomSearch, true)
            ? null : '%' + StringUtil.trim(this.randomSearch) + '%')

          var url = this.server + '/get'
          baseUrl = this.getBaseUrl(this.projectHost.host, true)
          const cri = this.currentRemoteItem || {}
          const chain = cri.Chain || {}
          const cId = chain.id || 0

          var req = {
            '[]': {
              'count': (isSub ? this.randomSubCount : this.randomCount) || 100,
              'page': (isSub ? this.randomSubPage : this.randomPage) || 0,
              'Input': {
                'toId': isSub ? item.id : 0,
                'chainId': cId,
                'flowId': isSub ? null : item.id,
                '@order': "step+,time+,downTime+,eventTime+",
                "disable": 0, // disable 这个字段会导致报错
                // 'disable': this.type == null || this.type == OPERATE_TYPE_REPLAY ? 0 : null,
                'name$': search,
                // 导致奇怪的 Vue 报错 EditTextEvent is not defined
                // 'disable': (vRandomDisable || {}).checked ? null : 0
                // 'disable': this.isAllowDisable ? null : 0
              },
              'TestRecord': {
                'randomId@': '/Input/id',
                // 'inputId@': '/Input/id',
                // '@combine': 'randomId,inputId',
                'testAccountId': this.getCurrentAccountId(),
                'host': StringUtil.isEmpty(baseUrl, true) ? null : baseUrl,
                '@order': 'date-'
              }, // 暂时不支持子项
//               '[]': isSub ? null : {
//                 'count': this.randomSubCount || 100,
//                 'page': this.randomSubPage || 0,
//                 'Input': {
//                   'toId@': '[]/Input/id',
//                   'chainId': cId,
//                   'flowId': item.id,
//                   '@order': "time-",
//                   'name$': subSearch
//                 },
//                 'TestRecord': {
//                   'randomId@': '/Input/id',
// //                  'testAccountId': this.getCurrentAccountId(),
//                   'host': StringUtil.isEmpty(baseUrl, true) ? null : baseUrl,
//                   '@order': 'date-'
//                 }
//               }
            },
            key: IS_NODE ? this.key : undefined  // 突破常规查询数量限制
          }

          if (IS_BROWSER) {
            this.onChange(false)
          }

          this.adminRequest(url, req, {}, function (url, res, err) {
            if (callback) {
              callback(url, res, err)
              return
            }
            App.onRandomListResponse(show, isSub, url, res, err)
          })
        } else if (callback) {
          callback(null, {}, null)
        }
      },

      onRandomListResponse: function (show, isSub, url, res, err) {
        res = res || {}

        App.onResponse(url, res, err)

        var data = res.data

        if (JSONResponse.isSuccess(data)) {
          App.isRandomListShow = ! isSub
          App.isRandomSubListShow = isSub
          if (isSub) {
            if (App.currentRandomItem == null) {
              App.currentRandomItem = {}
            }
            App.randomSubs = App.currentRandomItem.subs = App.currentRandomItem['[]'] = data['[]']
          }
          else {
            if (App.currentRemoteItem == null) {
              App.currentRemoteItem = {}
            }
            App.randoms = App.currentRemoteItem.randoms = data['[]']
          }
          this.getCurrentRandomSummary().summaryType = 'total' // App.onClickSummary('total', true)

          if (IS_BROWSER) {
            vOutput.value = show ? '' : (output || '')
            App.showDoc()
          }

          // if (! this.isRandomSummaryShow()) {
            App.showCompare4RandomList(show, isSub)
          // }

          //App.onChange(false)
          App.summary()
        }
      },

      // 设置文档
      showDoc: function () {
        if (this.setDoc(doc) == false) {
          this.getDoc(function (d) {
            App.setDoc(d);
          });
        }
      },


      saveCache: function (url, key, value) {
        var cache = this.getCache(url);
        cache[key] = value
        localStorage.setItem('AutoUI:' + url, JSON.stringify(cache))
      },
      getCache: function (url, key, defaultValue) {
        var cache = localStorage.getItem('AutoUI:' + url)
        try {
          cache = parseJSON(cache)
        } catch(e) {
          this.log('login  this.send >> try { cache = parseJSON(cache) } catch(e) {\n' + e.message)
        }
        cache = cache || {}
        var val = key == null ? cache : cache[key]
        return val == null && defaultValue != null ? defaultValue : val
      },

      getCurrentDocumentId: function() {
        var d = (this.currentRemoteItem || {}).Flow
        return d == null ? null : d.id;
      },
      getCurrentRandomId: function() {
        var r = (this.currentRandomItem || {}).Input
        return r == null ? null : r.id;
      },
      getCurrentScriptBelongId: function() {
        return this.getScriptBelongId(this.scriptType)
      },
      getScriptBelongId: function(scriptType) {
        var st = scriptType;
        var bid = st == 'global' ? 0 : ((st == 'account' ? this.getCurrentAccountId() : this.getCurrentDocumentId()) || 0)
        return bid
      },
      listScript: function() {
        var req = {
          'Script:pre': {
            'ahead': 1,
            'testAccountId': 0,
            'documentId': 0,
            '@order': 'date-'
          },
          'Script:post': {
            'ahead': 0,
            'testAccountId': 0,
            'documentId': 0,
            '@order': 'date-'
          }
        }

        var accounts = this.accounts || []
        for (let i = 0; i < accounts.length; i++) {
          var a = accounts[i]
          var id = a == null ? null : a.id
          if (id == null) {
            continue
          }

          req['account_' + id] = { // 用数字被居然强制格式化到 JSON 最前
            'Script:pre': {
              'ahead': 1,
              'testAccountId': id,
              'documentId': 0,
              '@order': 'date-'
            },
            'Script:post': {
              'ahead': 0,
              'testAccountId': id,
              'documentId': 0,
              '@order': 'date-'
            }
          }
        }

        this.adminRequest('/get', req, {}, function (url, res, err) {
          var data = res.data
          if (JSONResponse.isSuccess(data) != true) {
            App.log(err != null ? err : (data == null ? '' : data.msg))
            return
          }

          var scripts = App.scripts || {}
          var ss = scripts.global
          if (ss == null) {
            scripts.global = ss = {}
          }

          var bs = ss['0'] || {}
          if (bs == null) {
            ss['0'] = bs = {}
          }

          var pre = data['Script:pre']
          if (pre != null && pre.script != null) {
            bs.pre = data['Script:pre']
          }
          var post = data['Script:post']
          if (post != null && post.script != null) {
            bs.post = data['Script:post']
          }

          // delete data['Script:pre']
          // delete data['Script:post']

          var cs = scripts.account
          if (cs == null) {
            scripts.account = cs = {}
          }

          for (let key in data) {
            var val = data[key]
            var pre = val == null || key.startsWith('account_') != true ? null : val['Script:pre']
            if (pre == null) {
              continue
            }

            var post = val['Script:post']

            var bs = cs[key.substring('account_'.length)]

            if (pre != null) { // && pre.script != null) {
              bs.pre = pre
            }
            if (post != null) { // && post.script != null) {
              bs.post = post
            }
          }

          App.scripts = Object.assign(newDefaultScript(), scripts)
        })
      },


      /**登录确认
       */
      confirm: function () {
        switch (this.loginType) {
          case 'login':
            this.login(this.isAdminOperation)
            break
          case 'register':
            this.register(this.isAdminOperation)
            break
          case 'forget':
            this.resetPassword(this.isAdminOperation)
            break
        }
      },

      showLogin: function (show, isAdmin) {
        this.isLoginShow = show
        this.isAdminOperation = isAdmin
        this.operate = OPERATE_TYPE_HTTP

        if (show != true) {
          return
        }

        var user = isAdmin ? this.User : null  // add account   this.accounts[this.currentAccountIndex]

        // alert("showLogin  isAdmin = " + isAdmin + "; user = \n" + JSON.stringify(user, null, '    '))
        if (user == null || StringUtil.isEmpty(user.account, true)) {
          user = {
            account: '13000082001',
            phone: '13000082001',
            password: '123456'
          }
        }

        this.setRememberLogin(user.remember)
        var account = this.account = JSONResponse.getAccount(user)
        var password = this.password = JSONResponse.getPassword(user)

        var schemas = StringUtil.isEmpty(this.schema, true) ? null : StringUtil.split(this.schema)
        var pkg = (this.getPackage(this.host) || 'uigo.x') + '.activity_fragment'
        var cls = 'LoginActivity'

        const req = isAdmin ? {
          type: 0, // 登录方式，非必须 0-密码 1-验证码
          // asDBAccount: ! isAdminOperation,  // 直接 /execute 接口传 account, password
          phone: account,
          password: password,
          version: 1, // 全局默认版本号，非必须
          remember: vRemember.checked,
          format: false,
          defaults: isAdmin ? { // FIXME /method/invoke
            key: IS_NODE ? this.key : undefined  // 突破常规查询数量限制
          } : {
            '@database': StringUtil.isEmpty(this.database, true) ? undefined : this.database,
            '@schema': schemas == null || schemas.length != 1 ? undefined : this.schema
          }
        } : {
          account: account,
          password: password,
          "package": pkg, // 'uiauto',
          "class": cls, // 'UIAutoApp',
          "classArgs": [],
          "reuse": true,
          "method": 'login',
          "methodArgs": ["int:0", account, password, {
            'type': 'uigo.x.HttpManager$OnHttpResponseListener',
            'value': {
              'onHttpResponse(int,String,Throwable)': {
                'callback': true
              }
            }
          }]
        }

        this.isHeaderShow = true
        this.isRandomShow = true
        this.isRandomListShow = false

        if (IS_BROWSER && ! isAdmin) {
          this.prevMethod = this.method
          this.prevType = this.type
          this.prevOperate = this.operate

          this.prevUrl = vUrl.value
          this.prevUrlComment = vUrlComment.value
          this.prevInput = vInput.value
          this.prevComment = vComment.value
          this.prevWarning = vWarning.value
          this.prevRandom = vRandom.value
          this.prevHeader = vHeader.value
          this.prevScript = vScript.value

          this.method = HTTP_METHOD_POST
          this.type = REQUEST_TYPE_JSON
          this.showUrl(isAdmin, '/login') // isAdmin ? '/login' : '/method/invoke')
          vInput.value = JSON.stringify(req, null, '    ')

          this.testRandomCount = 1
          vRandom.value = `remember: vRemember.checked\naccount: App.account || req.account\npassword: App.password || req.password\nmethodArgs/1: App.account || req.account\nmethodArgs/2: App.password || req.password`
           + (StringUtil.isPhone(this.account) ? '\nphone: App.account' : '') + (StringUtil.isEmail(this.account) ? '\nemail: App.account' : '')
        }

        this.scripts = newDefaultScript()
        this.method = HTTP_METHOD_POST
        // this.type = REQUEST_TYPE_JSON
        this.showTestCase(false, this.isLocalShow)
        if (IS_BROWSER) {
          this.onChange(false)
        }
      },

      setRememberLogin: function (remember) {
        vRemember.checked = remember || false
      },

      getCurrentAccount: function() {
        return this.accounts == null ? null : this.accounts[this.currentAccountIndex]
      },
      getCurrentAccountId: function() {
        var a = this.getCurrentAccount()
        return a != null && a.isLoggedIn ? a.id : null
      },

      /**登录
       */
      login: function (isAdminOperation, callback, isTab) {
        this.isEditResponse = false
        var schemas = StringUtil.isEmpty(this.schema, true) ? null : StringUtil.split(this.schema)

        const isLoginShow = this.isLoginShow
        var curUser = this.getCurrentAccount() || {}

        var pkg = (this.getPackage(this.host) || 'uigo.x') + '.activity_fragment'
        const account = isLoginShow ? this.account : JSONResponse.getAccount(curUser)
        const password = isLoginShow ? this.password : JSONResponse.getPassword(curUser)
        var cls = 'LoginActivity'
        const req = isAdminOperation ? {
          type: 0, // 登录方式，非必须 0-密码 1-验证码
          // asDBAccount: ! isAdminOperation,  // 直接 /execute 接口传 account, password
          phone: account,
          password: password,
          version: 1, // 全局默认版本号，非必须
          remember: vRemember.checked,
          format: false,
          defaults: isAdminOperation ? {
            key: IS_NODE ? this.key : undefined  // 突破常规查询数量限制
          } : {
            '@database': StringUtil.isEmpty(this.database, true) ? undefined : this.database,
            '@schema': schemas == null || schemas.length != 1 ? undefined : this.schema
          }
        } : {
          account: account,
          password: password,
          "package": pkg, // 'uiauto',
          "class": cls, // 'UIAutoApp',
          "classArgs": [], // "String:" + account],
          "reuse": true,
          "method": 'login',
          "methodArgs": ["int:0", account, password, {
            'type': 'uigo.x.HttpManager$OnHttpResponseListener',
            'value': {
              'onHttpResponse(int,String,Throwable)': {
                'callback': true
              }
            }
          }]
        }

        if (isAdminOperation) {
          this.isLoginShow = false
          this.request(isAdminOperation, HTTP_METHOD_POST, REQUEST_TYPE_JSON, this.server + '/login', req, this.getHeader(vHeader.value), function (url, res, err) {
            if (callback) {
              callback(url, res, err)
              return
            }

            App.onLoginResponse(isAdminOperation, req, url, res, err);
          })
        }
        else {
          function recover() {
            App.isLoginShow = false

            if (App.prevUrl != null) {
              App.method = App.prevMethod || HTTP_METHOD_POST
              App.type = App.prevType || REQUEST_TYPE_JSON
              App.operate = App.prevOperate || OPERATE_TYPE_REPLAY

              vUrl.value = App.prevUrl || (baseUrl + '/login') // '/method/invoke')
              vUrlComment.value = App.prevUrlComment || ''
              vComment.value = App.prevComment || ''
              vWarning.value = App.prevWarning || ''
              vInput.value = App.prevInput || '{}'
              vRandom.value = App.prevRandom || ''
              vHeader.value = App.prevHeader || ''
              vScript.value = App.prevScript || ''

              App.prevUrl = null
            }
          }

          const baseUrl = this.getBaseUrl()

          if (IS_BROWSER && callback == null) {
            var item
            for (var i in this.accounts) {
              item = this.accounts[i]
              if (item != null && baseUrl == item.baseUrl && JSONResponse.getAccount(item) == account) {
                recover()
                alert(req.phone +  ' 已在测试账号中！')
                // this.currentAccountIndex = i
                item.remember = vRemember.checked
                this.onClickAccount(i, item)
                return
              }
            }
          }

          this.scripts = newDefaultScript()

          const loginMethod = (isLoginShow ? this.method : curUser.loginMethod) || HTTP_METHOD_POST
          const loginType = (isLoginShow ? REQUEST_TYPE_JSON : curUser.loginType) || REQUEST_TYPE_JSON
          const loginUrl = (isLoginShow ? this.getBranchUrl() : curUser.loginUrl) || '/login'
          const loginReq = (isLoginShow ? this.getRequest(vInput.value) : curUser.loginReq) || req
          const loginRandom = (isLoginShow ? vRandom.value : curUser.loginRandom) || ''
          const loginHeader = (isLoginShow ? this.getHeader(vHeader.value) : curUser.loginHeader) || {}

          function loginCallback(url, res, err, random) {
            recover()
            if (callback) {
              callback(url, res, err)
            } else {
              App.onLoginResponse(isAdminOperation, req, url, res, err, loginMethod, loginType, loginUrl, loginReq, loginRandom, loginHeader)
            }

            if (App.prevUrl != null) {
              App.method = App.prevMethod || HTTP_METHOD_POST
              App.type = App.prevType || REQUEST_TYPE_JSON
              App.operate = App.prevOperate || OPERATE_TYPE_REPLAY

              vUrl.value = App.prevUrl || (baseUrl + '/login') // '/method/invoke')
              vUrlComment.value = App.prevUrlComment || ''
              vComment.value = App.prevComment || ''
              vWarning.value = App.prevWarning || ''
              vInput.value = App.prevInput || '{}'
              vRandom.value = App.prevRandom || ''
              vHeader.value = App.prevHeader || ''
              vScript.value = App.prevScript || ''

              App.prevUrl = null
            }
          }

          if (isLoginShow) {
            this.isLoginShow = false

            // this.testRandomWithText(true, loginCallback)
            // loginCallback()
            // return
          }

          this.scripts = newDefaultScript()
          this.parseRandom(loginReq, loginHeader, loginRandom, 0, true, false, false, function(randomName, constConfig, constJson) {
              App.request(isAdminOperation, loginMethod, loginType, baseUrl + loginUrl, constJson, loginHeader, function (url, res, err) {
                if (App.isEnvCompareEnabled != true) {
                  loginCallback(url, res, err, null, loginMethod, loginType, loginUrl, constJson, loginHeader)
                  return
                }

                App.request(isAdminOperation, loginMethod, loginType, App.getBaseUrl(App.otherEnv) + loginUrl
                    , loginReq, loginHeader, function(url_, res_, err_) {
                      var data = res_.data
                      var user = data.user || data.userObj || data.userObject || data.userRsp || data.userResp || data.userBean || data.userData || data.data || data.User || data.Data
                      if (user != null) {
                        var headers = res.headers || {}
                        App.otherEnvTokenMap[req.phone + '@' + baseUrl] = headers.token || headers.Token || data.token || data.Token || user.token || user.Token
                        App.otherEnvCookieMap[req.phone + '@' + baseUrl] = res.cookie || headers.cookie || headers.Cookie || headers['set-cookie'] || headers['Set-Cookie']
                        App.saveCache(App.otherEnv, 'otherEnvTokenMap', App.otherEnvTokenMap)
                        App.saveCache(App.otherEnv, 'otherEnvCookieMap', App.otherEnvCookieMap)
                      }

                      if (callback) {
                        callback(url, res, err)
                        return
                      }

                      App.onResponse(url_, res_, err_);
                      App.onLoginResponse(isAdminOperation, req, url, res, err, loginMethod, loginType, loginUrl, constJson, loginRandom, loginHeader)
                    }, App.scripts)
              })
          })
        }
      },

      onLoginResponse: function(isAdmin, req, url, res, err, loginMethod, loginType, loginUrl, loginReq, loginRandom, loginHeader) {
        req = req || {}
        res = res || {}
        if (isAdmin) {
          var data = res.data || {}

          var user = data.user || data.userObj || data.userObject || data.userRsp || data.userResp || data.userBean || data.userData || data.data || data.User || data.Data
          if (user == null) {
            alert('登录失败，请检查网络后重试。\n' + data.msg + '\n详细信息可在浏览器控制台查看。')
            this.onResponse(url, res, err)
          }
          else {
            if (user != null) {
              var headers = res.headers || {}
              var phone = JSONResponse.getPhone(req) || JSONResponse.getPhone(user)
              var email = JSONResponse.getEmail(req) || JSONResponse.getEmail(user)
              user.remember = data.remember
              user.phone = phone || (StringUtil.isPhone(this.account) ? this.account : null)
              user.email = email || (StringUtil.isEmail(this.account) ? this.account : null)
              user.account = JSONResponse.getAccount(req) || user.phone || user.email || this.account
              user.password = JSONResponse.getPassword(req) || this.password
              user.token = headers.token || headers.Token || data.token || data.Token || user.token || user.Token || user.accessToken || user.access_token || data.accessToken || data.access_token
              user.cookie = res.cookie || headers.cookie || headers.Cookie || headers['set-cookie'] || headers['Set-Cookie']
              this.User = user
            }

            //保存User到缓存
            this.saveCache(this.server, 'User', user)

            if (this.currentAccountIndex == null || this.currentAccountIndex < 0) {
              this.currentAccountIndex = 0
            }
            var item = this.accounts[this.currentAccountIndex] || {}
            item.isLoggedIn = false
            this.onClickAccount(this.currentAccountIndex, item) //自动登录测试账号

            if (user.id > 0) {
              if (this.isChainShow && this.chainShowType != 1 && this.chainPaths.length <= 0 && this.chainGroups.length <= 0) {
                this.selectChainGroup(-1, null)
              }
              if (this.caseShowType != 1 && this.casePaths.length <= 0 && this.caseGroups.length <= 0) {
                this.selectCaseGroup(-1, null)
              }
              this.showTestCase(true, false)
            }
          }
        } else {
          this.onResponse(url, res, err)

          //由login按钮触发，不能通过callback回调来实现以下功能
          var resData = res.data || {}
          var data = parseJSON(resData['return'] || JSONResponse.getValByPath(resData, StringUtil.split('methodArgs/3/value/call(){}/onHttpResponse(int,String,Throwable)/0/methodArgs/1/value', '/'))) || resData
          var user = JSONResponse.isObject(data) ? data.user || data.User || data.data || data.info || data.member || data : data
          if (JSONResponse.isObject(user)) { //  || typeof data[JSONResponse.KEY_CODE] == 'undefined') {
            var headers = res.headers || {}
            var phone = JSONResponse.getPhone(req) || JSONResponse.getPhone(user) || (StringUtil.isPhone(this.account) ? this.account : null)
            var email = JSONResponse.getEmail(req) || JSONResponse.getEmail(user) || (StringUtil.isEmail(this.account) ? this.account : null)
            this.accounts.push({
              isLoggedIn: true,
              baseUrl: this.getBaseUrl(),
              id: JSONResponse.getId(user),
              name: JSONResponse.getName(user),
              phone: phone,
              email: email,
              account: JSONResponse.getAccount(req) || phone || email,
              password: JSONResponse.getPassword(req) || this.password,
              remember: data.remember,
              loginMethod: loginMethod,
              loginType: loginType,
              loginUrl: loginUrl,
              loginReq: loginReq,
              loginRandom: loginRandom,
              loginHeader: loginHeader,
              cookie: res.cookie || headers.cookie,
              token: headers.token || headers.Token || data.token || data.Token || user.token || user.Token || user.accessToken || user.access_token || data.accessToken || data.access_token
            })

            var lastItem = App.accounts[App.currentAccountIndex]
            if (lastItem != null) {
              lastItem.isLoggedIn = false
            }

            App.currentAccountIndex = App.accounts.length - 1
            App.saveAccounts()
            App.listScript()
          }
        }
      },

      /**注册
       */
      register: function (isAdminOperation) {
        this.scripts = newDefaultScript()
        this.showUrl(isAdminOperation, '/register')
        vInput.value = JSON.stringify(
          {
            Privacy: {
              phone: this.account,
              _password: this.password
            },
            User: {
              name: 'APIJSONUser'
            },
            verify: vVerify.value
          },
          null, '    ')
        this.showTestCase(false, false)
        this.onChange(false)
        this.send(isAdminOperation, function (url, res, err) {
          App.onResponse(url, res, err)

          var data = res.data

          if (JSONResponse.isSuccess(data)) {
            alert('注册成功')

            var privacy = data.Privacy || {}

            App.account = privacy.phone
            App.loginType = 'login'
          }
        }, this.scripts)
      },

      /**重置密码
       */
      resetPassword: function (isAdminOperation) {
        this.scripts = newDefaultScript()
        this.showUrl(isAdminOperation, '/put/password')
        vInput.value = JSON.stringify(
          {
            verify: vVerify.value,
            Privacy: {
              phone: this.account,
              _password: this.password
            }
          },
          null, '    ')
        this.showTestCase(false, this.isLocalShow)
        this.onChange(false)
        this.send(isAdminOperation, function (url, res, err) {
          App.onResponse(url, res, err)

          var data = res.data

          if (JSONResponse.isSuccess(data)) {
            alert('重置密码成功')

            var privacy = data.Privacy || {}

            App.account = privacy.phone
            App.loginType = 'login'
          }
        }, this.scripts)
      },

      /**退出
       */
      logout: function (isAdminOperation, callback) {
        this.isEditResponse = false
        var req = {}

        if (isAdminOperation) {
          // alert('logout  isAdminOperation  this.saveCache(this.server, User, {})')
          this.delegateId = null
          this.saveCache(this.server, 'delegateId', null)

          this.saveCache(this.server, 'User', {})
        }

        // alert('logout  isAdminOperation = ' + isAdminOperation + '; url = ' + url)
        if (isAdminOperation) {
          this.adminRequest('/logout', req, this.getHeader(vHeader.value), function (url, res, err) {
            if (callback) {
              callback(url, res, err)
              return
            }

            // alert('logout  clear admin ')

            App.clearUser()
            App.onResponse(url, res, err)
            App.showTestCase(false, App.isLocalShow)
          })
        }
        else {
          this.scripts = newDefaultScript()
          // this.showUrl(isAdminOperation, '/logout')
          // vInput.value = JSON.stringify(req, null, '    ')
          // this.method = HTTP_METHOD_POST
          // this.type = REQUEST_TYPE_JSON
          this.showTestCase(false, this.isLocalShow)
          this.onChange(false)
          // this.send(isAdminOperation, function (url, res, err) {
          this.request(isAdminOperation, HTTP_METHOD_POST, REQUEST_TYPE_JSON, '/logout'
              , req, this.getHeader(vHeader.value), function (url_, res_, err_) {
            if (App.isEnvCompareEnabled != true) {
              if (callback) {
                callback(url, res, err)
              }
              return
            }

            App.request(isAdminOperation, HTTP_METHOD_POST, REQUEST_TYPE_JSON, App.getBaseUrl(App.otherEnv) + '/logout'
                , req, App.getHeader(vHeader.value), function (url_, res_, err_) {
              if (callback) {
                callback(url, res, err)
                return
              }
            })

          }, this.scripts)
        }
      },

      /**获取验证码
       */
      getVerify: function (isAdminOperation) {
        // this.scripts = newDefaultScript()
        this.showUrl(isAdminOperation, '/post/verify')
        var type = this.loginType == 'login' ? 0 : (this.loginType == 'register' ? 1 : 2)
        vInput.value = JSON.stringify(
          {
            type: type,
            phone: this.account
          },
          null, '    ')
        this.showTestCase(false, this.isLocalShow)
        this.onChange(false)
        this.send(isAdminOperation, function (url, res, err) {
          App.onResponse(url, res, err)

          var data = res.data || {}
          var obj = JSONResponse.isSuccess(data) ? data.verify : null
          var verify = obj == null ? null : obj.verify
          if (verify != null) { //FIXME isEmpty校验时居然在verify=null! StringUtil.isEmpty(verify, true) == false) {
            vVerify.value = verify
          }
        }, this.scripts)
      },

      clearUser: function () {
        this.User.id = 0
        this.Privacy = {}
        this.casePaths = []
        this.caseGroups = []
        this.remotes = []
        // 导致刚登录成功就马上退出 this.delegateId = null
        this.saveCache(this.server, 'User', this.User) //应该用lastBaseUrl,baseUrl应随watch输入变化重新获取
        // this.saveCache(this.server, 'delegateId', this.delegateId) //应该用lastBaseUrl,baseUrl应随watch输入变化重新获取
      },

      /**计时回调
       */
      onHandle: function (before) {
        if (IS_NODE) {
          return;
        }

        this.isDelayShow = false
        if (inputted != before) {
          clearTimeout(handler);
          return;
        }

        this.view = 'output';
        vComment.value = '';
        vWarning.value = '';
        vUrlComment.value = '';
        vOutput.value = 'resolving...';

        //格式化输入代码
        try {
          try {
            this.header = this.getHeader(vHeader.value)
          } catch (e2) {
            this.isHeaderShow = true
            vHeader.select()
            throw new Error(e2.message)
          }

          before = StringUtil.trim(before);

          var afterObj;
          var after;
          var code = '';

          if (StringUtil.isEmpty(before)) {
            afterObj = {};
            after = '';
          } else {
            before = StringUtil.trim(before); // this.toDoubleJSON(StringUtil.trim(before));
            log('onHandle  before = \n' + before);

            var json = isSingle ? this.switchQuote(before) : before;
            try {
              afterObj = jsonlint.parse(json);
              after = JSON.stringify(afterObj, null, "    ");
              before = isSingle ? this.switchQuote(after) : after;
            }
            catch (e) {
              log('main.onHandle', 'try { return jsonlint.parse(before); \n } catch (e) {\n' + e.message)
              log('main.onHandle', 'return jsonlint.parse(this.removeComment(before));')

              try {
                afterObj = JSON5.parse(json);  // jsonlint.parse(this.removeComment(before));
                after = JSON.stringify(afterObj, null, "    ");
              } catch (e2) {
                throw new Error('请求 JSON 格式错误！请检查并编辑请求！\n\n如果JSON中有注释，请 手动删除 或 点击左边的 \'/" 按钮 来去掉。\n\n' + e.message + '\n\n' + e2.message)
              }
            }

            //关键词let在IE和Safari上不兼容
            if (this.isEditResponse != true) {
              try {
                code = this.getCode(after); //必须在before还是用 " 时使用，后面用会因为解析 ' 导致失败
              } catch (e) {
                code = '\n\n\n建议:\n使用其它浏览器，例如 谷歌Chrome、火狐FireFox 或者 微软Edge， 因为这样能自动生成请求代码.'
                  + '\nError:\n' + e.message + '\n\n\n';
              }
            }

            var selectionStart = vInput.selectionStart
            var selectionEnd = vInput.selectionEnd
            vInput.value = before
              + '\n\n\n                                                                                                       '
              + '                                                                                                       \n';  //解决遮挡

            vInput.selectionStart = selectionStart
            vInput.selectionEnd = selectionEnd
            vInput.setSelectionRange(selectionStart, selectionEnd)
          }

          vSend.disabled = false;

          if (this.isEditResponse != true) {
            vOutput.value = output = '登录后点 ↑ 上方左侧最后图标按钮可查看用例列表，点上方右侧中间图标按钮可上传用例并且添加到列表中 ↑ \nOK，请点左上方 [开始] 按钮来测试。[点击这里查看视频教程](https://i.youku.com/i/UNTg1NzI1MjQ4MA==/videos?spm=a2hzp.8244740.0.0)' + code;

            this.showDoc()
          }

          var docKey = this.isEditResponse ? 'TestRecord' : 'Flow';
          var currentItem = (this.currentRemoteItem || {})[docKey] || {}
          var detail = currentItem.detail;
          var extraComment = this.getExtraComment()

          try {
            var standardObj = null;
            try {
              standardObj = parseJSON(currentItem.standard);
            } catch (e3) {
              log(e3)
            }

            var isAPIJSONRouter = false;
            try {
              var apijson = parseJSON(currentItem.apijson);
              isAPIJSONRouter = JSONResponse.isObject(apijson)
            } catch (e3) {
              log(e3)
            }

            var m = this.getMethod();
            var w = isSingle || this.isEditResponse ? '' : StringUtil.trim(CodeUtil.parseComment(after, docObj == null ? null : docObj['[]'], m, this.database, this.language, this.isEditResponse != true, standardObj, null, true, isAPIJSONRouter));
            var c = isSingle ? '' : StringUtil.trim(CodeUtil.parseComment(after, docObj == null ? null : docObj['[]'], m, this.database, this.language, this.isEditResponse != true, standardObj, null, null, isAPIJSONRouter));

            //TODO 统计行数，补全到一致 vInput.value.lineNumbers
            if (isSingle != true) {
              if (afterObj.tag == null) {
                m = m == null ? 'GET' : m.toUpperCase()
                if (['GETS', 'HEADS', 'POST', 'PUT', 'DELETE'].indexOf(m) >= 0) {
                  w += ' ! 非开放请求必须设置 tag ！例如 "tag": "User"'
                  c += ' ! 非开放请求必须设置 tag ！例如 "tag": "User"'
                }
              }

              if (StringUtil.isEmpty(detail, true)) {
                c += extraComment == null ? '' : ('\n\n/*' + extraComment + '\n*/');
              } else {
                c += '\n\n/*' + (extraComment == null ? '' : extraComment + '\n\n') + detail + '\n*/';
              }
            }


            vWarning.value = w
              + '\n\n\n                                                                                                       '
              + '                                                                                                       \n';  //解决遮挡
            vComment.value = c
              + '\n\n\n                                                                                                       '
              + '                                                                                                       \n';  //解决遮挡

            vUrlComment.value = isSingle || StringUtil.isEmpty(this.urlComment, true)
              ? '' : vUrl.value+ ' // ' + (this.requestVersion > 0 ? 'V' + this.requestVersion : 'V*') + ' ' + this.urlComment;

            if (! isSingle) {
              var method = this.getMethod();  // m 已经 toUpperCase 了
              var isRestful = ! JSONObject.isAPIJSONPath(method);
              if (isRestful != true) {
                method = method.toUpperCase();
              }
              var apiMap = isRestful ? CodeUtil.thirdPartyApiMap : null;
              var api = apiMap == null ? null : apiMap['/' + method];
              var name = api == null ? null : api.name;
              if (StringUtil.isEmpty(name, true) == false) {
                this.urlComment = name;
                vUrlComment.value = vUrl.value+ ' // ' + (this.requestVersion > 0 ? 'V' + this.requestVersion : 'V*') + ' ' + this.urlComment
              }
            }

            onScrollChanged()
            onURLScrollChanged()
          } catch (e) {
            log('onHandle   try { vComment.value = CodeUtil.parseComment >> } catch (e) {\n' + e.message);
          }

          if (this.isPreviewEnabled) {
            try {
              // 去掉前面的 JSON
              var raw = StringUtil.trim(isSingle ? vInput.value : vComment.value);
              var start = raw.lastIndexOf('\n/*')
              var end = raw.lastIndexOf('\n*/')
              var ct = start < 0 || end <= start ? '' : StringUtil.trim(raw.substring(start + '\n/*'.length, end))

              markdownToHTML('```js\n' + (start < 0 || end <= start ? raw : raw.substring(0, start)) + '\n```\n'
                + (StringUtil.isEmpty(ct, true) ? '' : ct + '\n\n```js\n' + ct + '\n```\n'), true);
            } catch (e3) {
              log(e3)
            }
          }

          if (this.isEditResponse) {
            this.view = 'code';
            this.jsoncon = after
          }

        } catch(e) {
          log(e)
          vSend.disabled = true

          this.view = 'error'
          this.error = {
            msg: e.message
          }
        }
      },


      /**输入内容改变
       */
      onChange: function (delay) {
        this.setBaseUrl();

        if (IS_NODE || document.activeElement == vOption || this.options.length > 0) {
          return;
        }

        inputted = StringUtil.get(vInput.value);
        vComment.value = '';
        vWarning.value = '';
        vUrlComment.value = '';

        clearTimeout(handler);

        this.isDelayShow = delay;

        if (delay) {
          handler = setTimeout(function () {
            App.onHandle(inputted);
          }, 2000);
        } else {
          this.onHandle(inputted);
        }
      },

      /**单双引号切换
       */
      transfer: function () {
        isSingle = ! isSingle;
        
        vInput.value = this.switchQuote(vInput.value);
        
        this.isVideoFirst = isSingle;
        this.isTestCaseShow = false;

        // // 删除注释 <<<<<<<<<<<<<<<<<<<<<
        //
        // var input = this.removeComment(vInput.value);
        // if (vInput.value != input) {
        //   vInput.value = input
        // }
        //
        // // 删除注释 >>>>>>>>>>>>>>>>>>>>>

        this.onChange(false);

        var list = docObj == null ? null : docObj['[]'];
        if (list != null && list.length > 0) {
          this.onDocumentListResponse('', {data: docObj}, null, function (d) {
            App.setDoc(d);
          });
        }
      },

      isShowMethod: function() {
        return this.operate == OPERATE_TYPE_HTTP && (this.methods == null || this.methods.length != 1)
      },
      isShowType: function() {
        return this.operate == OPERATE_TYPE_HTTP && (this.types == null || this.types.length != 1)
      },

      /**请求类型切换
       */
      changeMethod: function (method) {
        if (StringUtil.isNotEmpty(method)) {
          CodeUtil.method = this.method = method
        } else {
        var methods = this.methods
        var count = methods == null ? 0 : methods.length
        if (count <= 0) {
          methods = HTTP_METHODS
          count = HTTP_METHODS.length
        }

        if (count > 1) {
          var index = methods.indexOf(this.method) + 1
          CodeUtil.method = this.method = methods[index % count]
        }
        }
        this.onChange(false);
      },

      /**获取显示的请求方法名称
       */
      getMethodName: function (method, type) {
        method = StringUtil.trim(method)
        if (method.length > 0) {
          return method
        }

        return [HTTP_METHOD_GET, REQUEST_TYPE_PARAM].indexOf(type) < 0 ? HTTP_METHOD_POST : HTTP_METHOD_GET
      },
      /**获取显示的请求类型名称
       */
      getTypeName: function (type, method) {
        var t = type
        if (StringUtil.isEmpty(t, true)) {
          if (StringUtil.isEmpty(method, true)) {
            t = REQUEST_TYPE_JSON
          }
          else if (method == HTTP_METHOD_GET) {
            t = REQUEST_TYPE_PARAM
          }
          else if (method == HTTP_METHOD_POST) {
            t = REQUEST_TYPE_JSON
          }
          else {
            t = REQUEST_TYPE_DATA
          }
        }

//        var methods = this.methods
//        if (this.isShowMethod()) {
//          return t
//        }
//
//        var ts = this.types
//        if (ts == null || ts.length <= 1 || (ts.length <= 2 && ts.indexOf(REQUEST_TYPE_PARAM) >= 0 && ts.indexOf(REQUEST_TYPE_GRPC) < 0)) {
//          return t == REQUEST_TYPE_PARAM ? 'GET' : 'POST'
//        }
        return t
      },
      /**请求类型切换
       */
      changeType: function (type) {
        if (StringUtil.isNotEmpty(type)) {
          CodeUtil.type = this.type = type
        } else {
        var types = this.types
        var count = types == null ? 0 : types.length
        if (count <= 0) {
          types = HTTP_CONTENT_TYPES
          count = HTTP_CONTENT_TYPES.length
        }

        if (count > 1) {
          var index = types.indexOf(this.type) + 1
          this.type = types[index % count]
          CodeUtil.type = this.type;
          }
        }

        var url = StringUtil.trim(vUrl.value).replaceAll('\n', '')
        var index = url.indexOf('?')
        if (index >= 0) {
          var paramObj = getRequestFromURL(url.substring(index), true)
          vUrl.value = url.substring(0, index)
          if (paramObj != null && JSONObject.isEmpty(paramObj) == false) {
            var originVal = this.getRequest(vInput.value, {});
            var isConflict = false;

            if (JSONObject.isEmpty(originVal) == false) {
              for (var k in paramObj) {
                if (originVal.hasOwnProperty(k)) {
                  isConflict = true;
                  break;
                }
              }
            }

            if (isConflict) {
              vInput.value = JSON.stringify(paramObj, null, '    ') + '\n\n// FIXME 从 URL 上的参数转换过来，需要与下面原来的字段合并为一个 JSON：\n\n' + StringUtil.get(vInput.value)
            }
            else {
              vInput.value = JSON.stringify(Object.assign(originVal, paramObj), null, '    ')
            }
          }
          clearTimeout(handler)  //解决 vUrl.value 和 vInput.value 变化导致刷新，而且会把 vInput.value 重置，加上下面 onChange 再刷新就卡死了
        }

        this.onChange(false);
      },

      /**获取显示的操作类型名称
       */
      getOperateName: function (operate) {
        operate = operate || OPERATE_TYPE_REPLAY
        return operate == OPERATE_TYPE_HTTP ? 'HTTP' : (operate == OPERATE_TYPE_REVIEW ? '查看' : (operate == OPERATE_TYPE_REPLAY ? '回放' : '录制'))
      },
      /**操作类型切换
       */
      changeOperate: function (operate) {
        if (StringUtil.isNotEmpty(operate)) {
          this.operate = operate
        } else {
          var operates = this.operates || [OPERATE_TYPE_RECORD, OPERATE_TYPE_REVIEW, OPERATE_TYPE_REPLAY]
          var count = operates.length
          if (count > 1) {
            var index = operates.indexOf(this.operate)
            index++;
            this.operate = operates[index % count]
          }
        }

        this.onChange(false);

        if (this.operate == OPERATE_TYPE_RECORD) {
          this.isRandomListShow = false
          this.randoms = []
          this.isRandomListShow = true
        }
        else {
          this.showRandomList(true, (this.currentRemoteItem || {}).Flow)
        }
      },

      changeScriptType: function (type) {
        type = type || 'case'
        if (type == 'account') {
          var id = this.getCurrentAccountId()
          if (id == null || id <= 0) {
            type = 'case'
          }
        }

        this.scriptBelongId = 0 // 解决可能的报错
        this.scriptType = type
        var bid = this.getCurrentScriptBelongId()

        var scripts = this.scripts
        if (scripts == null) {
          scripts = newDefaultScript()
          this.scripts = scripts
        }
        var ss = scripts[type]
        if (ss == null) {
          ss = {
            0: {
              pre: { // 可能有 id
                script: '' // index.html 中 v-model 绑定，不能为 null
              },
              post: {
                script: ''
              }
            },
            [bid]: {
              pre: { // 可能有 id
                script: '' // index.html 中 v-model 绑定，不能为 null
              },
              post: {
                script: ''
              }
            }
          }
          scripts[type] = ss
        }

        var bs = ss[bid]
        if (bs == null) {
          bs = {
            pre: { // 可能有 id
              script: '' // index.html 中 v-model 绑定，不能为 null
            },
            post: {
              script: ''
            }
          }
          ss[bid] = bs
        }
        var pre = bs.pre
        if (pre == null) {
          pre = {
            script: ''
          }
          bs.pre = pre
        }
        if (pre.script == null) {
          pre.script = ''
        }

        var post = bs.post
        if (post == null) {
          post = {
            script: ''
          }
          bs.post = post
        }
        if (post.script == null) {
          post.script = ''
        }

        this.scriptBelongId = bid
      },
      changeScriptPriority: function (isPre) {
        this.isPreScript = isPre == true
        this.changeScriptType(this.scriptType)
      },

      /**
       * 删除注释
       */
      removeComment: function (json) {
        var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)/g // 正则表达式
        try {
          return StringUtil.get(json).replace(reg, function(word) { // 去除注释后的文本
            return /^\/{2,}/.test(word) || /^\/\*/.test(word) ? "" : word;
          })
        } catch (e) {
          log('transfer  delete comment in json >> catch \n' + e.message);
        }
        return json;
      },

      handleFileSelect: function(event) {
        const isSub = this.isRandomSubListShow;
        const items = (isSub ? this.randomSubs : this.randoms) || [];
        const cri = this.currentRandomItem || {}
        const doc = (this.currentRemoteItem || {}).Flow || {}
        const random = cri.Input || {}
        const ind = isSub && this.currentRandomSubIndex != null ? this.currentRandomSubIndex : this.currentRandomIndex;
        const index = ind != null && ind >= 0 ? ind : -1; // items.length;
        const server = this.server

        var selectedFiles = Array.from(event.target.files);
        // const previewList = document.getElementById('previewList');
        // previewList.innerHTML = '';

        selectedFiles.forEach((file, i) => {
          const reader = new FileReader();
          reader.onload = (evt) => {
            // const img = document.createElement('img');
            // img.src = evt.target.result;
            // img.style.height = '100%';
            // img.style.margin = '1px';
            // previewList.appendChild(img);

            function callback(file, img, name, size, width, height, index, rank) {

              var item = JSONResponse.deepMerge({
                Input: {
                  id: -(index || 0) - 1, //表示未上传
                  toId: random.id,
                  userId: random.userId || doc.userId,
                  flowId: random.flowId || doc.id,
                  count: 1,
                  name: '分析位于 ' + index + ' 的这张图片',
                  img: img,
                  config: ''
                }
              }, items[index] || {});
              item.status = 'uploading';

              const r = item.Input || {};
              r.name = r.file = name;
              r.size = size;
              r.width = width;
              r.height = height;
              r.rank = rank;

              if (index < 0) { // || r.id == null || r.id <= 0) {
                items.unshift(item);
              } else {
                items[index] = item;
              }

              if (isSub) {
                App.randomSubs = items;
              } else {
                App.randoms = items;
              }

              try {
                Vue.set(items, index < 0 ? 0 : index, item);
              } catch (e) {
                console.error(e)
              }

              const formData = new FormData();
              formData.append('file', file);

              fetch(server + '/upload', {
                method: 'POST',
                body: formData
              })
                  .then(response => response.json())
                  .then(data => {
                    var path = data.path;
                    if (StringUtil.isEmpty(path, true) || data.size == null) {
                      throw new Error('上传失败！' + JSON.stringify(data || {}));
                    }

                    console.log('Upload successful:', data);
                    item.status = 'done';
                    if (!(server.includes('localhost') || server.includes('127.0.0.1'))) {
                      r.img = (path.startsWith('/') ? server + path : path) || r.img;
                    }

                    try {
                      Vue.set(items, index < 0 ? 0 : index, item);
                    } catch (e) {
                      console.error(e)
                    }

                    App.updateImage(r)
                  })
                  .catch(error => {
                    console.error('Upload failed:', error);
                    item.status = 'failed';
                  });
            }

            if (file.type && file.type.startsWith("video/")) {
              // === 视频处理 ===
              this.extractFramesAndUpload(file, 5, async (frameBlob, rank, totalFrames) => {
                const reader2 = new FileReader();
                reader2.onload = (evt) => { // TODO 传 toId，视频作为分组，次数作为抽取帧数
                  var fn = file.name + '-' + rank + '.jpg'
                  callback(new File([frameBlob], fn), reader2.result, fn, frameBlob.size, frameBlob.width, frameBlob.height, -1, rank)
                }
                reader2.readAsDataURL(frameBlob);
              });
            } else {
              callback(file, reader.result, file.name, file.size, file.width, file.height, index)
            }

          };
          reader.readAsDataURL(file);
        });
      },

      /**
       * 从视频抽帧（倒序），支持并发上传
       * @param {File} file - 视频文件
       * @param {number} concurrency - 最大并发数
       * @param {function} onFrameReady - 回调(frameBlob, rank, totalFrames)
       */
      extractFramesAndUpload: function(file, concurrency, onFrameReady) {
        const url = URL.createObjectURL(file);
        const video = document.createElement("video");
        video.src = url;
        video.preload = "metadata";

        video.onloadedmetadata = async () => {
          const duration = video.duration;
          let timestamps = [];

          if (duration <= 100) {
            for (let t = Math.floor(duration); t >= 0; t--) timestamps.push(t);
          } else {
            const step = duration / 100;
            for (let i = 100; i >= 0; i--) timestamps.push(i * step);
          }

          const totalFrames = timestamps.length;
          let index = 0;

          async function worker() {
            while (index < totalFrames) {
              const i = index++;
              const time = timestamps[i];
              const frameBlob = await FileUtil.captureFrame(video, time);
              // rank 按倒序：0 表示最新帧，totalFrames-1 表示最早帧
              const rank = i;
              onFrameReady(frameBlob, rank, totalFrames);
            }
          }

          // 并发执行
          const workers = [];
          for (let i = 0; i < concurrency; i++) {
            workers.push(worker());
          }
          await Promise.all(workers);

          URL.revokeObjectURL(url);
        };
      },

      transferImage: function(img, uploadUrl) {

        var self = this;

        var src =
            img.currentSrc ||
            img.src ||
            img.getAttribute("data-src") ||
            img.getAttribute("data-original");

        if (!src) {
          return Promise.reject("empty src");
        }

        // base64
        if (src.indexOf("data:image") === 0) {

          var blob = this.dataURLToBlob(src);

          return this.resizeBlob(blob)
              .then(function(blob){
                return self.uploadBlob(blob, uploadUrl);
              });

        }

        // canvas读取
        return this.imgToBlob(img)

            .then(function(blob){
              return self.resizeBlob(blob);
            })

            .then(function(blob){
              return self.uploadBlob(blob, uploadUrl);
            })

            .catch(function(){

              // fetch下载
              return fetch(src)
                  .then(function(res){

                    if (!res.ok) throw new Error();

                    return res.blob();
                  })
                  .then(function(blob){
                    return self.resizeBlob(blob);
                  })
                  .then(function(blob){
                    return self.uploadBlob(blob, uploadUrl);
                  });

            })

            .catch(function(){

              // fallback url
              var form = new FormData();
              form.append("url", src);

              return fetch(uploadUrl,{
                method:"POST",
                body:form
              }).then(function(r){
                return r.json();
              });

            });

      },


      imgToBlob: function(img){

        return new Promise(function(resolve,reject){

          try{

            var w = img.naturalWidth;
            var h = img.naturalHeight;

            if(!w || !h){
              reject();
              return;
            }

            var canvas = document.createElement("canvas");

            canvas.width = w;
            canvas.height = h;

            var ctx = canvas.getContext("2d");
            ctx.drawImage(img,0,0);

            canvas.toBlob(function(blob){

              if(blob) resolve(blob);
              else reject();

            },"image/jpeg",0.95);

          }catch(e){
            reject(e);
          }

        });

      },


      resizeBlob: function(blob){

        return new Promise(function(resolve,reject){

          var img = new Image();

          img.onload = function(){

            var w = img.width;
            var h = img.height;

            if (w > 1080) {

              var ratio = 1080 / w;

              w = 1080;
              h = Math.round(h * ratio);
            }

            var canvas = document.createElement("canvas");

            canvas.width = w;
            canvas.height = h;

            var ctx = canvas.getContext("2d");

            ctx.drawImage(img,0,0,w,h);

            canvas.toBlob(function(newBlob){

              if(newBlob) resolve(newBlob);
              else reject();

            },"image/jpeg",0.9);

          };

          img.onerror = reject;

          img.src = URL.createObjectURL(blob);

        });

      },


      uploadBlob: function(blob, uploadUrl){

        var form = new FormData();

        form.append("file", blob, "image_"+Date.now()+".jpg");

        return fetch(uploadUrl,{
          method:"POST",
          body:form
        })
            .then(function(res){

              if(!res.ok){
                throw new Error("upload failed");
              }

              return res.json();
            });

      },

      dataURLToBlob: function(dataurl){

        var arr = dataurl.split(",");
        var mime = arr[0].match(/:(.*?);/)[1];

        var bstr = atob(arr[1]);
        var n = bstr.length;

        var u8arr = new Uint8Array(n);

        while(n--){
          u8arr[n] = bstr.charCodeAt(n);
        }

        return new Blob([u8arr],{type:mime});

      },

      uploadImage: function(randomIndex, randomSubIndex) {
        const isSub = randomSubIndex != null;
        const items = (isSub ? this.randomSubs : this.randoms) || [];
        const cri = this.currentRandomItem || {}
        const ind = isSub && randomSubIndex != null ? randomSubIndex : randomIndex;
        const item = items[ind] || {}
        const random = item.Input || {}
        const testRecord = item.TestRecord || {}

        if (StringUtil.isEmpty(item.img) && StringUtil.isEmpty(item.screenshot)) {
          alert('Please select an image file.');
          this.onClickAddRandom(randomIndex, randomSubIndex)
          return;
        }

        var vImg = this.$refs["randomImg" + ind];
        if (Array.isArray(vImg)) {
          vImg = vImg[0];
        }

        if (! vImg) {
          console.warn("img ref not found:", ind);
          return;
        }

        // const formData = new FormData();
        // formData.append('file', item.screenshot);

        this.transferImage(vImg, this.server + "/upload")
        //     .then(function(res){
        //       console.log(res.path);
        //     });
        //
        // fetch(this.server + '/upload', {
        //   method: 'POST',
        //   body: formData
        // })
        //     .then(response => response.json())
            .then(data => {
              var path = data.path;
              if (StringUtil.isEmpty(path, true) || data.size == null) {
                throw new Error('上传失败！' + JSON.stringify(data || {}));
              }

              console.log('Upload successful:', data);
              item.status = 'done';
              const screenshot = (path.startsWith('/') ? App.server + path : path) || item.screenshot || '';
              const img = (path.startsWith('/') ? App.server + path : path) || item.img || '';
              var keys = StringUtil.splitPath(data.path, false);
              var fn = keys == null ? null : keys[keys.length - 1];
              testRecord.randomId = random.id
              testRecord.file = random.file = StringUtil.isNotEmpty(fn) ? fn : random.file;
              testRecord.img = random.img || img; // FIXME 改用点击区域图标？还是在原图上画框、点击放大？
              testRecord.screenshot = App.img = screenshot;
              random.size = (data.size || (StringUtil.length(screenshot) * (3/4)) - (screenshot.endsWith('==') ? 2 : 1));
              random.width = data.width || random.width;
              random.height = data.height || random.height;
              try {
                Vue.set(items, ind, item);
              } catch (e) {
                console.error(e)
              }

              // App.updateImage(random) // event 无效 App.doOnKeyUp(event, 'random', false, item)
              App.updateImage(random, {
                id: testRecord.id,
                file: testRecord.file,
                img: img,
                screenshot: testRecord.screenshot,
                size: testRecord.size,
                width: testRecord.width,
                height: testRecord.height
              }) // event 无效 App.doOnKeyUp(event, 'random', false, item)
            })
            .catch(error => {
              console.error('Upload failed:', error);
              alert('Failed to upload image.');
              item.status = 'failed';
            });
      },

      updateImage: function (random, testRecord) {
        if (random == null || testRecord == null) {
          alert('上传/修改图片失败, random == null || testRecord == null!');
          return
        }

        var id = testRecord.id
        var isPost = id == null || id <= 0
        var tr = isPost ? JSON.parse(JSON.stringify(testRecord)) : testRecord;
        if (isPost) {
          tr.id = undefined
        }
        tr.userId = undefined

        this.adminRequest((isPost ? '/post' : '/put'), {
          TestRecord: tr,
          tag: 'TestRecord'
        }, {}, function (url, res, err) {
          App.onResponse(url, res, err)
          var data = res.data
          var isOk = JSONResponse.isSuccess(data)

          var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((data || {}).msg))
          if (err != null) {
            msg += '\nerr: ' + err.msg
            alert((isPost ? '新增' : '修改') + (isOk ? '成功' : '失败') + '\nname: ' + random.name + msg)
          }
          if (isPost) {
            random.id = (data.Input || {}).id
          }
          App.isRandomShow = true
          App.isRandomListShow = true
          App.isRandomEditable = ! isOk
        })
      },

      imgRatio: 1920/1080,
      syncCanvasSize: function(stage) {
        const img = this.imgMap[stage];
        const canvas = this.canvasMap[stage];
        if (img == null || canvas == null) {
          return;
        }

        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        this.draw(stage);

        const realWidth = this.isFullScreen ? 0 : (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || 1920);
        const maxTextWidth = realWidth <= 0 ? 0 : Math.max(vAfterTitle.clientWidth || 0, vDiffTitle.clientWidth || 0, vBeforeTitle.clientWidth || 0);
        const imgWidth = maxTextWidth <= 0 ? 0 :  Math.max(maxTextWidth, img.width || vAfter.width || vDiffBefore.width || vDiffAfter.width || vBefore.width || realWidth/(window.devicePixelRatio*3));
        if (imgWidth <= 0 || imgWidth >= 700) {
          return;
        }
        const realHeight = this.isFullScreen ? 0 : (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 1920);
        const maxTextHeight = realHeight <= 0 ? 0 : Math.max(vAfterTitle.clientHeight || 0, vDiffTitle.clientHeight || 0, vBeforeTitle.clientHeight || 0);
        const imgHeight = maxTextHeight <= 0 ? 0 :  Math.max(maxTextHeight, img.height || vAfter.height || vDiffBefore.height || vDiffAfter.height || vBefore.height || realHeight/(window.devicePixelRatio*3));
        if (imgHeight <= 0 || imgHeight >= 700*21/9) {
          return;
        }

        this.imgRatio = imgWidth/imgHeight;

        // this.moveSplit(Math.min(0.7, Math.max(0.3, (2*imgWidth + 450 + 20)/realWidth)));
      },
      getCanvasXY: function(stage, event) {
        const el = this.canvasMap[stage];
        const rect = el.getBoundingClientRect();
        return [event.clientX - rect.left, event.clientY - rect.top];
      },

      draw: function(stage) {
        const detection = this.detection || {};
        const img = this.imgMap[stage];
        const canvas = this.canvasMap[stage];
        const det = detection[stage];
        const isBefore = stage === 'before' || stage === 'diffBefore';
        const isDiff = stage === 'diffBefore' || stage === 'diffAfter';

        const cri = this.currentRandomItem || this.randoms[this.currentRandomIndex] || {};
        const tr = (isBefore ? cri.TestRecord : detection) || {};
        const corrects = tr.corrects || [];
        const wrongs = tr.wrongs || [];

        JSONResponse.drawDetections(canvas, det, {
          hoverId: this.hoverIds[stage],
          visiblePaths: this.visiblePaths,
          labelBackground: true,
          rotateBoxes: true,
          rotateText: false,
          stage: stage,
          corrects: corrects,
          wrongs: wrongs,
          styleOverride: isDiff ? (box, isBefore) => {
            if (! box.color) { // 防止空色
              box.color = [255, 255, 255, 128]
            }
            // 以原始颜色为基准，做红移或蓝移，透明度相对调节
            // return { color: JSONResponse.shiftColor(box.color, isBefore ? 'red' : 'blue', isBefore ? 1.2 : 0.8) };  // 红移，透明度放大20%；蓝移，透明度缩小 20%
            return { color: JSONResponse.adjustBrightness(box.color, isBefore ? 'brighten' : 'darken', isBefore ? 1.2 : 0.8) };  // 暗移，透明度放大20%；亮移，透明度缩小 20%
          } : null
        }, img);

        this.drawDrawingBox(stage);
      },
      drawAll: function() {
        ['before', 'after', 'diffBefore', 'diffAfter'].forEach(stage => this.draw(stage));
      },
      compute: function() {
        const detection = this.detection || {};
        var total = detection.total;
        const cri = this.currentRandomItem || this.randoms[this.currentRandomIndex] || {};
        const random = cri.Input = cri.Input || {};
        const tr = cri.TestRecord = cri.TestRecord || {};
        const corrects = tr.corrects = tr.corrects || [];
        const wrongs = tr.wrongs = tr.wrongs || [];

        // var tests = this.tests[String(this.currentAccountIndex)] || {}
        // var currentResponse = (tests[random.flowId] || {})[
        //     (random.id > 0 ? random.id : (random.toId + '' + random.id))
        // ]
        const curTr = detection;
        const curCorrects = curTr.corrects = curTr.corrects || [];
        const curWrongs = curTr.wrongs = curTr.wrongs || [];

        ['before', 'after'].forEach(stage => {
          var det2 = detection[stage]
          var ws = stage == 'after' ? curWrongs : wrongs;
          var cs = stage == 'after' ? curCorrects : corrects;
          var wrongCount = ws == null ? 0 : ws.length;
          const bboxes = JSONResponse.getBboxes(det2) || [];
          var correctCount = Math.max(bboxes.length - wrongCount, cs == null ? 0 : cs.length);
          // bboxes.forEach(bbox => {
          //   if (bbox.correct === false) {
          //     wrongCount ++;
          //   } else {
          //     correctCount ++;
          //   }
          // })

          detection[stage + 'Correct'] = correctCount;
          detection[stage + 'Wrong'] = wrongCount;
          detection[stage + 'Miss'] = total - correctCount;
          var recall = detection[stage + 'Recall'] = correctCount/total;
          var precision = detection[stage + 'Precision'] = correctCount/(correctCount + wrongCount);
          var f1 = detection[stage + 'F1'] = (2*recall*precision)/(recall + precision);
          detection[stage + 'RecallStr'] = (100*recall).toFixed(0) + '%';
          detection[stage + 'PrecisionStr'] = (100*precision).toFixed(0) + '%';
          detection[stage + 'F1Str'] = (100*f1).toFixed(0) + '%';
        })

        if (total <= 0) {
          detection.total = total = detection.afterCorrect || detection.beforeCorrect;
        }

        var diffCorrect = detection.afterCorrect - detection.beforeCorrect;
        var diffWrong = detection.afterWrong - detection.beforeWrong;
        var diffMiss = detection.afterMiss - detection.beforeMiss;
        var diffRecall = detection.afterRecall - detection.beforeRecall;
        var diffPrecision = detection.afterPrecision - detection.beforePrecision;
        var diffF1 = detection.afterF1 - detection.beforeF1;
        detection.diffCorrectStr = diffCorrect >= 0 ? '+' + diffCorrect : diffCorrect;
        detection.diffWrongStr = diffWrong >= 0 ? '+' + diffWrong : diffWrong;
        detection.diffMissStr = diffMiss >= 0 ? '+' + diffMiss : diffMiss;
        detection.diffRecallStr = (diffRecall >= 0 ? '+' : '') + (100*diffRecall).toFixed(0) + '%';
        detection.diffPrecisionStr = (diffPrecision >= 0 ? '+' : '') + (100*diffPrecision).toFixed(0) + '%';
        detection.diffF1Str = (diffF1 >= 0 ? '+' : '') + (100*diffF1).toFixed(0) + '%';
      },

      summary: function() {
        const detection = this.detection || {};

        let allImgTotal = 0;
        let allImgCorrect = 0;
        let allImgWrong = 0;
        let allImgMiss = 0;

        let allTotal = 0;
        let allCorrect = 0;
        let allWrong = 0;
        let allMiss = 0;

        let sameImgTotal = 0;
        let sameImgCorrect = 0;
        let sameImgWrong = 0;
        let sameImgMiss = 0;

        let sameTotal = 0;
        let sameCorrect = 0;
        let sameWrong = 0;
        let sameMiss = 0;

        // 之前的用完整数据，可以提醒目前还有图片未测试/提交 var beforeSameIds = detection.sameIds || [];
        let sameIds = this.sameIds || [];

        let randoms = this.randoms || []
        for (let i = 0; i < randoms.length; i ++) {
          let rand = randoms[i];
          if (rand == null) {
            continue;
          }

          allImgTotal ++;
          var tr2 = rand.TestRecord;
          if (tr2 == null) {
            allImgMiss ++;
            continue;
          }

          var isAfter = tr2.reportId != this.reportId && sameIds.includes(tr2.id);
          if (isAfter) {
            sameImgTotal ++;
          }

          var total = tr2.total || tr2.correct || 0;
          var wrong = tr2.wrong || 0;
          var correct = tr2.correct || total - wrong;
          allTotal += total;
          allWrong += wrong;
          allCorrect += correct;
          allMiss += total - correct;

          if (wrong <= 0) {
            allImgCorrect ++;
          } else {
            allImgWrong ++;
          }

          if (isAfter) {
            sameTotal += total;
            sameWrong += wrong;
            sameCorrect += correct;
            sameMiss += total - correct;

            if (wrong <= 0) {
              sameImgCorrect ++;
            } else {
              sameImgWrong ++;
            }
          }
        }

        let allImgRecall = detection.beforeImgRecall = allImgCorrect/allImgTotal;
        let allImgPrecision = detection.beforeImgPrecision = allImgCorrect/(allImgCorrect + allImgWrong);
        let allImgF1 = detection.beforeImgF1 = (2*allImgRecall*allImgPrecision)/(allImgRecall + allImgPrecision);

        let allRecall = detection.beforeAllRecall = allCorrect/allTotal;
        let allPrecision = detection.beforeAllPrecision = allCorrect/(allCorrect + allWrong);
        let allF1 = detection.beforeAllF1 = (2*allRecall*allPrecision)/(allRecall + allPrecision);

        detection.beforeImgRecallStr = (100*allImgRecall).toFixed(0);
        detection.beforeImgPrecisionStr = (100*allImgPrecision).toFixed(0);
        detection.beforeImgF1Str = (100*allImgF1).toFixed(0);

        detection.beforeAllRecallStr = (100*allRecall).toFixed(0);
        detection.beforeAllPrecisionStr = (100*allPrecision).toFixed(0);
        detection.beforeAllF1Str = (100*allF1).toFixed(0);

        this.detection = detection;

        var did = ((this.currentRemoteItem || {}).Flow || {}).id
        // var compareRandomIds = this.compareRandomIds || [];
        // var afterIds = [...new Set([...compareRandomIds, ...(this.sameIds || [])])];
        // var beforeIds = [...new Set([...compareRandomIds, ...(detection.sameIds || [])])];
        // var afterIds = this.sameIds || [];
        let beforeIds = detection.sameIds || [];

        const afterSame = {
          imgTotal: sameImgTotal,
          imgWrong: sameImgWrong,
          imgCorrect: sameImgCorrect,
          imgMiss: sameImgMiss,
          allTotal: sameTotal,
          allWrong: sameWrong,
          allCorrect: sameCorrect,
          allMiss: sameMiss
        };

        this.adminRequest('/get', {
          "TestRecord[]": {
            "count": 2,
            "TestRecord": {
              "@column": "reportId;sum(total):allTotal;sum(correct):allCorrect;sum(wrong):allWrong;sum(miss):allMiss;count(*):imgTotal;sum(wrong + miss <= 0):imgCorrect;sum(wrong > 0):imgWrong;sum(miss > 0):imgMiss",
              "@raw": "@column",
              "@group": "reportId",
              "@order": "reportId-",
              'documentId': did,
              "total>=": 0,
              "wrong>=": 0,
              "correct>=": 0,
              "reportId>": 0,
              "randomId>": 0,
              // 'randomId{}': compareRandomIds.length <= 0 ? null : compareRandomIds,
              // "@explain": true
            },
          },
          // 引用失败 "TestRecord-reportId[]": {
          //   "count": 2,
          //   "TestRecord": {
          //     "@column": "reportId",
          //     "@group": "reportId",
          //     "@order": "reportId-",
          //     "total>=": 0,
          //     "wrong>=": 0,
          //     "correct>=": 0
          //   }
          // },
          // "TestRecord:post": {
          //   "@column": "reportId",
          //   "@order": "reportId-"
          // },
          // "TestRecord:pre": {
          //   "reportId<@": "TestRecord:post/reportId",
          //   "@column": "reportId",
          //   "@order": "reportId-"
          // },
          // "TestRecord:after": {
          //   "reportId@": "TestRecord:post/reportId",
          //   "@column": "reportId;sum(total):allTotal;sum(correct):allCorrect;sum(wrong):allWrong;sum(miss):allMiss;count(*):imgTotal;sum(wrong + miss <= 0):imgCorrect;sum(wrong > 0):imgWrong;sum(miss > 0):imgMiss",
          //   "@raw": "@column",
          //   "@group": "reportId",
          //   "@order": "reportId-",
          //   "total>=": 0,
          //   "wrong>=": 0,
          //   "correct>=": 0
          // },
          // "TestRecord:before": {
          //   "reportId@": "TestRecord:pre/reportId",
          //   "@column": "reportId;sum(total):allTotal;sum(correct):allCorrect;sum(wrong):allWrong;sum(miss):allMiss;count(*):imgTotal;sum(wrong + miss <= 0):imgCorrect;sum(wrong > 0):imgWrong;sum(miss > 0):imgMiss",
          //   "@raw": "@column",
          //   "@group": "reportId",
          //   "@order": "reportId-",
          //   "total>=": 0,
          //   "wrong>=": 0,
          //   "correct>=": 0
          // },
          // "TestRecord-reportId:ids2[]": {
          //   "TestRecord": {
          //     "reportId<@": "TestRecord:pre/reportId",
          //     "@column": "max(reportId):reportId",
          //     "@group": "randomId",
          //     "total>=": 0,
          //     "wrong>=": 0,
          //     "correct>=": 0
          //   }
          // },
          "TestRecord:beforeSame": {
            // "reportId{}@": "TestRecord-reportId:ids2[]",
            // 'randomId{}': beforeIds.length <= 0 ? null : beforeIds,
            'id{}': beforeIds,
            'documentId': did,
            "@column": "sum(total):allTotal;sum(correct):allCorrect;sum(wrong):allWrong;sum(miss):allMiss;count(*):imgTotal;sum(wrong + miss <= 0):imgCorrect;sum(wrong > 0):imgWrong;sum(miss > 0):imgMiss",
            "@raw": "@column",
            "total>=": 0,
            "wrong>=": 0,
            "correct>=": 0,
            "reportId>": 0,
            "randomId>": 0,
          }
          // "@explain": true,
        }, {}, function (url, res, err) {
          // App.onResponse(url, res, err)

          const data = res.data || {}
          const trs = data['TestRecord[]'] || [data['TestRecord:after'], data['TestRecord:before']];
          const beforeSame = data['TestRecord:beforeSame'] || {};
          const extras = [afterSame, beforeSame];

          ['after', 'before', 'diffBefore', 'diffAfter'].forEach((stage, i) => {
            const isDiff = stage == 'diffBefore' || stage == 'diffAfter';
            const tr = trs[i] || {};

            if (isDiff) {
              ['Img', 'All'].forEach(type => {
                const correct = +detection['after' + type + 'Correct'] - +detection['before' + type + 'Correct'];
                const wrong = +detection['after' + type + 'Wrong'] - +detection['before' + type + 'Wrong'];
                const miss = +detection['after' + type + 'Miss'] - +detection['before' + type + 'Miss'];
                const recall = +detection['after' + type + 'Recall'] - +detection['before' + type + 'Recall'];
                const precision = +detection['after' + type + 'Precision'] - +detection['before' + type + 'Precision'];
                const f1 = +detection['after' + type + 'F1'] - +detection['before' + type + 'F1'];

                detection['diff' + type + 'CorrectStr'] = (correct >= 0 ? '+' : '') + correct;
                detection['diff' + type + 'WrongStr'] = (wrong >= 0 ? '+' : '') + wrong;
                detection['diff' + type + 'MissStr'] = (miss >= 0 ? '+' : '') + miss;
                detection['diff' + type + 'RecallStr'] = (recall >= 0 ? '+' : '') + (100 * recall).toFixed(0);
                detection['diff' + type + 'PrecisionStr'] = (precision >= 0 ? '+' : '') + (100 * precision).toFixed(0);
                detection['diff' + type + 'F1Str'] = (f1 >= 0 ? '+' : '') + (100 * f1).toFixed(0);
              });
            } else {
              const extra = extras[i] || {};
              const imgTotal = detection[stage + 'ImgTotal'] = +(tr.imgTotal || tr.imgCorrect || 0) + +(extra.imgTotal || 0);
              const imgWrong = detection[stage + 'ImgWrong'] = +(tr.imgWrong || 0) + +(extra.imgWrong || 0);
              const imgCorrect = detection[stage + 'ImgCorrect'] = (+(tr.imgCorrect || 0) + +(extra.imgCorrect || 0)); // 前面为 0 会导致后面的计算 || (imgTotal - imgWrong);
              const imgMiss = detection[stage + 'ImgMiss'] = (+(tr.imgMiss || 0) + +(extra.imgMiss || 0)) || (imgTotal - imgCorrect);

              const allTotal = detection[stage + 'AllTotal'] = +(tr.allTotal || tr.allCorrect || 0) + +(extra.allTotal || 0);
              const allWrong = detection[stage + 'AllWrong'] = +(tr.allWrong || 0) + +(extra.allWrong || 0);
              const allCorrect = detection[stage + 'AllCorrect'] = (+(tr.allCorrect || 0) + +(extra.allCorrect || 0)); // 前面为 0 会导致后面的计算 || (allTotal - allWrong);
              const allMiss = detection[stage + 'AllMiss'] = (+(tr.allMiss || 0) + +(extra.allMiss || 0)) || (allTotal - allCorrect);

              const imgRecall = detection[stage + 'ImgRecall'] = imgCorrect / imgTotal; // allImgCorrect / allImgTotal;
              const imgPrecision = detection[stage + 'ImgPrecision'] = imgCorrect / (imgCorrect + imgWrong); // allImgCorrect / (allImgCorrect + allImgWrong);
              const imgF1 = detection[stage + 'ImgF1'] = (2 * imgRecall * imgPrecision) / (imgRecall + imgPrecision); // (2 * allImgRecall * allImgPrecision) / (allImgRecall + allImgPrecision);

              const allRecall = detection[stage + 'AllRecall'] = allCorrect / allTotal;
              const allPrecision = detection[stage + 'AllPrecision'] = allCorrect / (allCorrect + allWrong);
              const allF1 = detection[stage + 'AllF1'] = (2 * allRecall * allPrecision) / (allRecall + allPrecision);

              detection[stage + 'ImgRecallStr'] = (100 * imgRecall).toFixed(0);
              detection[stage + 'ImgPrecisionStr'] = (100 * imgPrecision).toFixed(0);
              detection[stage + 'ImgF1Str'] = (100 * imgF1).toFixed(0);

              detection[stage + 'AllRecallStr'] = (100 * allRecall).toFixed(0);
              detection[stage + 'AllPrecisionStr'] = (100 * allPrecision).toFixed(0);
              detection[stage + 'AllF1Str'] = (100 * allF1).toFixed(0);
            }
          })

          App.detection = detection;
        });
      },
      processDiffAndAutoMark: function() {
        var detection = this.detection || {};
        const cri = this.currentRandomItem || this.randoms[this.currentRandomIndex] || {};
        const random = cri.Input = cri.Input || {};
        const tr = cri.TestRecord = cri.TestRecord || {};
        const corrects = tr.corrects = tr.corrects || [];
        const wrongs = tr.wrongs = tr.wrongs || [];

        // vBefore.src = vDiff.src = vAfter.src = this.img = random.img;
        this.file = random.file;

        // var tests = this.tests[String(this.currentAccountIndex)] || {}
        // var currentResponse = (tests[random.flowId] || {})[
        //     (random.id > 0 ? random.id : (random.toId + '' + random.id))
        // ]
        const curTr = detection;
        const curCorrects = curTr.corrects = curTr.corrects || [];
        const curWrongs = curTr.wrongs = curTr.wrongs || [];

        var beforeBoxes = JSONResponse.getBboxes(detection.before) || [];   // 上次参考结果
        var beforeMissBboxes = this.isMLEnabled ? JSONResponse.getBboxes(detection.missTruth) : null;
        if (beforeMissBboxes != null && beforeMissBboxes.length > 0) {
          beforeBoxes = JSONResponse.deepMerge(beforeBoxes, beforeMissBboxes);
        }

        const afterBoxes = JSONResponse.getBboxes(detection.after) || [];    // 当前检测结果
        const iouThreshold = (detection.diffThreshold || 90) / 100;

        const diffBoxes = [];
        const total = detection.total || 0;
        var correctCount = 0;
        var file = StringUtil.trim(this.file).replaceAll(/'images'/ig, '').replaceAll(/'image'/ig, '').replaceAll(/'img'/ig, '')
            .replaceAll(/'pictures'/ig, '').replaceAll(/'picture'/ig, '').replaceAll(/'pic'/ig, '')
            .replaceAll(/'photos'/ig, '').replaceAll(/'photo'/ig, '')
            .replaceAll(/'downloads'/ig, '').replaceAll(/'download'/ig, '')
            .replaceAll(/'files'/ig, '').replaceAll(/'file'/ig, '')
            .replaceAll(/'Unknown'/ig, '').replaceAll(/'Unknow'/ig, '')
            .replaceAll(/'Unnamed'/ig, '').replaceAll(/'Unname'/ig, '')
            .replaceAll(/'no name'/ig, '').replaceAll(/'Test'/ig, '')
            .replaceAll(/'examples'/ig, '').replaceAll(/'example'/ig, '')
            .replaceAll(/'new'/ig, '').replaceAll('测试', '')
            .replaceAll('未命名', '').replaceAll('未知', '').replaceAll('文件', '')
            .replaceAll('示例', '').replaceAll('新建', '').replaceAll('下载', '')
            .replaceAll('图片', '').replaceAll('照片', '').replaceAll('相片', '');

        var dotInd = file.lastIndexOf('.');
        file = dotInd >= 0 ? file.substring(0, dotInd).trim() : file.trim();
        // TODO 排除 0ab3e 等哈希值及汉字、字母外的各种字符

        const matchMap = {};

        afterBoxes.forEach((curBox, curInd) => {
          let bestIou = 0;
          let bestRef = null;
          let bestInd = -1;

          beforeBoxes.forEach((refBox, refInd) => {
            var curBbox = JSONResponse.getXYWHD(JSONResponse.getBbox(curBox));
            var refBbox = JSONResponse.getXYWHD(JSONResponse.getBbox(refBox));
            const iou = JSONResponse.computeIoU(curBbox, refBbox);
            if (iou > bestIou) {
              bestIou = iou;
              bestRef = refBox;
              bestInd = refInd;
            }
          });

          var label = curBox.label;
          var correct = curWrongs.includes(curInd) ? false : (curCorrects.includes(curInd) ? true : null);
          if (bestIou >= iouThreshold && bestRef != null) {
            // 如果匹配到：标签一致→继承correct，标签不同→打X
            // curBox.correct = label === bestRef.label ? wrongs.includes(bestInd) : ! bestRef.correct;
            // bestRef.match = (bestRef.match || 0) + 1;
            matchMap[bestInd] = (matchMap[bestInd] || 0) + 1;

            correct = wrongs.includes(bestInd) ? false : (corrects.includes(bestInd) ? true : correct);
          } else {
            // 没有匹配→打X
            var matchFile = StringUtil.isNotEmpty(file, true) && (
                (StringUtil.isNotEmpty(label, true) && file.indexOf(label) >= 0)
                || (StringUtil.isNotEmpty(curBox.ocr, true) && file.indexOf(curBox.ocr) >= 0)
            );
            // curBox.correct = total == 1 ? matchFile : (matchFile || total <= 0 || correctCount < total ? true : curBox.correct);
            correct = total == 1 ? matchFile : (matchFile ? true : correct);
          }

          if (correct == false) {
            var ind = curCorrects.indexOf(curInd);
            if (ind >= 0) {
              curCorrects.splice(ind, 1);
            }
            if (curWrongs.indexOf(curInd) < 0) {
              curWrongs.push(curInd);
            }
          } else if (correct == true) {
            var ind = curWrongs.indexOf(curInd);
            if (ind >= 0) {
              curWrongs.splice(ind, 1);
            }
            if (curCorrects.indexOf(curInd) < 0) {
              curCorrects.push(curInd);
            }
          }

          if (correct != false) {
            correctCount ++;
          }

          // diff 结果逻辑：只把差异明显的保存到 diff
          if (iouThreshold <= 0 || bestIou < iouThreshold || bestRef == null || label !== bestRef.label) {
            diffBoxes.push({
              ...curBox,
              // isBefore: false,
              '@index': curInd,
            });
          }
        });

        var missTruth = this.missTruth = this.missTruth || {};
        var missBboxes = missTruth.bboxes || [];
        var missPolygons = missTruth.polygons || [];
        var missLines = missTruth.lines || [];
        var missPoints = missTruth.points || [];

        const beforePolygons = JSONResponse.getPolygons(detection.before) || [];   // 上次参考结果
        const beforeLines = JSONResponse.getLines(detection.before) || [];   // 上次参考结果
        const beforePoints = JSONResponse.getPoints(detection.before) || [];   // 上次参考结果

        beforeBoxes.forEach((refBox, refInd) => {
          var macth = refBox == null ? null : matchMap[refInd];
          if (refBox != null && (macth == null || macth <= 0)) {
            var box = {
              ...refBox,
              // isBefore: true,
              '@before': true
            };
            diffBoxes.push(box);

            if (curWrongs.includes(refInd)) { // || wrongs.includes(refInd)) {
              return;
            }

            missBboxes.push(box); // 需要在渲染前合并时区分

            var polygon = beforePolygons[refInd]
            if (polygon != null) {
              missPolygons.push(polygon)
            }

            var line = beforeLines[refInd]
            if (line != null) {
              missLines.push(line)
            }

            var point = beforePoints[refInd]
            if (point != null) {
              missPoints.push(point)
            }
          }
        });

        // 如果需要也展示 before 中 unmatched 的box，可遍历 beforeBoxes 进行差异补充
        var diff = detection.diff || {};
        diff.bboxes = diffBoxes;
        detection.diff = diff;
        this.detection = detection;

        if (missBboxes.length > 0) {
          missTruth.bboxes = missBboxes;
        }
        if (missPolygons.length > 0) {
          missTruth.polygons = missPolygons;
        }
        if (missLines.length > 0) {
          missTruth.lines = missLines;
        }
        if (missPoints.length > 0) {
          missTruth.points = missPoints;
        }
        this.missTruth = missTruth;

        this.drawAll();
        this.compute();
        return diff;
      },

      exportJSON: function () {
        const jsonStr = JSON.stringify(this.detection, null, 2);
        const blob = new Blob([jsonStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'detection_export.json';
        a.click();
        URL.revokeObjectURL(url);
      },

      /**
       * 修正光标坐标与 canvas 标签位置对应的问题
       * 需要考虑 canvas 在页面上显示尺寸与实际 canvas 尺寸比例的缩放
       */
      getCanvasXY: function(stage, event) {
        const canvas = this.canvasMap[stage];
        const rect = canvas.getBoundingClientRect();

        // canvas 上的实际像素坐标需要根据缩放计算
        const scaleX = canvas.width / rect.width;
        const scaleY = canvas.height / rect.height;

        const x = (event.clientX - rect.left) * scaleX;
        const y = (event.clientY - rect.top) * scaleY;

        return [x, y];
      },

      onClickFullScreen: function(event) {
        var isFullScreen = this.isFullScreen = ! this.isFullScreen;
        this.isRandomShow = ! isFullScreen;
        const realWidth = isFullScreen ? 0 : (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || 1920);
        const maxTextWidth = realWidth <= 0 ? 0 : Math.max(vAfterTitle.clientWidth || 0, vDiffTitle.clientWidth || 0, vBeforeTitle.clientWidth || 0);
        const imgWidth = maxTextWidth <= 0 ? 0 : Math.max(maxTextWidth, Math.min(700, vAfter.width || vDiffAfter.width || vDiffBefore.width || vBefore.width || realWidth/(window.devicePixelRatio*3)));
        this.moveSplit(isFullScreen ? 0.4 : Math.max(0.6, (2*imgWidth + 450 + 20)/realWidth));
        event?.preventDefault();
      },
      /**
       * 在 drawDetections 里绘制标签按钮时也需要保证位置和点击检测使用相同的坐标体系
       * 例如点击 √ / × 时：
       */
      onClick: function(stage, event) {
        if (this.isDrawingBox || this.isDragging || this.isResizing || this.isRotating) {
          return;
        }

        const detection = this.detection;
        const isDiff = stage == 'diffBefore' || stage == 'diffAfter';
        const img = this.imgMap[stage];
        const canvas = this.canvasMap[stage];
        const [x, y] = this.getCanvasXY(stage, event);

        const height = canvas.height || (img || {}).height;
        const width = canvas.width || (img || {}).width;
        const nw = img == null ? 0 : (img.naturalWidth || 0);
        const nh = img == null ? 0 : (img.naturalHeight || 0);
        const xRate = nw < 1 ? 1 : width/nw;
        const yRate = nh < 1 ? 1 : height/nh;

        const corrects = detection.corrects = detection.corrects || [];
        const wrongs = detection.wrongs = detection.wrongs || [];

        const bboxes = JSONResponse.getBboxes(detection[stage]) || []
        let len = bboxes.length;
        var range = ((canvas || {}).height || (img || {}).height || 240) * (len <= 1 ? 0.5 : (len <= 5 ? 0.1/len : 0.02));
        for (let i = 0; i < len; i ++) {
          const item = bboxes[i];
          if (item == null || item['@before']) {
            continue;
          }

          var [bx, by, bw, bh, bd] = JSONResponse.getXYWHD(JSONResponse.getBbox(item), width, height, xRate, yRate);

          // 无效
          // const labelX = bx + bw + 4; // 和 drawDetections 中计算按钮位置保持一致
          // const labelY = by; // 同上

          // const size = 16; // 按钮点击区域大小
          // if (x >= labelX && x <= labelX + size && y >= labelY && y <= labelY + size) {
          //   // item.correct = true;
          //   if (corrects.indexOf(i) < 0) {
          //     corrects.push(i);
          //   }
          //   break;
          // }
          //
          // if (x >= labelX + size + 4 && x <= labelX + size * 2 + 4 && y >= labelY && y <= labelY + size) {
          //   // item.correct = false;
          //   if (wrongs.indexOf(i) < 0) {
          //     wrongs.push(i);
          //   }
          //   break;
          // }
          const ind = isDiff ? item['@index'] : i;

          //TODO 当左上角、右下角的拖动球 和 右上角、左下角 的旋转球 显示时，点击球开始 缩放/旋转

          // 其它情况，检测是否点击到框内
          if (ind != null && ind >= 0 && JSONResponse.isOnBorder(x, y, [bx, by, bw, bh], range)) {
            // item.correct = item.correct === false ? true : false;
            var wInd = wrongs.indexOf(ind);
            var cInd = corrects.indexOf(ind);
            if (wInd >= 0) {
              wrongs.splice(wInd, 1);
              if (cInd < 0) {
                corrects.push(ind);
              }
            } else {
              if (cInd >= 0) {
                corrects.splice(cInd, 1);
              }
              if (wInd < 0) {
                wrongs.push(ind);
              }
            }

            this.isRandomShow = true
            this.isRandomListShow = false
            this.isRandomSubListShow = true
            this.currentBbox = item

            var reqLinkConfigs = item.reqLinkConfigs || {};
            var resLinkConfigs = item.resLinkConfigs || {};
            this.randomTestTitle = 'UI/Data 关联: ' + (item.viewIdName || item.viewType || item.viewPath)
            // vRandom.value = StringUtil.trim(item.reqLinkConfigs) + "\n\n// / \n\n" + StringUtil.trim(item.resLinkConfigs)

            var randomSubs = (this.currentRandomItem || {}).subs || this.randomSubs || []
            var subs = []
            for (let j = 0; j < randomSubs.length; j++) {
              const rs = randomSubs[j]
              const r = rs == null ? null : rs.Random
              if (r != null && JSONResponse.isViewPathMatch(r.path, item.assertPath || item.viewPath)) {
                subs.push(rs)
              }
            }

            if (StringUtil.isEmpty(subs)) {
              for (let k in reqLinkConfigs) {
                subs.push({Random: {isRes: 0, userId: this.User.id, name: k, path: item.assertPath || item.viewPath, config: reqLinkConfigs[k]}})
              }
              for (let k in resLinkConfigs) {
                subs.push({Random: {isRes: 1, userId: this.User.id, name: k, path: item.assertPath || item.viewPath, config: reqLinkConfigs[k]}})
              }
            }

            this.randomSubs = (this.currentRandomItem || {}).subs = subs

            break;
          }
        }

        // this.draw(stage);
        this.draw('diffBefore');
        this.draw('diffAfter');
        this.draw('after');
        this.compute();

        var random = this.currentRandomItem || this.randoms[this.currentRandomIndex];
        var compareType = random == null ? null : random.compareType;
        if (compareType == null || compareType <= JSONResponse.COMPARE_EQUAL) {
          random.compareType = JSONResponse.COMPARE_VALUE_CHANGE;
          random.compareColor = 'blue';
          random.compareMessage = '画框 ✓ 对 X 错改变';
        }
      },

      // 鼠标按下开始画框
      onMousedown: function(stage, event) {
        if (stage != 'after') {
          return;
        }
        
        const img = this.imgMap[stage];
        const canvas = this.canvasMap[stage];
        const [x, y] = this.getCanvasXY(stage, event);

        const height = canvas.height || (img || {}).height;
        const width = canvas.width || (img || {}).width;
        const nw = img == null ? 0 : (img.naturalWidth || 0);
        const nh = img == null ? 0 : (img.naturalHeight || 0);
        const xRate = nw < 1 ? 1 : width/nw;
        const yRate = nh < 1 ? 1 : height/nh;

        let found = null;
        let foundItem = null;
        let foundBbox = null;
        var bboxes = JSONResponse.getBboxes(this.detection[stage]) || []
        let len = bboxes.length;
        var range = (height || 240) * (len <= 1 ? 0.5 : (len <= 5 ? 0.1/len : 0.02));
        for (var i = 0; i < len; i++) {
          const item = bboxes[i];
          if (item == null) {
            continue;
          }

          var [bx, by, bw, bh, bd] = JSONResponse.getXYWHD(JSONResponse.getBbox(item), width, height, xRate, yRate);
          if (JSONResponse.isOnBorder(x, y, [bx, by, bw, bh, bd], range)) {
            found = i;
            foundItem = item;
            foundBbox = [bx, by, bw, bh, bd];
            break;
          }
        }

        if (found == null || found < 0) {
          // 没有点击到现有框，开始画新框
          this.isDragging = false;
          this.isResizing = false;
          this.isRotating = false;
          this.dragMode = '';
          this.isDrawingBox = true;
          this.drawingBox = {startX: x, startY: y, endX: x, endY: y};
          event.preventDefault();
          return;
        }

        // 点击到了现有框，准备操作模式
        this.hoverIds[stage] = found;
        this.originBox = {
          startX: foundBbox[0],
          startY: foundBbox[1],
          endX: foundBbox[0] + foundBbox[2],
          endY: foundBbox[1] + foundBbox[3],
          degree: foundBbox[4] || 0
        };

        // 未点中某个框时：当光标在左上角、右下角的拖动球 或 右上角、左下角 的旋转球范围内时，显示对应球；已点中某个框时：根据球的类型来 缩放/旋转 框
        // 检测操作类型
        const corners = this.getBoxCorners(foundBbox[0], foundBbox[1], foundBbox[2], foundBbox[3]);
        const rotationHandles = [
          {x: foundBbox[0] + foundBbox[2], y: foundBbox[1]}, // 右上
          {x: foundBbox[0], y: foundBbox[1] + foundBbox[3]}  // 左下
        ];

        // 检查是否点击旋转球
        for (let i = 0; i < rotationHandles.length; i++) {
          const handle = rotationHandles[i];
          const distance = Math.sqrt(Math.pow(x - handle.x, 2) + Math.pow(y - handle.y, 2));
          if (distance <= 15) { // 旋转球检测范围
            this.isRotating = true;
            this.dragMode = 'rotate';
            this.rotationCenter = {
              x: foundBbox[0] + foundBbox[2] / 2,
              y: foundBbox[1] + foundBbox[3] / 2
            };
            this.dragStartX = x;
            this.dragStartY = y;
            break;
          }
        }

        if (! this.isRotating) {
          // 检查是否点击角点（用于缩放）
          if (this.isNearCorner(x, y, corners.tl, range)) {
            this.isResizing = true;
            this.dragMode = 'resize';
            this.currentCorner = 'tl';
          } else if (this.isNearCorner(x, y, corners.tr, range)) {
            this.isResizing = true;
            this.dragMode = 'resize';
            this.currentCorner = 'tr';
          } else if (this.isNearCorner(x, y, corners.bl, range)) {
            this.isResizing = true;
            this.dragMode = 'resize';
            this.currentCorner = 'bl';
          } else if (this.isNearCorner(x, y, corners.br, range)) {
            this.isResizing = true;
            this.dragMode = 'resize';
            this.currentCorner = 'br';
          } else {
            // 点击框内部或边框，执行拖拉
            this.isDragging = true;
            this.dragMode = 'move';
          }
        }

        if (this.isDragging || this.isResizing || this.isRotating) {
          this.dragStartX = x;
          this.dragStartY = y;
        }

        this.isDrawingBox = false;
        event.preventDefault();  
      },
      onMousemove: function(stage, event) {
        // 画框时的实时更新
        const canvas = this.canvasMap[stage];
        const [x, y] = this.getCanvasXY(stage, event);
        const height = canvas.height || (img || {}).height;
        var bboxes = JSONResponse.getBboxes(this.detection[stage]) || []
        let len = bboxes.length;
        var range = (height || 240) * (len <= 1 ? 0.5 : (len <= 5 ? 0.1/len : 0.02));

        const img = this.imgMap[stage];
        const width = canvas.width || (img || {}).width;
        const nw = img == null ? 0 : (img.naturalWidth || 0);
        const nh = img == null ? 0 : (img.naturalHeight || 0);
        const xRate = nw < 1 ? 1 : width/nw;
        const yRate = nh < 1 ? 1 : height/nh;

        let found = null;
        let foundItem = null;
        let foundBbox = null;
        for (let i = 0; i < len; i++) {
          const item = bboxes[i];
          if (item == null) {
            continue;
          }

          let [bx, by, bw, bh, bd] = JSONResponse.getXYWHD(JSONResponse.getBbox(item), width, height, xRate, yRate);
          if (JSONResponse.isOnBorder(x, y, [bx, by, bw, bh, bd], range)) {
            found = i;
            foundItem = item;
            foundBbox = [bx, by, bw, bh, bd];
            break;
          }
        }

        var changed = found !== this.hoverIds[stage];
        if (changed && ! (this.isDrawingBox || this.isDragging || this.isResizing || this.isRotating)) {
          this.hoverIds[stage] = found;
        }

        if (stage === 'after') {
          let drawingBox = this.drawingBox = this.drawingBox || {};
          if (this.isDrawingBox) {
            drawingBox.endX = x;
            drawingBox.endY = y;
            this.draw(stage);
            return;
          }

          // 未点中某个框时：当光标在左上角、右下角的拖动球 或 右上角、左下角 的旋转球范围内时，显示对应球；已点中某个框时：根据球的类型来 缩放/旋转 框
          // 检测操作类型
          const corners = foundBbox == null || (this.isDrawingBox || this.isDragging || this.isResizing || this.isRotating)
              ? null : this.getBoxCorners(foundBbox[0], foundBbox[1], foundBbox[2], foundBbox[3]);
          // 检查是否点击角点（用于缩放）
          let isDragging = this.isDragging;
          let isResizing = this.isResizing;
          let isRotating = this.isRotating;
          if (corners != null) {
            if (this.isNearCorner(x, y, corners.tl, range)) {
              this.dragMode = 'resize';
              this.currentCorner = 'tl';
              isResizing = true;
            } else if (this.isNearCorner(x, y, corners.tr, range)) {
              this.dragMode = 'resize';
              this.currentCorner = 'tr';
              isRotating = true;
            } else if (this.isNearCorner(x, y, corners.bl, range)) {
              this.dragMode = 'resize';
              this.currentCorner = 'bl';
              isRotating = true;
            } else if (this.isNearCorner(x, y, corners.br, range)) {
              this.dragMode = 'resize';
              this.currentCorner = 'br';
              isResizing = true;
            // } else if (found != null) {
            //   isDragging = true;
            }
          }

          let originBox = this.originBox; // = this.originBox || {};
          if (isDragging && originBox != null) {
            // 拖拉模式：更新框的位置
            const deltaX = x - this.dragStartX;
            const deltaY = y - this.dragStartY;

            let originBox = this.originBox = this.originBox || {};
            drawingBox.startX = originBox.startX + deltaX;
            drawingBox.startY = originBox.startY + deltaY;
            drawingBox.endX = originBox.endX + deltaX;
            drawingBox.endY = originBox.endY + deltaY;
            drawingBox.degree = originBox.degree;

            this.draw(stage);
            return;
          }

          if (isResizing && originBox != null) {
            // 缩放模式：根据角点调整框的大小

            switch (this.currentCorner) {
              case 'tl': // 左上角
                drawingBox.startX = Math.min(x, originBox.endX - range);
                drawingBox.startY = Math.min(y, originBox.endY - range);
                drawingBox.endX = originBox.endX;
                drawingBox.endY = originBox.endY;
                break;
              case 'tr': // 右上角
                drawingBox.startX = originBox.startX;
                drawingBox.startY = Math.min(y, originBox.endY - range);
                drawingBox.endX = Math.max(x, originBox.startX + range);
                drawingBox.endY = originBox.endY;
                break;
              case 'bl': // 左下角
                drawingBox.startX = Math.min(x, originBox.endX - range);
                drawingBox.startY = originBox.startY;
                drawingBox.endX = originBox.endX;
                drawingBox.endY = Math.max(y, originBox.startY + range);
                break;
              case 'br': // 右下角
                drawingBox.startX = originBox.startX;
                drawingBox.startY = originBox.startY;
                drawingBox.endX = Math.max(x, originBox.startX + range);
                drawingBox.endY = Math.max(y, originBox.startY + range);
                break;
            }

            drawingBox.degree = originBox.degree;
            this.draw(stage);
            return;
          }

          if (isRotating && originBox != null) {
            // 旋转模式：计算旋转角度
            const centerX = this.rotationCenter.x;
            const centerY = this.rotationCenter.y;

            const startAngle = Math.atan2(this.dragStartY - centerY, this.dragStartX - centerX);
            const currentAngle = Math.atan2(y - centerY, x - centerX);

            const deltaAngle = currentAngle - startAngle;
            let originBox = this.originBox || {};
            drawingBox.degree = (originBox.degree + deltaAngle * 180 / Math.PI) % 360;

            // 保持框的原始大小和位置
            drawingBox.startX = originBox.startX;
            drawingBox.startY = originBox.startY;
            drawingBox.endX = originBox.endX;
            drawingBox.endY = originBox.endY;

            this.draw(stage);
            return;
          }

        }

        if (changed || StringUtil.isNotEmpty(this.currentCorner, true)) {
          this.draw(stage);
        }
      },
      // 鼠标松开结束画框并显示弹窗
      onMouseup: function(stage, event) {
        if (stage != 'after' || ! (this.isDrawingBox || this.isDragging || this.isResizing || this.isRotating)) {
          return;
        }

        const img = this.imgMap[stage];
        const canvas = this.canvasMap[stage];
        const [x, y] = this.getCanvasXY(stage, event);
        const height = canvas.height || (img || {}).height;
        var detection = this.detection = this.detection || {};
        var bboxes = JSONResponse.getBboxes(detection.after) || []
        let len = bboxes.length;
        var range = (height || 240) * (len <= 1 ? 0.5 : (len <= 5 ? 0.1/len : 0.02));

        var drawingBox = this.drawingBox = this.drawingBox || {};

        if (this.isDragging || this.isResizing || this.isRotating) {
          // 保存修改后的框数据
          var hoverId = this.hoverIds[stage];
          var bbox = bboxes[hoverId] || {};

          const minX = Math.min(drawingBox.startX, drawingBox.endX);
          const maxX = Math.max(drawingBox.startX, drawingBox.endX);
          const minY = Math.min(drawingBox.startY, drawingBox.endY);
          const maxY = Math.max(drawingBox.startY, drawingBox.endY);

          if (maxX - minX >= 2*range && maxY - minY >= 2*range) {
            let degree = (drawingBox.degree || 0)%360;
            bbox.bbox = [minX, minY, maxX - minX, maxY - minY, degree >= 0 ? degree : degree + 360];
          }

          // 重置操作状态
          this.isDragging = false;
          this.isResizing = false;
          this.isRotating = false;
          this.dragMode = '';
          this.originBox = null;

          this.draw(stage);
          event.preventDefault();
          return;
        }

        var startX = drawingBox.startX;
        var startY = drawingBox.startY;
        var endX = drawingBox.endX = x;
        var endY = drawingBox.endY = y;
        
        // 计算框的实际坐标（确保左上角和右下角正确）
        const minX = Math.min(startX, endX);
        const maxX = Math.max(startX, endX);
        const minY = Math.min(startY, endY);
        const maxY = Math.max(startY, endY);

        this.isDrawingBox = false;
        this.isDragging = false;

        // 如果框太小，忽略
        if (maxX - minX < 2*range || maxY - minY < 2*range) {
          this.draw(stage);
          return;
        }

        this.showLabelModal(event.clientX, event.clientY); // 位置总是在右下角 this.showLabelModal(stage, maxX, maxY);
        
        event.preventDefault();
      },

      // 绘制正在画的框
      drawDrawingBox: function(stage) {
        var hoverId = this.hoverIds[stage];
        if (stage != 'after' || ! (hoverId != null || this.isDrawingBox || this.isDragging || this.isResizing || this.isRotating)) {
          return;
        }
        
        const canvas = this.canvasMap[stage];
        if (! canvas) {
          return;
        }
        
        const ctx = canvas.getContext('2d');
        if (! ctx) {
          return;
        }

        var drawingBox = this.drawingBox = this.drawingBox || {};
        var startX = drawingBox.startX;
        var startY = drawingBox.startY;
        var endX = drawingBox.endX;
        var endY = drawingBox.endY;
        var degree = drawingBox.degree || 0;

        // 如果正在操作现有框，使用操作中的框坐标
        if (this.isDragging || this.isResizing || this.isRotating) {
          // 使用drawingBox的坐标
        } else if (hoverId != null) {
          // 如果鼠标悬停在框上，显示该框的角点
          const img = this.imgMap[stage];
          const height = canvas.height || (img || {}).height;
          const width = canvas.width || (img || {}).width;
          const nw = img == null ? 0 : (img.naturalWidth || 0);
          const nh = img == null ? 0 : (img.naturalHeight || 0);
          const xRate = nw < 1 ? 1 : width/nw;
          const yRate = nh < 1 ? 1 : height/nh;

          var bboxes = JSONResponse.getBboxes(this.detection[stage]) || [];
          const item = bboxes[this.hoverIds[stage]];
          if (item) {
            var [bx, by, bw, bh, bd] = JSONResponse.getXYWHD(JSONResponse.getBbox(item), width, height, xRate, yRate);
            startX = bx;
            startY = by;
            endX = bx + bw;
            endY = by + bh;
            degree = bd || 0;
          }
        }

        const minX = Math.min(startX, endX);
        const maxX = Math.max(startX, endX);
        const minY = Math.min(startY, endY);
        const maxY = Math.max(startY, endY);
        const width = maxX - minX;
        const height = maxY - minY;
        const centerX = minX + width / 2;
        const centerY = minY + height / 2;

        ctx.save();
        
        // 如果有旋转，应用旋转变换
        if (degree !== 0) {
          ctx.translate(centerX, centerY);
          ctx.rotate(degree * Math.PI / 180);
          ctx.translate(-centerX, -centerY);
        }

        // 设置绘制样式
        ctx.strokeStyle = '#FF6B6B';
        ctx.lineWidth = 2;
        ctx.setLineDash(this.isDrawingBox ? [5, 5] : []);
        ctx.strokeRect(minX, minY, width, height);
        ctx.setLineDash([]);

        // 绘制角点
        if (! this.isDrawingBox) {
          ctx.fillStyle = '#FF6B6B';
          const corners = [
            {x: minX, y: minY},           // 左上
            {x: maxX, y: minY},           // 右上
            {x: minX, y: maxY},           // 左下
            {x: maxX, y: maxY}            // 右下
          ];
          
          corners.forEach(corner => {
            ctx.beginPath();
            ctx.arc(corner.x, corner.y, 5, 0, 2 * Math.PI);
            ctx.fill();
          });

          // 绘制旋转球
          const rotationHandles = [
            {x: maxX, y: minY},           // 右上
            {x: minX, y: maxY}            // 左下
          ];
          
          ctx.strokeStyle = '#4ECDC4';
          ctx.lineWidth = 2;
          rotationHandles.forEach(handle => {
            ctx.beginPath();
            ctx.arc(handle.x, handle.y, 8, 0, 2 * Math.PI);
            ctx.stroke();
            
            // 绘制旋转指示线
            ctx.beginPath();
            ctx.moveTo(handle.x, handle.y - 12);
            ctx.lineTo(handle.x, handle.y - 20);
            ctx.stroke();
          });
        }

        ctx.restore();
      },

            // 获取框的四个角点坐标
      getBoxCorners: function(x, y, width, height) {
        return {
          tl: {x: x, y: y},                    // 左上
          tr: {x: x + width, y: y},            // 右上
          bl: {x: x, y: y + height},           // 左下
          br: {x: x + width, y: y + height}    // 右下
        };
      },

      // 检测鼠标是否靠近指定点
      isNearCorner: function(mouseX, mouseY, corner, threshold = 10) {
        const distance = Math.sqrt(
          Math.pow(mouseX - corner.x, 2) + Math.pow(mouseY - corner.y, 2)
        );
        return distance <= threshold;
      },

      // 检测鼠标是否靠近旋转球
      isNearRotationHandle: function(mouseX, mouseY, handle, threshold = 15) {
        const distance = Math.sqrt(
          Math.pow(mouseX - handle.x, 2) + Math.pow(mouseY - handle.y, 2)
        );
        return distance <= threshold;
      },

      // 计算两点之间的角度（用于旋转）
      calculateAngle: function(centerX, centerY, pointX, pointY) {
        return Math.atan2(pointY - centerY, pointX - centerX);
      },

      // 应用旋转变换到坐标
      applyRotation: function(x, y, centerX, centerY, angle) {
        const cos = Math.cos(angle);
        const sin = Math.sin(angle);
        
        const translatedX = x - centerX;
        const translatedY = y - centerY;
        
        const rotatedX = translatedX * cos - translatedY * sin;
        const rotatedY = translatedX * sin + translatedY * cos;
        
        return {
          x: rotatedX + centerX,
          y: rotatedY + centerY
        };
      },

      // 显示标签弹窗
      showLabelModal: function(clientX, clientY) {
        // 直接使用鼠标事件的页面坐标
        let x = clientX + 10;
        let y = clientY + 10;
        
        // 边界检查，确保弹窗不会超出屏幕
        const modalWidth = 300;  // 弹窗宽度
        const modalHeight = 200; // 弹窗高度
        
        if (x + modalWidth > window.innerWidth) {
          x = clientX - modalWidth - 10;
        }
        
        if (y + modalHeight > window.innerHeight) {
          y = clientY - modalHeight - 10;
        }
        
        this.labelModalPosition = {
          x: Math.max(10, x),  // 确保不会超出左边界
          y: Math.max(10, y)   // 确保不会超出上边界
        };

        this.isLabelModalShow = true;
        // this.labelIndex = -1;
        // this.boxLabels = [];
        
        // 聚焦到输入框
        this.$nextTick(() => {
          const input = document.getElementById('labelInput');
          if (input) input.focus();
        });
      },

      // 隐藏标签弹窗
      hideLabelModal: function() {
        this.isLabelModalShow = false;
        this.newLabelText = '';
        this.draw('after');
      },

      // 添加标签
      addLabel: function() {
        var txt = StringUtil.trim(this.newLabelText);
        if (StringUtil.isEmpty(txt)) {
          return;
        }

        var boxLabels = this.boxLabels = this.boxLabels || [];
        var color = this.selectedColor = this.selectedColor || {};
        var rgba = [color.red, color.blue, color.green, color.alpha];
        for (let i = 0; i < boxLabels.length; i++) {
          var label = boxLabels[i];
          if (label == null) {
            continue
          }

          if (label.text == txt) {
            alert('标签名与第 ' + i + ' 个标签重复，请重新输入！');
            return;
          }
          if (label.color == rgba) {
            alert('标签颜色与第 ' + i + ' 个标签重复，请重新选择！');
            return;
          }
        }

        this.currentLabel = {
          text: txt,
          color: rgba
        };
        this.newLabelText = '';
        boxLabels.unshift(this.currentLabel);
        this.labelIndex = 0;
        this.saveBoxLabel(this.currentLabel, this.labelIndex);
      },

      // 删除标签
      removeLabel: function(index) {
        this.boxLabels.splice(index, 1);
      },

      // 选择标签颜色
      selectLabelColor: function(color) {
        this.selectedColor = color;
      },

      // 显示颜色选择器弹窗
      showColorPickerModal: function(index, label) {
        this.isColorPickerShow = true;
        // this.tempColor = this.selectedColor;
        this.labelIndex = index;
        this.currentLabel = label || this.boxLabels[index];

        this.$nextTick(() => {
            this.initColorPicker();
        });
      },
      // 隐藏颜色选择器弹窗
      hideColorPickerModal: function() {
        this.isColorPickerShow = false;
      },
      // 初始化颜色选择器
      initColorPicker: function() {
        const colorPickerContainer = document.getElementById('color-picker-container');
        if (! colorPickerContainer || this.colorPicker) return;
        
        this.colorPicker = new iro.ColorPicker('#color-picker-container', {
            width: 250,
            color: this.selectedColor.hexString,
            layout: [
                {
                    component: iro.ui.Wheel,
                    options: {}
                },
                {
                    component: iro.ui.Slider,
                    options: {
                        sliderType: 'hue'
                    }
                }
            ]
        });
        
        this.colorPicker.on('color:change', (color) => {
            this.selectedColor = color; // .hexString;
            var label = this.currentLabel || this.boxLabels[this.labelIndex] || {}
            label.color = [color.red, color.blue, color.green, color.alpha];
        });
      },

      // 确认选择颜色
      confirmColor: function() {
        // this.selectedColor = this.tempColor;
        this.hideColorPickerModal();
      },

      // 保存框的标签到检测结果中
      saveBoxLabel: function(label, index) {
        var boxLabels = this.boxLabels || [];
        if (boxLabels.length <= 0) {
          this.hideLabelModal();
          return;
        }

        const detection = this.detection;
        const after = detection.after = detection.after || {};
        const bboxes = after.bboxes = after.bboxes || [];

        var drawingBox = this.drawingBox = this.drawingBox || {};
        var startX = drawingBox.startX;
        var startY = drawingBox.startY;
        var endX = drawingBox.endX;
        var endY = drawingBox.endY;

        const minX = Math.min(startX, endX);
        const maxX = Math.max(startX, endX);
        const minY = Math.min(startY, endY);
        const maxY = Math.max(startY, endY);

        this.labelIndex = index || 0;
        this.currentLabel = label = label || boxLabels[index] || {}
        // 创建新的框对象
        const newBox = {
          bbox: [minX, minY, maxX - minX, maxY - minY],
          label: label.text,
          color: label.color,
          score: 1
        };
        
        bboxes.push(newBox);
        
        this.hideLabelModal();
        this.draw('after');
        // this.draw('diff');
        this.compute();
      },
      
      showAndSend: function (branchUrl, req, isAdminOperation, callback) {
        this.showUrl(isAdminOperation, branchUrl)
        vInput.value = JSON.stringify(req, null, '    ')
        this.showTestCase(false, this.isLocalShow)
        this.onChange(false)
        this.send(isAdminOperation, callback)
      },
      /**发送请求
       */
      send: function(isAdminOperation, callback, caseScript_, accountScript_, globalScript_, ignorePreScript) {
        if (this.operate != OPERATE_TYPE_HTTP) {
          this.onClickTestRandom()
          return
        }

        if (this.isTestCaseShow) {
          alert('请先打开一个 操作流程 Flow！')
          return
        }

        if (StringUtil.isEmpty(this.projectHost.host, true)) {
          var url = StringUtil.get(vUrl.value)
          if (url.startsWith('/') != true && url.startsWith('http://') != true && url.startsWith('https://') != true) {
            alert('URL 缺少 http:// 或 https:// 前缀，可能不完整或不合法，\n可能使用同域的 Host，很可能访问出错！')
          }
        }
        else {
          // if (StringUtil.get(vUrl.value).indexOf('://') >= 0) {
          //   alert('URL Host 已经隐藏(固定) 为 \n' + this.host + ' \n将会自动在前面补全，导致 URL 不合法访问出错！\n如果要改 Host，右上角设置 > 显示(编辑)URL Host')
          // }
        }

        this.onHandle(vInput.value)

        clearTimeout(handler)

        if (this.isEditResponse) {
          this.onChange(false)
          return
        }

        var header
        try {
          header = this.getHeader(vHeader.value)
        } catch (e) {
          // alert(e.message)
          return
        }

        var req = this.getRequest(vInput.value, {})

        var url = this.getUrl()

        vOutput.value = "requesting... \nURL = " + url

        errHandler = function () {
          vOutput.value = "requesting... \nURL = " + url + "\n\n可能" + ERR_MSG
        }
        setTimeout(errHandler, 5000)
        this.view = 'output';

        var caseScript = (caseScript_ != null ? caseScript_ : ((this.scripts || {}).case || {})[this.getCurrentDocumentId() || 0]) || {}

        var method = this.isShowMethod() ? this.method : null

        this.setBaseUrl()
        this.request(isAdminOperation, method, this.type, url, req, isAdminOperation ? {} : header, callback, caseScript, accountScript_, globalScript_, ignorePreScript)

        var baseUrls = this.getCache('', 'baseUrls', [])
        var bu = this.getBaseUrl(url)
        if (StringUtil.isNotEmpty(bu, true) && baseUrls.indexOf(bu) < 0) {
          baseUrls.push(bu)
          this.saveCache('', 'baseUrls', baseUrls)
          var projectHosts = this.projectHosts || []
          var projectHost = this.projectHost || {}
          var find = false
          for (var j = 0; j < projectHosts.length; j ++) {
              var pjt = projectHosts[j]
              if (pjt == null || StringUtil.isEmpty(pjt.host, true)) {
                 continue
              }

              if (pjt.host == projectHost.host) {
                  find = true
                  break
              }
          }

          if (find != true) {
             projectHosts.push({host: bu, project: projectHost.project})
             this.projectHosts = projectHosts
             this.saveCache('', 'projectHosts', projectHosts)
          }

        }

        this.locals = this.locals || []
        if (this.locals.length >= 1000) { //最多1000条，太多会很卡
          this.locals.splice(900, this.locals.length - 900)
        }
        var path = this.getMethod()
        this.locals.unshift({
          'Flow': {
            'userId': this.User.id,
            'project': (this.projectHost || {}).project,
            'name': this.formatDateTime() + ' ' + (this.urlComment || StringUtil.trim(req.tag)),
            'operation': CodeUtil.getOperation(path, req),
            'method': method,
            'type': this.type,
            'url': '/' + path,
            'request': JSON.stringify(req, null, '    '),
            'header': vHeader.value,
            'scripts': this.scripts
          }
        })
        this.saveCache('', 'locals', this.locals)
      },

      adminRequest: function (url, req, header, callback) {
        this.requestPost(true, url, req, header, callback)
      },
      requestGet: function (isAdminOperation, url, req, header, callback) {
        this.request(isAdminOperation, HTTP_METHOD_GET, REQUEST_TYPE_PARAM, url, req, header, callback)
      },
      requestPost: function (isAdminOperation, url, req, header, callback) {
        this.request(isAdminOperation, HTTP_METHOD_POST, REQUEST_TYPE_JSON, url, req, header, callback)
      },
      //请求
      request: function (isAdminOperation, method, type, url, req, header, callback, caseScript_, accountScript_, globalScript_, ignorePreScript, timeout_, wait_, retry_) {
        this.loadingCount ++

        if (url.indexOf('://') < 0) {
          url = (isAdminOperation ? this.server : this.getBaseUrl()) + (url.startsWith('/') ? url : '/' + url)
        }

        const isEnvCompare = this.isEnvCompareEnabled

        const scripts = (isAdminOperation || caseScript_ == null ? null : this.scripts) || {}
        const globalScript = (isAdminOperation ? null : (globalScript_ != null ? globalScript_ : (scripts.global || {})[0])) || {}
        const accountScript = (isAdminOperation ? null : (accountScript_ != null ? accountScript_ : (scripts.account || {})[this.getCurrentAccountId() || 0])) || {}
        const caseScript = (isAdminOperation ? null : caseScript_) || {}
        const timeout = timeout_ != null ? timeout_ : this.timeout
        const wait = wait_ != null ? wait_ : (this.wait || 0)
        var retry = retry_ != null ? retry_ : (this.retry || 0)

        var evalPostScript = function () {}

        const onHttpResponse = function (res) {
          App.currentHttpResponse = res
          clearTimeout(errHandler)
          var postEvalResult = evalPostScript(url, res, null)
          if (postEvalResult == BREAK_ALL) {
            return
          }

          App.loadingCount --
          res = res || {}

          if (isDelegate) {
            var hs = res.headers || {}
            var delegateId = hs['Apijson-Delegate-Id'] || hs['apijson-delegate-id']

            if (delegateId != null) {
              if (isEnvCompare) {
                if (delegateId != App.otherEnvDelegateId) {
                  App.otherEnvDelegateId = delegateId
                  App.saveCache(App.server, 'otherEnvDelegateId', delegateId)
                }
              } else {
                if (delegateId != App.delegateId) {
                  App.delegateId = delegateId
                  App.saveCache(App.server, 'delegateId', delegateId)
                }
              }
            }
          }

          //any one of then callback throw error will cause it calls then(null)
          // if ((res.config || {}).method == 'options') {
          //   return
          // }
          if (DEBUG) {
            log('send >> success:\n' + JSON.stringify(res.data, null, '    '))
          }

          //未登录，清空缓存
          if (res.data != null && res.data.code == 407) {
            // alert('request res.data != null && res.data.code == 407 >> isAdminOperation = ' + isAdminOperation)
            if (isAdminOperation) {
              // alert('request App.User = {} App.server = ' + App.server)

              App.clearUser()
            }
            else {
              // alert('request App.accounts[App.currentAccountIndex].isLoggedIn = false ')
              var account = App.accounts[App.currentAccountIndex]
              if (account != null) {
                account.isLoggedIn = false
              }
            }
          }

          if (postEvalResult == BREAK_LAST) {
            return
          }

          if (callback != null) {
            callback(url, res, null)
            return
          }
          App.onResponse(url, res, null)
        }

        const onHttpCatch = function (err) {
          if (retry != null && retry > 0 && retryReq(err)) {
            return;
          }

          var errObj = err instanceof Array == false && err instanceof Object ? err : {}
          var res = {status: errObj.status || (errObj.response || {}).status, request: {url: url, headers: header, data: req}, data: (errObj.response || {}).data}
          App.currentHttpResponse = res

          var postEvalResult = evalPostScript(url, res, err)
          if (postEvalResult == BREAK_ALL) {
            return
          }

          App.loadingCount --

          log('send >> error:\n' + err)
          if (isAdminOperation) {
            App.delegateId = null
          }

          if (postEvalResult == BREAK_LAST) {
            return
          }

          if (callback != null) {
            callback(url, res, err)
            return
          }

          if (typeof App.autoTestCallback == 'function') {
            App.autoTestCallback('Error when testing: ' + err + '.\nurl: ' + url + ' \nrequest: \n' + JSON.stringify(req, null, '    '), err)
          }

          App.onResponse(url, {request: {url: url, headers: header, data: req}}, err)
        }

        var retryReq = function (err) {
          if (retry == null || retry < 0) {
            onHttpCatch(err)
            return false
          }

          retry --
          try {
            setTimeout(function () {
              sendRequest(isAdminOperation, method, type, url, req, header, callback)
            }, wait < 0 ? 0 : wait)
          } catch (e) {
            App.log('request retryReq retry = ' + retry + ' >> try {\n' +
                '            sendRequest(isAdminOperation, method, type, url, req, header, callback)\n' +
                '          } catch (e) = ' + e.message)
            return retryReq(err)
          }

          return true
        }

        var sendRequest = function (isAdminOperation, method, type, url, req, header, callback) {
          var hs = ""
          if (isDelegate && header != null) {
            for (var k in header) {
                var v = k == null ? null : header[k]
                if (k == null || k.toLowerCase() == 'apijson-delegate-id') {
                    continue
                }
                hs += '\n' + k + ': ' + (v instanceof Object ? JSON.stringify(v) : v)
            }
          }

          var isParam = HTTP_URL_ARG_TYPES.indexOf(type) >= 0

          if (req != null && JSONResponse.getType(req) == 'object') { // 支持 URL 里有 Path Variable，例如 http://apijson.cn:8080/{method}/{table}
            var ind = -1 // 支持 ?id={id} 这种动态参数  url.indexOf('?')
            var uri = ind < 0 ? url : url.substring(0, ind)

            var newReq = {}
            for (var k in req) {
                var v = k == null ? null : req[k]
                var kind = uri.indexOf('{' + k + '}')
                if (kind >= 0) {
                  if (v instanceof Array) {
                    var multiInd = uri.indexOf(':=')
                  }

                  uri = uri.replaceAll('${' + k + '}', v).replaceAll('{{' + k + '}}', v).replaceAll('{' + k + '}', v)
                  continue
                }

                newReq[k] = v
            }

            url = uri + (ind < 0 ? '' : url.substring(ind))
            req = newReq
          }

          var interceptors = axios.interceptors
          if (interceptors) {
              interceptors.request.use(function (config) {
                config.metadata = { startTime: new Date().getTime()}
                return config;
              }, function (error) {
                return Promise.reject(error);
              })

              interceptors.response.use(function (response) {
                response.config.metadata.endTime = new Date().getTime()
                response.duration = response.config.metadata.endTime - response.config.metadata.startTime
                return response;
              }, function (error) {
                error.config.metadata.endTime = new Date().getTime();
                error.duration = error.config.metadata.endTime - error.config.metadata.startTime;
                return Promise.reject(error);
              })
          }

          // Object.defineProperty(req, 'constructor', {
          //   value: 'getInstance',
          //   enumerable: true,
          //   configurable: false,
          //   writable: true
          // })

          var isJSON = HTTP_JSON_TYPES.indexOf(type) >= 0;
          if (isJSON && (req.constructor != null || req.package != null || req.class != null || req.method != null || req.prototype != null)) {
            req = JSON.stringify(req)
            header = header || {}
            header['Content-Type'] = 'application/json'
          }

          // axios.defaults.withcredentials = true
          axios({
            method: method != null ? method : (HTTP_METHODS.indexOf(type) >= 0 ? type.toLowerCase() : (type == REQUEST_TYPE_PARAM ? 'get' : 'post')),
            url: (isDelegate ? (
                  App.server + '/delegate?$_type=' + (type || REQUEST_TYPE_JSON)
                  + (StringUtil.isEmpty(App.delegateId, true) ? '' : '&$_delegate_id=' + App.delegateId)
                  + '&$_delegate_url=' + encodeURIComponent(url)
                  + (StringUtil.isEmpty(hs, true) ? '' : '&$_headers=' + encodeURIComponent(hs.trim()))
                ) : (
                  App.isEncodeEnabled ? encodeURI(url) : url
                )
            ),
            params: isParam ? req : null,
            data: isJSON ? req : (HTTP_DATA_TYPES.indexOf(type) >= 0 ? toFormData(req) : null),
            headers: header,  //Accept-Encoding（HTTP Header 大小写不敏感，SpringBoot 接收后自动转小写）可能导致 Response 乱码
            withCredentials: true, //Cookie 必须要  type == REQUEST_TYPE_JSON
            // crossDomain: true
            timeout: timeout
          })
            .then(onHttpResponse)
            .catch(onHttpCatch)
        }

        var evalScript = isAdminOperation || caseScript_ == null ? function () {} : function (isPre, code, res, err) {
          var logger = console.log
          console.log = function(msg) {
            logger(msg)
            vOutput.value = StringUtil.get(msg)
          }

          App.view = 'output'
          vOutput.value = ''

          try {
//             var s = `(function () {
// var App = ` + App + `;
//
// var type = ` + type + `;
// var url = ` + url + `;
// var req = ` + (req == null ? null : JSON.stringify(req)) + `;
// var header = ` + (header == null ? null : JSON.stringify(header)) + `;
//
// ` + (isPre ? '' : `
// // var res = ` + (res == null ? null : JSON.stringify(res)) + `;
// var data = ` + (res == null || res.data == null ? null : JSON.stringify(res.data)) + `;
// var err = ` + (err == null ? null : JSON.stringify(err)) + `;
//
// `) + code + `
//           })()`
//
//             eval(s)

            var isTest = false;
            var isInject = false;
            var data = res == null ? null : res.data
            var result = eval(code)
            console.log = logger
            return result
          }
          catch (e) {
            console.log(e);
            console.log = logger

            App.loadingCount --

            // TODO if (isPre) {
            App.view = 'error'
            App.error = {
              msg: '执行脚本报错：\n' + e.message
            }

            if (callback != null) {
              callback(url, res, e)
            } else {
              // catch 中也 evalScript 导致死循环
              // if (isPre != true) {
              //   throw e
              // }

              // TODO 右侧底部新增断言列表
              App.onResponse(url, null, new Error('执行脚本报错：\n' + e.message)) // this.onResponse is not a function
              // callback = function (url, res, err) {}  // 仅仅为了后续在 then 不执行 onResponse
            }
          }

          return BREAK_ALL
        }

        // const preScript = function () {
        //   if (isAdminOperation) {
        //     return
        //   }

          var preScript = ''

          var globalPreScript = isAdminOperation || ignorePreScript || caseScript_ == null ? null : StringUtil.trim((globalScript.pre || {}).script)
          if (StringUtil.isNotEmpty(globalPreScript, true)) {
            preScript += globalPreScript + '\n\n' // evalScript(true, globalPreScript)
          }

          var accountPreScript = isAdminOperation || ignorePreScript || caseScript_ == null ? null : StringUtil.trim((accountScript.pre || {}).script)
          if (StringUtil.isNotEmpty(accountPreScript, true)) {
            preScript += accountPreScript + '\n\n' // evalScript(true, accountPreScript)
          }

          var casePreScript = isAdminOperation || ignorePreScript || caseScript_ == null ? null : StringUtil.trim((caseScript.pre || {}).script)
          if (StringUtil.isNotEmpty(casePreScript, true)) {
            preScript += casePreScript + '\n\n' // evalScript(true, casePreScript)
          }

          var preEvalResult = null;
          if (StringUtil.isNotEmpty(preScript, true)) {
            preEvalResult = evalScript(true, preScript)
          }

        // }

        evalPostScript = isAdminOperation || caseScript_ == null ? function () {} : function (url, res, err) {
          var postScript = ''

          var casePostScript = StringUtil.trim((caseScript.post || {}).script)
          if (StringUtil.isNotEmpty(casePostScript, true)) {
            postScript += casePostScript + '\n\n' // evalScript(false, casePostScript, res, err)
          }

          var accountPostScript = StringUtil.trim((accountScript.post || {}).script)
          if (StringUtil.isNotEmpty(accountPostScript, true)) {
            postScript += accountPostScript + '\n\n' // evalScript(false, accountPostScript, res, err)
          }

          var globalPostScript = StringUtil.trim((globalScript.post || {}).script)
          if (StringUtil.isNotEmpty(globalPostScript, true)) {
            postScript += globalPostScript + '\n\n' // evalScript(false, globalPostScript, res, err)
          }

          if (StringUtil.isNotEmpty(postScript, true)) {
            if (StringUtil.isNotEmpty(preScript, true)) { // 如果有副作用代码，则通过判断 if (isPre) {..} 在里面执行
              postScript = preScript + '\n\n// request >>>>>>>>>>>>>>>>>>>>>>>>>> response \n\n' + postScript
            }

            return evalScript(false, postScript, res, err)
          }

          return null;
        }

        if (preEvalResult == BREAK_ALL) {
          return
        }

        type = type || REQUEST_TYPE_JSON
        url = StringUtil.noBlank(url)
        if (url.startsWith('/')) {
          url = (isAdminOperation ? this.server : this.getBaseUrl()) + url
        }

        var isDelegate = (isAdminOperation == false && this.isDelegateEnabled)
         || (isAdminOperation && (url.indexOf('://apijson.cn:9090') > 0 || url.indexOf('.devin.ai') > 0))

        if (header != null && header.Cookie != null) {
          if (isDelegate) {
            header['Set-Cookie'] = header.Cookie
            delete header.Cookie
          }
          else if (IS_BROWSER) {
            document.cookie = header.Cookie
          }
        } else if (IS_NODE) {
          var curUser = isAdminOperation ? this.User : this.getCurrentAccount()
          if (curUser != null && curUser[isEnvCompare ? 'phone' : 'cookie'] != null) {
            if (header == null) {
              header = {}
            }

            // Node 环境内通过 headers 设置 Cookie 无效
            header.Cookie = isEnvCompare ? this.otherEnvCookieMap[curUser.phone + '@' + baseUrl] : curUser.cookie
          }
        }

        var delegateId = isEnvCompare ? this.otherEnvDelegateId : this.delegateId
        if (isDelegate && delegateId != null && (header == null || header['Apijson-Delegate-Id'] == null)) {
          if (header == null) {
            header = {};
          }
          header['Apijson-Delegate-Id'] = delegateId
        }


        if (IS_NODE) {
          if (DEBUG) {
            log('req = ' + JSON.stringify(req, null, '  '))
          }
          // 低版本 node 报错 cannot find module 'node:url' ，高版本报错 TypeError: axiosCookieJarSupport is not a function
          //   const axiosCookieJarSupport = require('axios-cookiejar-support').default;
          //   const tough = require('tough-cookie');
          //   axiosCookieJarSupport(axios);
          //   const cookieJar = new tough.CookieJar();
          //   axios.defaults.jar = cookieJar;
          //   axios.defaults.withCredentials = true;

          // const {parse, stringify, toJSON, fromJSON} = require('flatted');
          // JSON.stringify = stringify;
          // JSON.parse = parse;

          // const CircularJSON = require('circular-json');
          // JSON.stringify = CircularJSON.stringify;
          // JSON.parse = CircularJSON.parse;
        }

        if (preEvalResult == BREAK_LAST) {
          return
        }

        retryReq()
      },

      lastReqTime: 0,
      /**请求回调
       */
      onResponse: function (url, res, err) {
        if (res == null) {
          res = {}
        } else {
          var time = res.config == null || res.config.metadata == null ? null : res.config.metadata.startTime;
          if (time != null && time > 0 && time < this.lastReqTime) {
            return
          }

          this.lastReqTime = time == null || time <= 0 ? 0 : time;
        }

        if (DEBUG) {
          log('onResponse url = ' + url + '\nerr = ' + err + '\nreq = \n'
            + (res.request == null || res.request.data == null ? 'null' : JSON.stringify(res.request.data))
            + '\n\nres = \n' + (res.data == null ? 'null' : JSON.stringify(res.data))
          )
        }

        if (err != null) {
          if (IS_BROWSER) {
            var errObj = err instanceof Array == false && err instanceof Object ? err : {}
            var data = (errObj.response || {}).data
            var msg = typeof data == 'string' ? StringUtil.trim(data) : JSON.stringify(data, null, '    ')
            msg = "Response:\nurl = " + url + "\nerror = " + err.message + (StringUtil.isEmpty(msg) ? '' : '\n\n' + msg) + '\n\n' + ERR_MSG
            // vOutput.value = "Response:\nurl = " + url + "\nerror = " + err.message;
            this.view = 'error';
            this.error = {
              msg: msg
            }
            this.output = msg
          }
        }
        else {
          if (IS_BROWSER) {
            var data = res.data || {}
            var isStr = typeof data == 'string'
            if (isSingle && (isStr != true) && data instanceof Object && (data instanceof Array == false) && JSONResponse.isSuccess(data)) { //不格式化错误的结果
              data = JSONResponse.formatObject(data);
            }
            this.jsoncon = isStr ? data : JSON.stringify(data, null, '    ')
            this.view = 'code' // isStr ? 'output' : 'code'

            vOutput.value = isStr ? data : ''
          }

          // 会导致断言用了这个
          // if (this.currentRemoteItem == null) {
          //   this.currentRemoteItem = {}
          // }
          // if (this.currentRemoteItem.TestRecord == null) {
          //   this.currentRemoteItem.TestRecord = {}
          // }
          // this.currentRemoteItem.TestRecord.response = data
        }
      },


      /**处理复制事件
       * @param event
       */
      doOnCopy: function(event) {
        var target = event.target;
        var selectionStart = target.selectionStart;
        var selectionEnd = target.selectionEnd;

        if (target == vUrl) {
          try {
            var contentType = CONTENT_TYPE_MAP[this.type];
            var json = this.getRequest(vInput.value)
            var header = this.getHeader(vHeader.value);
            var headerStr = '';
            if (header != null) {
              for (var k in header) {
                var v = header[k];
                headerStr += '\n' + k + ': ' + StringUtil.get(v);
              }
            }

            console.log('复制时自动转换:\n'
            + `Request URL: ` + vUrl.value + `
Request Method: ` + (this.type == REQUEST_TYPE_PARAM ? 'GET' : 'POST') + (StringUtil.isEmpty(contentType, true) ? '' : `
Content-Type: ` + contentType) + (StringUtil.isEmpty(headerStr, true) ? '' : headerStr)
              + '\n\n' + JSON.stringify(json));
          } catch (e) {
            log(e)
          }
        }
        else if (target == vHeader || target == vRandom) {  // key: value 转 { "key": value }
          if (selectionStart < 0 || selectionStart <= selectionEnd) {
            try {
              var selection = selectionStart < 0 ? target.value : StringUtil.get(target.value).substring(selectionStart, selectionEnd);
              var lines = StringUtil.split(selection, '\n');
              var json = {};

              for (var i = 0; i < lines.length; i ++) {
                var l = StringUtil.trim(lines[i]) || '';
                if (l.startsWith('//')) {
                  continue;
                }

                var ind = l.lastIndexOf(' //');
                l = ind < 0 ? l : StringUtil.trim(l.substring(0, ind));

                ind = l.indexOf(':');
                if (ind >= 0) {
                  var left = target == vHeader ? StringUtil.trim(l.substring(0, ind)) : l.substring(0, ind);
                  json[left] = StringUtil.trim(l.substring(ind + 1));
                }
              }

              if (Object.keys(json).length > 0) {
                var txt = JSON.stringify(json)
                console.log('复制时自动转换:\n' +  txt)
                navigator.clipboard.writeText(selection + '\n\n' + txt);
                alert('复制内容最后拼接了，控制台 Console 也打印了：\n' + txt);
              }
            } catch (e) {
              log(e)
            }
          }
        }

      },

      /**处理粘贴事件
       * @param event
       */
      doOnPaste: function(event) {
        var paste = (event.clipboardData || window.clipboardData || navigator.clipboard).getData('text');
        var target = event.target;
        var selectionStart = target.selectionStart;
        var selectionEnd = target.selectionEnd;

        if (StringUtil.isNotEmpty(paste, true) && (StringUtil.isEmpty(target.value, true)
          || selectionStart <= 0 && selectionEnd >= StringUtil.get(target.value).length)) {
          if (target == vUrl) {  // TODO 把 Chrome 或 Charles 等抓到的 Response Header 和 Content 自动粘贴到 vUrl, vHeader
            try {
              if (paste.trim().indexOf('\n') > 0) {  // 解决正常的 URL 都粘贴不了
                var contentStart = 0;
                var lines = StringUtil.split(paste, '\n');
                var header = '';

                for (var i = 0; i < lines.length; i++) {
                  var l = StringUtil.trim(lines[i]);
                  var ind = l.indexOf(':');
                  var left = ind < 0 ? '' : StringUtil.trim(l.substring(0, ind));

                  if (/^[a-zA-Z0-9\- ]+$/g.test(left)) {
                    var lowerKey = left.toLowerCase();
                    var value = l.substring(ind + 1).trim();

                    if (lowerKey == 'host') {
                      this.setBaseUrl(value.endsWith(':443') ? 'https://' + value.substring(0, value.length - ':443'.length) : 'http://' + value);
                      event.preventDefault();
                    }
                    else if (lowerKey == 'request method') {
                      value = value.toUpperCase();
                      this.method = value
                      // this.type = value == 'GET' ? 'PARAM' : (value == 'POST' ? 'JSON' : value);
                      event.preventDefault();
                    }
                    else if (lowerKey == 'content-type') {
                      var type = vType.value != 'JSON' ? null : CONTENT_VALUE_TYPE_MAP[value];
                      if (StringUtil.isEmpty(type, true) != true) {
                        // this.type = type;
                        event.preventDefault();
                      }
                    }
                    else if (lowerKey == 'request url') {
                      vUrl.value = value;
                      event.preventDefault();
                    }
                    else if (StringUtil.isEmpty(lowerKey, true) || lowerKey.startsWith('accept-')
                      || lowerKey.startsWith('access-control-') || IGNORE_HEADERS.indexOf(lowerKey) >= 0) {
                      // 忽略
                    }
                    else {
                      header += '\n' + left + ': ' + StringUtil.trim(l.substring(ind + 1));
                    }

                    contentStart += lines[i].length + 1;
                  }
                  else {
                    if (ind <= 0 || StringUtil.isEmpty(l) || l.startsWith('HTTP/') || l.startsWith('HTTPS/')) {  // HTTP/1.1 200
                      contentStart += lines[i].length + 1;
                      continue;
                    }

                    var ind = l.indexOf(' ');
                    var m = ind < 0 ? '' : StringUtil.trim(l.substring(0, ind));
                    if (APIJSON_METHODS.indexOf(m.toLowerCase()) >= 0) {  // POST /gets HTTP/1.1
                      contentStart += lines[i].length + 1;
                      var t = m.toUpperCase()
                      this.method = t
                      // this.type = t == 'GET' ? 'PARAM' : (t == 'POST' ? 'JSON' : t);

                      l = l.substring(ind).trim();
                      ind = l.indexOf(' ');
                      var url = ind < 0 ? l : l.substring(0, ind);
                      if (url.length > 0 && url != '/') {
                        vUrl.value = this.getBaseUrl() + (url.startsWith('/') ? url : '/' + url);
                      }

                      event.preventDefault();
                      continue;
                    }

                    var content = StringUtil.trim(paste.substring(contentStart));
                    var json = null;
                    try {
                      json = JSON5.parse(content);  // { "a":1, "b": "c" }
                    }
                    catch (e) {
                      log(e)
                      try {
                        json = getRequestFromURL('?' + content, true);  // a=1&b=c
                      } catch (e2) {
                        log(e2)
                      }
                    }

                    vInput.value = json == null ? '' : JSON.stringify(json, null, '    ');
                    event.preventDefault();
                    break;
                  }

                }

                if (StringUtil.isEmpty(header, true) != true) {
                  vHeader.value = StringUtil.trim(header);
                  event.preventDefault();
                }
              }
            }
            catch (e) {
              log(e)
            }
          }
          else if (target == vHeader || target == vRandom) {  // { "key": value } 转 key: value
            try {
              var json = JSON5.parse(paste);
              var newStr = '';
              for (var k in json) {
                var v = json[k];
                if (v instanceof Object || v instanceof Array) {
                  v = JSON.stringify(v);
                }
                newStr += '\n' + k + ': ' + (target != vHeader && typeof v == 'string' ? "'" + v.replaceAll("'", "\\'") + "'" : StringUtil.get(v));
              }
              target.value = StringUtil.trim(newStr);
              event.preventDefault();
            }
            catch (e) {
              log(e)
            }
          }
          else if (target == vInput) {  // key: value 转 { "key": value }
            try {
              try {
                JSON5.parse(paste);  // 正常的 JSON 就不用转了
              }
              catch (e) {
                var lines = StringUtil.split(paste, '\n');
                var json = {};

                for (var i = 0; i < lines.length; i++) {
                  var l = StringUtil.trim(lines[i]) || '';
                  if (l.startsWith('//')) {
                    continue;
                  }

                  var ind = l.lastIndexOf(' //');
                  l = ind < 0 ? l : StringUtil.trim(l.substring(0, ind));

                  ind = l.indexOf(':');
                  if (ind >= 0) {
                    var left = target == vHeader ? StringUtil.trim(l.substring(0, ind)) : l.substring(0, ind);
                    if (left.indexOf('=') >= 0 || left.indexOf('&') >= 0) {
                      try {
                        json = getRequestFromURL('?' + paste, true);
                        if (Object.keys(json).length > 0) {
                          break;
                        }
                      } catch (e2) {
                        log(e)
                      }
                    }

                    json[left] = StringUtil.trim(l.substring(ind + 1));
                  }
                }

                if (Object.keys(json).length <= 0) {
                  json = getRequestFromURL('?' + paste, true);
                }

                if (Object.keys(json).length > 0) {
                  vInput.value = JSON.stringify(json, null, '    ');
                  event.preventDefault();
                }
              }
            }
            catch (e) {
              log(e)
            }
          }
        }

      },

      /**处理按键事件
       * @param event
       */
      doOnKeyUp: function (event, type, isFilter, item) {
        var keyCode = event.keyCode ? event.keyCode : (event.which ? event.which : event.charCode);
        var isEnter = keyCode == 13

        if (type == 'ask') {
          if (isEnter) {
            event.preventDefault();

            if (event.shiftKey) {
              vAskAI.value = StringUtil.trim(vAskAI.value) + '\n'
              return
            }

            var query = vAskAI.value = StringUtil.trim(vAskAI.value)

            var isRes = false;

            if (StringUtil.isEmpty(query)) {
              var view = this.view;
              var isCode = view == 'code'

// 太长             var output = view == 'error' || view == 'output' ? this.output : null;
              var output = view == 'error' ? this.output : null;
              var d = view == 'markdown' || view == 'html' ? doc : null;

              var res = isCode ? JSON.parse(this.jsoncon) : null;
//              res = this.removeDebugInfo(res);
              if (JSONResponse.isObject(res)) {
                  delete res['trace:stack']
                  delete res['debug:info|help']
              }
              var resStr = res == null ? null : JSON.stringify(res)

              var headers = isCode ? (this.currentHttpResponse || {}).headers : null
              var headerStr = ''
              if (headers != null) {
                  for (var k in headers) {
                    var v = headers[k];
                    headerStr += '\n' + k + ': ' + StringUtil.get(v);
                  }
              }

              var curItem = (this.isTestCaseShow != true ? this.currentRandomItem : this.remotes[this.currentDocIndex]) || {}
              var curDoc = curItem.Flow || {}
              var curRecord = curItem.TestRecord || {}
              var curRandom = curItem.Input || {}

//              var isRandom = this.isTestCaseShow != true && this.isRandomShow == true
//              var tests = this.tests[String(this.currentAccountIndex)] || {}
//              var currentResponse = (tests[isRandom ? random.flowId : document.id] || {})[
//                  isRandom ? (random.id > 0 ? random.id : (random.toId + '' + random.id)) : 0
//              ]
//
//              resStr = currentResponse != null ? JSON.stringify(currentResponse) : curRecord.response;

              if (this.isTestCaseShow != true) {
                  vAskAI.value = "### Request:\n" + this.method + " " + this.type + " " + this.getUrl()
                              + (StringUtil.isEmpty(vHeader.value) ? '' : "\n" + StringUtil.trim(vHeader.value))
                              + (StringUtil.isEmpty(vInput.value) ? '' : "\n\n```js\n" + StringUtil.trim(vInput.value) + '\n```')
                              + (isCode ? "\n\n### Response: " : '')
                              + (StringUtil.isEmpty(headerStr) ? '' : '\n' + StringUtil.trim(headerStr))
                              + (StringUtil.isEmpty(resStr) ? '' : "\n\n```js\n" + resStr + '\n```')
                              + (StringUtil.isEmpty(vAskAI.value) ? '' : '\n\n' + StringUtil.trim(vAskAI.value)) + '\n'
                              + (StringUtil.isEmpty(output) ? '' : '\n\n### 提示信息: \n' + StringUtil.trim(output))
                              // 太长 + (StringUtil.isEmpty(d) ? '' : '\n\n### 文档: \n' + d)
                              + '\n'
              } else {
                  vAskAI.value = "### Request:\n" + curDoc.method + " " + curDoc.type + " " + (this.getBaseUrl() + curDoc.url)
                              + (StringUtil.isEmpty(curDoc.header) ? '' : "\n" + StringUtil.trim(curDoc.header))
                              + (StringUtil.isEmpty(curDoc.request) ? '' : "\n\n```js\n" + StringUtil.trim(curDoc.request) + '\n```')
                              + (isCode ? "\n\n### Response: " : '')
                              + (StringUtil.isEmpty(curRecord.header) ? '' : '\n' + StringUtil.trim(curRecord.header))
                              + (StringUtil.isEmpty(resStr) ? '' : "\n\n```js\n" + resStr + '\n```')
                              + (StringUtil.isEmpty(vAskAI.value) ? '' : '\n\n' + StringUtil.trim(vAskAI.value)) + '\n'
                              + (StringUtil.isEmpty(output) ? '' : '\n\n### 提示信息: \n' + StringUtil.trim(output))
                              // 太长 + (StringUtil.isEmpty(d) ? '' : '\n\n### 文档: \n' + d)
                              + '\n'
              }

              isRes = true;
            }

            const user_query = StringUtil.trim(vAskAI.value);
            if (StringUtil.isEmpty(user_query)) {
              alert('请输入问题！')
              return;
            }

            function onMessage(item) {
              var data2 = item == null ? null : item.data
              if (data2 == null || item.type != 'chunk') {
                return;
              }

              var answer = StringUtil.get(typeof data2 == 'string' ? data2 : (data instanceof Array ? data2.join() : JSON.stringify(data2)))
                .replaceAll('/wiki/Tencent/APIJSON#', 'https://deepwiki.com/Tencent/APIJSON/').replaceAll('/wiki/TommyLemon/AutoUI#', 'https://deepwiki.com/TommyLemon/AutoUI/');
              App.view = 'markdown';
              vOutput.value += answer;
              markdownToHTML(vOutput.value)
            }

            function queryResult() {
                App.request(true, HTTP_METHOD_GET, REQUEST_TYPE_PARAM, 'https://api.devin.ai/ada/query/' + App.uuid, {}, {}, function (url, res, err) {
//                    App.onResponse(url, res, err)
                    var data = res.data || {}
//                    var isOk = JSONResponse.isSuccess(data)

                    var msg = ''; // isOk ? '' : ('\nmsg: ' + StringUtil.get(data.msg))
                    if (err != null) {
                        msg += '\nerr: ' + err.message
                        vOutput.value = err.message
                        App.view = 'error';
                        return
                    }

                    var queries = data.queries || []
                    var last = queries[queries.length - 1] || {}
                    var query = last.user_query || user_query
                    var response = last.response || []
                    var answer = '#### ' + query + '\n<br/>';
                    for (var i = 0; i < response.length; i ++) {
                      //                      onMessage(response[i])
                      var item = response[i];
                      var data2 = item == null ? null : item.data;
                      if (data2 == null || item.type != 'chunk') {
                          continue;
                      }

                      answer += '\n' + StringUtil.trim(typeof data2 == 'string' ? data2 : (data2 instanceof Array ? data2.join() : JSON.stringify(data2)))
                        .replaceAll('/wiki/Tencent/APIJSON#', 'https://deepwiki.com/Tencent/APIJSON/').replaceAll('/wiki/TommyLemon/AutoUI#', 'https://deepwiki.com/TommyLemon/AutoUI/');
                    }

                    answer += '\n<br/>\n';

                    App.view = 'markdown';
                    vOutput.value = answer;
                    markdownToHTML(answer); // vOutput.value)
                })

//                App.uuid = null; // 解决第一次后的都不生效
                vAskAI.value = '';
            }

            function askAI() {
              vOutput.value = '#### ' + user_query + '\n<br/>';
              App.loadingCount ++;
              const ws = new WebSocket('wss://api.devin.ai/ada/ws/query/' + App.uuid);

              // 连接成功
              ws.onopen = () => {
                console.log('WebSocket connected');
                // 这里通常不需要主动发送内容，除非协议需要
              };

              // 收到消息
              ws.onmessage = (event) => {
                try {
                  const data = JSON.parse(event.data);
                  console.log('Message:', data);
                  onMessage(data)

                  if (data.type === 'chunk') {
                    console.log('Chunk Data:', data.data);
                  }
                } catch (err) {
                  console.error('Failed to parse message:', event.data);
                }
              };

              // 连接关闭
              ws.onclose = () => {
                console.log('WebSocket closed');
                App.loadingCount --;
                queryResult();
              };

              // 错误处理
              ws.onerror = (err) => {
                console.error('WebSocket error:', err);
                queryResult();
              };

            }

            // 可能少调用了 https://api2.amplitude.com/2/httpapi 导致不能同一个会话二次请求
//            if (StringUtil.isEmpty(this.uuid, true)) {
               try {
                 this.uuid = crypto.randomUUID();
               } catch (e) {
                 console.error('Failed to generate UUID:', e);
                 this.uuid = '';
               }

              if (StringUtil.isEmpty(this.uuid, true)) {
                this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                  return v.toString(16);
                });
              }
//            }

            this.adminRequest('https://api.devin.ai/ada/query', {
              "engine_id": vDeepSearch.checked ? "agent" : "multihop",
              "user_query": "<relevant_context>" + (isRes ? "这是用 HTTP 接口工具 TommyLemon/AutoUI 发请求后的响应结果，分析并" : "") + "用中文回答：</relevant_context><br/>\n" + user_query,
              "keywords": [],
              "repo_names": JSONObject.isAPIJSONPath(this.getMethod()) ? [
                "Tencent/APIJSON", "TommyLemon/AutoUI"
              ] : ["TommyLemon/AutoUI"],
              "additional_context": "",
              "query_id": this.uuid,
              "use_notes": false,
              "generate_summary": false
            }, {}, function (url, res, err) {
              App.onResponse(url, res, err)
              var data = res.data || {}
//              var isOk = JSONResponse.isSuccess(data)

              var msg = ''; // isOk ? '' : ('\nmsg: ' + StringUtil.get(data.msg))
              if (err != null) {
                msg += '\nerr: ' + err.message
                vOutput.value = err.message
                App.view = 'error';
                return
              }

              askAI();
            })
          }
          return
        }

        if (type == 'option') {
          if (isEnter) {
            this.selectInput(item);
          }
          return
        }

        if (type == 'project') {
          if (isEnter || item.host == (this.projectHost || {}).host) {
            this.projectHost = {project: item.project}
            this.saveCache('', 'projectHost', this.projectHost)
          }
          return
        }

        var project = (this.projectHost || {}).project

        if (isFilter && (type == 'caseGroup' || type == 'chainGroup')) {
          this.isCaseGroupEditable = true
//          this.isChainGroupEditable = true
        }

        var obj = event.srcElement ? event.srcElement : event.target;
        if ($(obj).attr('id') == 'vUrl') {
          vUrlComment.value = ''
          this.currentDocItem = null
          this.currentRemoteItem = null
        }

        if (isEnter) { // enter
          if (isFilter) {
            this.onFilterChange(type)
            return
          }

          if (type == null) {
// 无效，这时已经换行了           if (event.target == vUrl) {
//               event.preventDefault();
//            }
            this.send(false);
            return
          }

          if (type == 'chainGroupAdd' || type == 'chainGroup') {
            var isAdd = type == 'chainGroupAdd'
            var groupName = item == null ? null : item.groupName
            if (StringUtil.isEmpty(groupName)) {
               alert('请输入名称!')
               return
            }

            var groupId = item == null ? null : item.groupId
            if (groupId == null || groupId <= 0) {
              if (! isAdd) {
                alert('请选择有效的列表项!')
                return
              }

              groupId = new Date().getTime()
            }

            //修改 Flow
            this.adminRequest((isAdd ? '/post' : '/put'), {
              Chain: {
                'groupName': groupName,
                'groupId': isAdd ? groupId : null,
                'groupId{}': isAdd ? null : [groupId]
              },
              tag: 'Chain-group'
            }, {}, function (url, res, err) {
              App.onResponse(url, res, err)
              var isOk = JSONResponse.isSuccess(res.data)

              var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((res.data || {}).msg))
              if (err != null) {
                msg += '\nerr: ' + err.msg
              }
              alert((isAdd ? '新增' : '修改') + (isOk ? '成功' : '失败') + (isAdd ? '! \n' :'！\ngroupId: ' + groupId) + '\ngroupName: ' + groupName + msg)

              App.isCaseGroupEditable = ! isOk
            })

            return
          }

          if (type == 'chainAdd') {
            var groupName = item == null ? null : item.groupName
            if (StringUtil.isEmpty(groupName)) {
               alert('请输入名称!')
               return
            }

            var search = StringUtil.isEmpty(groupName, true) ? null : '%' + StringUtil.trim(groupName).replaceAll('_', '\\_').replaceAll('%', '\\%') + '%'
            var methods = this.methods
            var types = this.types

            //修改 Flow
            this.adminRequest('/get', {
                format: false,
                'Flow[]': {
                  'count': 100, //200 条测试直接卡死 0,
                  'page': 0,
                  'Flow': {
                    // '@column': 'id,userId,version,date,name,operation,method,type,url,request,screenshot',
                    '@order': 'version-,time-',
                    'userId': this.User.id,
                    'project': StringUtil.isEmpty(project, true) ? null : project,
                    'name$': search,
                    'operation$': search,
                    'url$': search,
                    // 'group{}': group == null || StringUtil.isNotEmpty(groupUrl) ? null : 'length(group)<=0',
                    // 'group{}': group == null ? null : (group.groupName == null ? "=null" : [group.groupName]),
                    '@combine': search == null ? null : 'name$,operation$,url$',
                    'method{}': methods == null || methods.length <= 0 ? null : methods,
                    'type{}': types == null || types.length <= 0 ? null : types,
                    // '@null': 'sqlauto', //'sqlauto{}': '=null'
                    // '@having': StringUtil.isEmpty(groupUrl) ? null : "substring_index(substr,'/',1)<0"
                  }
                }
            }, {}, function (url, res, err) {
              App.onResponse(url, res, err)
              var isOk = JSONResponse.isSuccess(res.data)

              var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((res.data || {}).msg))
              if (err != null) {
                msg += '\nerr: ' + err.msg
              }
              if (! isOk) {
                alert(isOk ? '选择右侧接口来添加' : '查询失败！\ngroupName: ' + groupName + msg)
                return
              }

              var list = res.data['Flow[]'] || []
              var options = []
              for (var i = 0; i < list.length; i ++) {
                  var item = list[i] || {}
                  options.push({
                        name: '[' + item.method + '][' + item.type + ']', // + item.name + ' ' + item.url,
                        type: item.name,
                        comment: item.url,
                        value: item
                     }
                  )
              }

              App.options = options
              document.activeElement = vOption // vChainAdd.focusout()
              currentTarget = vChainAdd
              vOption.focus()
              //              App.showOptions(target, text, before, after);

            })

            return
          }

          if (type == 'caseGroup') {
            var groupUrl = item == null ? null : item.groupUrl
            var rawName = item == null ? null : item.rawName
            // if (StringUtil.isEmpty(url)) {
            //   alert('请选择有效的选项！item.url == null !')
            //   return
            // }

            //修改 Flow
            this.adminRequest('/put', {
              Flow: {
                'project': StringUtil.isEmpty(project, true) ? null : project,
                'group': item.groupName,
                '@raw': '@key',
                '@key':"url:substr(url,1,length(url)-length(substring_index(url,'/',-1))-1)",
                'url{}': [groupUrl],
                'group{}': rawName == null ? "=null" : [rawName]
              },
              tag: 'Flow-group'
            }, {}, function (url, res, err) {
              App.onResponse(url, res, err)
              var isOk = JSONResponse.isSuccess(res.data)

              var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((res.data || {}).msg))
              if (err != null) {
                msg += '\nerr: ' + err.msg
              }
              alert('修改' + (isOk ? '成功' : '失败') + '！\ngroupUrl: ' + item.groupUrl + '\ngroupName: ' + item.groupName + '\nrawName: ' + item.rawName + msg)

              App.isCaseGroupEditable = ! isOk
            })

            return
          }

          if (type == 'random' || type == 'randomSub') {
            var r = item == null ? null : item.Input
            if (r == null || r.id == null) {
              alert('请选择有效的选项！item.Input.id == null !')
              return
            }

            //修改 Random 的 count
            this.adminRequest('/put', {
              Input: {
                id: r.id,
                count: r.count,
                name: r.name
              // },
              // TestRecord: {
              //   id: 
              //   img: r.img,
              //   file: r.file,
              //   size: r.size,
              //   width: r.width,
              //   height: r.height
              },
              tag: 'Input'
            }, {}, function (url, res, err) {
              App.onResponse(url, res, err)
              var isOk = JSONResponse.isSuccess(res.data)

              var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((res.data || {}).msg))
              if (err != null) {
                msg += '\nerr: ' + err.msg
              }
              alert('修改' + (isOk ? '成功' : '失败') + '！\nurl: ' + item.url + '\nname: ' + r.name + msg)

              App.isRandomEditable = ! isOk
            })

            return
          }

          if (type == 'randomKeyPath') {
            var d = (this.currentRemoteItem || {}).Flow
            if (d == null || d.id == null) {
              alert('请选择有效的用例！item.Flow.id == null !')
              return
            }

            var path = (item || {}).path || vRandomKeyPath.value || 'image'
            if (StringUtil.isEmpty(path, true)) {
              alert('请输入有效的图片 JSON key 路径！以 / 分隔，key 中 / 等特殊字符需要 encodeURLComponent 转义')
              return
            }

            //修改 Flow 的 path
            this.adminRequest('/put', {
              Flow: {
                id: d.id,
                path: path,
              },
              tag: 'Flow'
            }, {}, function (url, res, err) {
              App.onResponse(url, res, err)
              var isOk = JSONResponse.isSuccess(res.data)

              var msg = isOk ? '' : ('\nmsg: ' + StringUtil.get((res.data || {}).msg))
              if (err != null) {
                msg += '\nerr: ' + err.msg
              }
              alert('修改' + (isOk ? '成功' : '失败') + '！\npath: ' + path)

              App.isRandomEditable = ! isOk
            })

            return
          }

        }
        else {
          if (isFilter) {
            return
          }
          if (type == 'random' || type == 'randomSub') {
            this.isRandomEditable = true
            return
          }
          if (type == 'document' || type == 'testCase') {
            return
          }

          this.urlComment = '';
          this.requestVersion = '';
          this.onChange(true);
        }
      },

      pageDown: function(type) {
        type = type || ''
        var page
        switch (type) {
          case 'caseGroup':
            page = this.caseGroupPage
            break
          case 'testCase':
            page = this.testCasePage
            break
          case 'random':
            page = this.randomPage
            break
          case 'randomSub':
            page = this.randomSubPage
            break
          default:
            page = this.page
            break
        }

        if (page == null) {
          page = 0
        }

        if (page > 0) {
          page --
          switch (type) {
            case 'caseGroup':
              this.caseGroupPage = page
              break
            case 'testCase':
              this.testCasePage = page
              break
            case 'random':
              this.randomPage = page
              break
            case 'randomSub':
              this.randomSubPage = page
              break
            default:
              this.page = page
              break
          }

          this.onFilterChange(type)
        }
      },
      pageUp: function(type) {
        type = type || ''
        switch (type) {
          case 'caseGroup':
            this.caseGroupPage ++
            break
          case 'testCase':
            this.testCasePage ++
            break
          case 'random':
            this.randomPage ++
            break
          case 'randomSub':
            this.randomSubPage ++
            break
          default:
            this.page ++
            break
        }
        this.onFilterChange(type)
      },
      onDisableChange: function ($event) {
        this.onFilterChange('random')
      },
      onFilterChange: function(type) {
        type = type || ''
        if (type == 'testCase' || type == 'caseGroup' || type == 'chainGroup') {
          var isChainShow = this.isChainShow
          var paths = isChainShow ? this.chainPaths : this.casePaths
          var index = paths.length - 1
          var group = paths[index]
          var groupId = group == null ? 0 : (group.groupId || 0)
          var groupUrl = group == null ? '' : (group.groupUrl || '')
          var groupKey = isChainShow ? groupId + '' : groupUrl

          if (type == 'chainGroup') {
            this.chainGroupPages[groupKey] = this.chainGroupPage
            this.chainGroupCounts[groupKey] = this.chainGroupCount
            this.chainGroupSearches[groupKey] = this.chainGroupSearch
            if (index < 0) {
              this.saveCache(this.server, 'chainGroupPage', this.chainGroupPage)
              this.saveCache(this.server, 'chainGroupCount', this.chainGroupCount)
              // this.saveCache(this.server, 'chainGroupSearch', this.chainGroupSearch)
            }
            this.saveCache(this.server, 'chainGroupPages', this.chainGroupPages)
            this.saveCache(this.server, 'chainGroupCounts', this.chainGroupCounts)
            // this.saveCache(this.server, 'chainGroupSearches', this.chainGroupSearches)

            this.selectChainGroup(this.currentChainGroupIndex, null)
          }
          else if (type == 'caseGroup') {
            this.caseGroupPages[groupKey] = this.caseGroupPage
            this.caseGroupCounts[groupKey] = this.caseGroupCount
            this.caseGroupSearches[groupKey] = this.caseGroupSearch
            if (index < 0) {
              this.saveCache(this.server, 'caseGroupPage', this.caseGroupPage)
              this.saveCache(this.server, 'caseGroupCount', this.caseGroupCount)
              // this.saveCache(this.server, 'caseGroupSearch', this.caseGroupSearch)
            }
            this.saveCache(this.server, 'caseGroupPages', this.caseGroupPages)
            this.saveCache(this.server, 'caseGroupCounts', this.caseGroupCounts)
            // this.saveCache(this.server, 'caseGroupSearches', this.caseGroupSearches)

            this.selectCaseGroup()
          }
          else {
            this.testCasePages[groupKey] = this.testCasePage
            this.testCaseCounts[groupKey] = this.testCaseCount
            this.testCaseSearches[groupKey] = this.testCaseSearch

            if (index < 0) {
              this.saveCache(this.server, 'testCasePage', this.testCasePage)
              this.saveCache(this.server, 'testCaseCount', this.testCaseCount)
            }
            this.saveCache(this.server, 'testCasePages', this.testCasePages)
            this.saveCache(this.server, 'testCaseCounts', this.testCaseCounts)
            // this.saveCache(this.server, 'testCaseSearches', this.testCaseSearches)

            this.resetTestCount(this.currentAccountIndex)

            this.isStatisticsEnabled = false
            this.remotes = null
            this.showTestCase(true, false)
          }
        }
        else if (type == 'random') {
          this.saveCache(this.server, 'randomPage', this.randomPage)
          this.saveCache(this.server, 'randomCount', this.randomCount)

          this.resetTestCount(this.currentAccountIndex, true)

          var cri = this.currentRemoteItem || {}
          cri.randoms = null
          this.randoms = null
          this.showRandomList(true, cri.Flow, false)
        }
        else if (type == 'randomSub') {
          this.saveCache(this.server, 'randomSubPage', this.randomSubPage)
          this.saveCache(this.server, 'randomSubCount', this.randomSubCount)

          this.resetTestCount(this.currentAccountIndex, true, true)

          var cri = this.currentRandomItem || {}
          this.randomSubs = null
          this.showRandomList(true, cri.Input, true)
        }
        else {
            docObj = null
            doc = null
            this.saveCache(this.server, 'page', this.page)
            this.saveCache(this.server, 'count', this.count)
            // this.saveCache(this.server, 'docObj', null)
            // this.saveCache(this.server, 'doc', null)

            this.onChange(false)

            //虽然性能更好，但长时间没反应，用户会觉得未生效
            // this.getDoc(function (d) {
            //   // vOutput.value = 'resolving...';
            //   App.setDoc(d)
            //   App.onChange(false)
            // });
        }
      },

      /**转为请求代码
       * @param rq
       */
      getCode: function (rq) {
        var s = '\n\n\n### 请求代码(自动生成) \n';
        switch (this.language) {
          case CodeUtil.LANGUAGE_PYTHON:
            s += '\n#### <= Web-Python: 注释符用 \'\\#\''
              + ' \n ```python \n'
              + CodeUtil.parsePythonRequest(null, parseJSON(rq), 0, isSingle, vInput.value)
              + '\n ``` \n注：关键词转换 null: None, false: False, true: True';
            break;
          default:
            s += '\n没有生成代码，可能生成代码(封装,解析)的语言配置错误。\n';
            break;
        }

        if (((this.User || {}).id || 0) > 0) {
          s += '\n\n#### 开放源码 '
            + '\nAPIJSON 接口测试: https://github.com/TommyLemon/APIAuto '
            + '\nAPIJSON 单元测试: https://github.com/TommyLemon/UnitAuto '
            + '\nAPIJSON 中文文档: https://github.com/vincentCheng/apijson-doc '
            + '\nAPIJSON 英文文档: https://github.com/ruoranw/APIJSONdocs '
            + '\nAPIJSON 官方网站: https://github.com/APIJSON/apijson.cn '
            + '\nAPIJSON -Java版: https://github.com/Tencent/APIJSON '
            + '\nAPIJSON - C# 版: https://github.com/liaozb/APIJSON.NET '
            + '\nAPIJSON - Go 版: https://github.com/glennliao/apijson-go '
            + '\nAPIJSON - PHP版: https://github.com/kvnZero/hyperf-APIJSON '
            + '\nAPIJSON -Node版: https://github.com/kevinaskin/apijson-node '
            + '\nAPIJSON -Python: https://github.com/zhangchunlin/uliweb-apijson '
            + '\n感谢热心的作者们的贡献，GitHub 右上角点 ⭐Star 支持下他们吧 ^_^';
        }

        return s;
      },


      /**显示文档
       * @param d
       **/
      setDoc: function (d) {
        if (d == null) { //解决死循环 || d == '') {
          return false;
        }
        doc = d;
        var url = StringUtil.trim((this.coverage || {}).url)
        if (url != null && url.startsWith('/')) {
          url = this.getBaseUrl() + url
          this.view = 'html'
          vHtml.innerHTML = '<iframe width="100%" height="100%" src="' + url + '"></iframe><br>'
          return true
        }
        var html = null // (this.coverage || {}).html
        if (StringUtil.isEmpty(html) != true) {
          this.view = 'html'
          vHtml.innerHTML = html
          return true
        }
        vOutput.value = (StringUtil.isEmpty(url, true) ? (StringUtil.isEmpty(html, true) ? '' : StringUtil.trim(html) + '<br>') : '<iframe src="' + url + '"></iframe><br>')
          + (this.isTestCaseShow ? '' : output) + (
          '\n\n\n## 文档 \n\n 通用文档见 [APIJSON通用文档](https://github.com/Tencent/APIJSON/blob/master/Flow.md#3.2) \n### 数据字典\n自动查数据库表和字段属性来生成 \n\n' + d
          + '<h3 align="center">关于</h3>'
        + '<p align="center">AutoUI - 📱 零代码快准稳 UI 智能录制回放平台'
        + '<br> 🚀 自动兼容任意宽高比分辨率屏幕，自动精准等待网络请求，录制回放快、准、稳！'
        + '<br>由 <a href="https://github.com/TommyLemon/AutoUI" target="_blank">AutoUI(前端网页工具)</a>, <a href="https://github.com/Tencent/APIJSON" target="_blank">APIJSON(后端接口服务)</a> 等提供技术支持'
          + '<br>遵循 <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache-2.0 开源协议</a>'
        + '<br>Copyright &copy; 2019-' + new Date().getFullYear() + ' Tommy Lemon'
          + '<br><a href="https://beian.miit.gov.cn/" target="_blank"><span >粤ICP备18005508号-1</span></a>'
          + '</p><br><br>'
        );

        this.view = 'markdown';
        markdownToHTML(vOutput.value);
        return true;
      },


      /**
       * 获取文档
       */
      getDoc: function (callback) {

      	var isTSQL = ['ORACLE', 'DAMENG'].indexOf(this.database) >= 0
      	var isNotTSQL = ! isTSQL

        var count = this.count || 100  //超过就太卡了
        var page = this.page || 0

        var schemas = StringUtil.isEmpty(this.schema, true) ? null : StringUtil.split(this.schema)

        var search = StringUtil.isEmpty(this.search, true) ? null : '%' + StringUtil.trim(this.search) + '%'
        this.adminRequest('/get', {
          format: false,
          '@database': StringUtil.isEmpty(this.database, true) ? undefined : this.database,
          // '@schema': StringUtil.isEmpty(this.schema, true) ? undefined : this.schema,
          '@schema': App.schema,
          'Device[]': {
            'count': count,
            'page': page,
            'Device': {
              '@order': 'name+'
            }
          },
          'System[]': {
            'count': count,
            'page': page,
              // 'table_name!$': ['\\_%', 'sys\\_%', 'system\\_%'],
              //FIXME  多个 schema 有同名表时数据总是取前面的  不属于 pg_class 表 'nspname': this.schema,
            'System': {
              '@order': 'name+,versionCode-,versionName-',
              'name$': search,
              'versionName$': search,
              '@combine': StringUtil.isEmpty(search) ? null : 'name$,versionName$'
            }
              // 界面又不显示这个字段，搜出来莫名其妙 'detail$': search,
              // '@combine': search == null ? null : 'tag$,detail$',
          }
        }, {}, function (url, res, err) {
          App.onDocumentListResponse(url, res, err, callback)
        })
      },

      lastDocReqTime: 0,
      onDocumentListResponse: function(url, res, err, callback) {
        if (err != null || res == null || res.data == null) {
          log('getDoc  err != null || res == null || res.data == null >> return;');
          if (callback != null) {
            callback('')
          }
          return;
        }

        var time = res.config == null || res.config.metadata == null ? 0 : (res.config.metadata.startTime || 0)
        if (time < this.lastDocReqTime) {
          return
        }

        this.lastDocReqTime = time;

//        log('getDoc  docRq.responseText = \n' + docRq.responseText);
        docObj = res.data || {};  //避免后面又调用 onChange ，onChange 又调用 getDoc 导致死循环


        //转为文档格式
          var doc = '';

          //[] <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
          var list = docObj == null ? null : docObj['[]'];
          CodeUtil.tableList = list;
          if (list != null) {
            if (DEBUG) {
              log('getDoc  [] = \n' + format(JSON.stringify(list)));
            }

            for (var i = 0; i < list.length; i++) {
              var item = list[i];

              //Table
              var table = item == null ? null : item.Input
              if (table == null) {
                continue;
              }
              if (DEBUG) {
                log('getDoc [] for i=' + i + ': table = \n' + format(JSON.stringify(table)));
              }

              // item.Table.table_name = table.table_name
              // item.Table.table_comment = table_comment

              var pkg = table.package

              // TODO 对 isAPIJSON 和 isRESTful 生成不一样的
              doc += '\n### ' + (i + 1) + '. ' + pkg + '\n'

              //Column[]

              var columnList = item['Input[]'];
              if (columnList == null) {
                continue;
              }
              if (DEBUG) {
                log('getDoc [] for ' + i + ': columnList = \n' + format(JSON.stringify(columnList)));
              }

              for (var j = 0; j < columnList.length; j++) {
                var column = (columnList[j] || {}); // [App.database != 'SQLSERVER' ? 'Column' : 'SysColumn'];
                var name = column == null ? null : column.class;
                if (name == null) {
                  continue;
                }

                if (DEBUG) {
                  log('getDoc [] for j=' + j + ': column = \n' + format(JSON.stringify(column)));
                }

                doc += '\n' + (j + 1) + ') ' + name + '(' + StringUtil.get(column.arguments) + ')';

              }

              doc += '\n\n\n';

            }

          }
          //[] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

          doc += '\n\n';

          //Function[] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


          App.onChange(false);

          if (callback != null) {
            callback(doc);
          }

//      	  log('getDoc  callback(doc); = \n' + doc);
      },

      getTableKey: function(database) {
        database = database || this.database
        return this.database == 'SQLSERVER' ? 'SysTable' : (['ORALCE', 'DAMENG'].indexOf(database) >= 0 ? 'AllTable' : 'Table')
      },
      getColumnKey: function(database) {
        database = database || this.database
        return this.database == 'SQLSERVER' ? 'SysColumn' : (['ORALCE', 'DAMENG'].indexOf(database) >= 0 ? 'AllColumn' : 'Column')
      },
      getTableObj: function(tableIndex) {
        var list = docObj == null ? null : docObj['[]']
        var item = list == null ? null : list[tableIndex]
        return item == null ? null : item[this.getTableKey()];
      },
      getColumnList: function(tableIndex) {
        var list = docObj == null ? null : docObj['[]']
        var item = list == null ? null : list[tableIndex]
        return item == null ? null : item['[]']
      },
      getColumnListWithModelName: function(modelName, schemaName) {
        var list = docObj == null ? null : docObj['[]']
        if (list != null) {
          for (var i = 0; i < list.length; i++) {
            var table = this.getTableObj(i)
            if (table != null && this.getModelName(i) == modelName
              && (schemaName == null || table.table_schema == schemaName)) {
              return list[i]['[]']
            }
          }
        }
        return null
      },
      getTableByName: function(tableName, schemaName) {
        var list = docObj == null ? null : docObj['[]']
        if (list != null) {
          for (var i = 0; i < list.length; i++) {
            var table = this.getTableObj(i)
            if (table != null && table.table_name == tableName
              && (schemaName == null || table.table_schema == schemaName)) {
              return table
            }
          }
        }

        return null
      },
      getTableByModelName: function(modelName, schemaName) {
        var list = docObj == null ? null : docObj['[]']
        if (list != null) {
          for (var i = 0; i < list.length; i++) {
            var table = this.getTableObj(i)
            if (table != null && this.getModelName(i) == modelName
              && (schemaName == null || table.table_schema == schemaName)) {
              return table
            }
          }
        }
        return null
      },
      getColumnTypeWithModelName: function(columnName, modelName, schemaName) {
        var columnList = this.getColumnListWithModelName(modelName, schemaName)
        if (columnList != null) {
          for (var j = 0; j < columnList.length; j++) {
            var column = this.getColumnObj(columnList, j)
            if (column != null && column.column_name == columnName) {
              return column
            }
          }
        }
        return null
      },
      getColumnObj: function(columnList, columnIndex) {
        return columnList == null ? null : (columnList[columnIndex] || {})[this.getColumnKey()];
      },

      getSchemaName: function(tableIndex) {
        var table = this.getTableObj(tableIndex)
        var sch = table == null ? null : table.table_shema
        if (StringUtil.isNotEmpty(sch)) {
          return sch
        }

        var schemas = StringUtil.isEmpty(this.schema, true) ? null : StringUtil.split(this.schema)
        return schemas == null || schemas.length != 1 ? null : this.schema
      },
      getTableName: function(tableIndex) {
        var table = this.getTableObj(tableIndex)
        return table == null ? '' : table.table_name
      },
      getColumnName: function(columnList, columnIndex) {
        var column = this.getColumnObj(columnList, columnIndex)
        return column == null ? '' : column.column_name
      },
      getModelName: function(tableIndex) {
        var table = this.getTableObj(tableIndex)
        var table_name = table == null ? null : table.table_name

        var accessMap = table_name == null ? null : CodeUtil.accessMap
        var access = accessMap == null ? null : accessMap[StringUtil.toLowerCase(table.table_schema) + '.' + StringUtil.toLowerCase(table_name)]
        var alias = access == null ? null : access.alias

        return StringUtil.isEmpty(alias, true) ? StringUtil.firstCase(table_name, true) : alias
      },
      getModelNameByTableName: function(tableName, schemaName) {
        var table = this.getTableByName(tableName, schemaName)
        var table_name = table == null ? null : table.table_name

        var accessMap = table_name == null ? null : CodeUtil.accessMap
        var access = accessMap == null ? null : accessMap[StringUtil.toLowerCase(table.table_schema) + '.' + StringUtil.toLowerCase(table_name)]
        var alias = access == null ? null : access.alias

        return StringUtil.isEmpty(alias, true) ? StringUtil.firstCase(table_name, true) : alias
      },

      getTotalAndCoverageString: function(typeName, count, total) {
    	  count = count || 0
    	  total = total || 0
        if (count > total) {
          count = total
        }
    	  return '共 ' + total + ' 个' + (typeName || '子项') + '，覆盖 ' + count  + ' 个'
		  + (Number.isInteger(total) != true || total <= 0 ? '' : '，覆盖率 ' + (100*count/total).toFixed(2) + '%');
      },
      getRealClassTotal: function(data, packageName) {
    	if (data == null) {
    		return 0;
    	}
    	var packageList = data.packageList
    	var len = packageList == null ? 0 : packageList.length
  		if (StringUtil.isEmpty(packageName, true)) {
  			return Math.max(data.classTotal || 0, len);
  		}
    	if (len <= 0) {
    		return 0;
    	}
  		for (var i in packageList) {
  			var pkgObj = packageList[i]
  			if (pkgObj != null && pkgObj['package'] == packageName) {
  				return Math.max(pkgObj.classTotal || 0, (pkgObj.classList || []).length || 0);
  			}
  		}
  		return 0;
	  },
	  getRealMethodTotal: function(data, packageName, className) {
		    if (data == null) {
	    	 return 0;
	      }
	    	var packageList = data.packageList
	    	var len = packageList == null ? 0 : packageList.length
  			if (StringUtil.isEmpty(packageName, true)) {
  				if (StringUtil.isEmpty(className, true)) {
  					return Math.max(data.classTotal || 0, len);
  				}
  				return 0;
  			}
	    	if (len <= 0) {
	    		return 0;
	    	}
		  for (var i in packageList) {
			  var pkgObj = packageList[i]
			  if (pkgObj != null && pkgObj['package'] == packageName) {
				  var classList = pkgObj.classList
				  var len2 = classList == null ? 0 : classList.length
				  if (StringUtil.isEmpty(className, true)) {
					 return Math.max(data.methodTotal || 0, len2);
				  }
				  if (len2 <= 0) {
			    	  return 0;
			      }
				  for (var j in classList) {
					  var clsObj = classList[j]
					  if (clsObj != null && clsObj['class'] == className) {
						  return Math.max(clsObj.methodTotal || 0, (clsObj.methodList || []).length || 0);
					  }
				  }
			  }
		  }
		  return 0;
	  },
      toDoubleJSON: function (json, defaultValue) {
        if (StringUtil.isEmpty(json)) {
          return defaultValue == null ? '{}' : JSON.stringify(defaultValue)
        }
        else if (json.indexOf("'") >= 0) {
          json = json.replace(/'/g, '"');
        }
        return json;
      },

      switchQuote: function (before) {
        if (before == null) {
          return before;
        }

        var newBefore = '';
        for (var i = 0; i < before.length; i++) {
          var chr = before.substring(i, i + 1); // .charAt(i);
          if (chr == '"') {
            newBefore += "'"; // chr = "'";
          }
          else if (chr == "'") {
            newBefore += '"'; // chr = '"';
          }
          else {
            newBefore += chr;
          }
        }
        return newBefore;
      },

      /**转为Markdown格式
       * @param s
       * @return {*}
       */
      toMD: function (s) {
        if (s == null) {
          return ''
        }

        if (s instanceof Object) {
          s = JSON.stringify(s)
        }

        if (typeof s != 'string') {
          return StringUtil.get(s)
        }

        //无效
        s = s.replace(/\|/g, '\|');
        s = s.replace(/\n/g, ' <br /> ');
        // s = s.replace(/ /g, '&ensp;');
        return s;
      },

      /**判断key是否为表名，用CodeUtil里的同名函数会在Safari上报undefined
       * @param key
       * @return
       */
      isTableKey: function (key) {
        log('isTableKey  typeof key = ' + (typeof key));
        if (key == null) {
          return false;
        }
        return /^[A-Z][A-Za-z0-9_]*$/.test(key);
      },

      log: function (msg) {
        // this.log('Main.  ' + msg)
      },

      getDoc4TestCase: function () {
        var list = this.remotes || []
        var doc = ''
        for (var i = 0; i < list.length; i ++) {
          var item = list[i] == null ? null : list[i].Flow
          if (item == null || item.name == null) {
            continue
          }
          doc += '\n\n#### ' + (item.version > 0 ? 'V' + item.version : 'V*') + ' ' + item.name  + '    ' + item.url
          doc += '\n```json\n' + item.request + '\n```\n'
        }
        return doc
      },

      enableCross: function (enable) {
        this.isCrossEnabled = enable
        this.crossProcess = enable ? '交叉账号:已开启' : '交叉账号:已关闭'
        this.saveCache(this.server, 'isCrossEnabled', enable)
      },

      enableML: function (enable) {
        this.isMLEnabled = enable
        this.testProcess = enable ? '机器学习:已开启' : '机器学习:已关闭'
        this.saveCache(this.server, 'isMLEnabled', enable)
        // if (enable) {
        //   this.isEnvCompareEnabled = false
        //   this.saveCache(this.server, 'isEnvCompareEnabled', this.isEnvCompareEnabled)
        // }

        this.resetTestCount(this.currentAccountIndex)

        this.remotes = null
        this.showTestCase(true, false)
      },

      resetTestCount: function (accountIndex, isRandom, isSub) {
        if (isRandom) {
          this.resetCount(isSub ? this.currentRandomItem : this.currentRemoteItem, isRandom, isSub, accountIndex)
          return
        }

        if (accountIndex == -1) {
          this.logoutSummary = this.resetCount(this.logoutSummary, false, false, accountIndex)
        }
        else if (accountIndex >= 0 && accountIndex < (this.accounts || []).length) {
          var accountItem = this.resetCount(this.getSummary(accountIndex), false, false, accountIndex)
          this.accounts[accountIndex] = accountItem
        }
      },

      onClickTestScript: function () {
        var logger = console.log
        console.log = function(msg) {
          logger(msg)
          vOutput.value = StringUtil.get(msg)
        }

        this.view = 'output'
        vOutput.value = ''

        try {
          var isTest = true
          var isPre = this.isPreScript

          var isAdminOperation = false
          var type = this.type
          var url = this.getUrl()
          var req = this.getRequest(vInput.value, {})
          var header = this.getHeader(vHeader.value)
          var callback = null

          var data = isPre ? undefined : (this.jsoncon == null ? null : parseJSON(this.jsoncon))
          var res = isPre ? undefined : {
            data: data
          }
          var err = isPre ? undefined : null

          var sendRequest = function (isAdminOperation, method, type, url, req, header, callback) {
            App.request(isAdminOperation, method, type, url, req, header, callback)
          }

          eval(vScript.value);
        }
        catch(e) {
          console.log(e);
          console.log = logger

          this.view = 'error'
          this.error = {
            msg: '执行脚本报错：\n' + e.message
          }
        }

        console.log = logger
      },

      /**事件配置，动态替换键值对
       * @param show
       */
      onClickTestRandom: function (isCross, callback) {
        this.isRandomTest = true
        this.reportId = new Date().getTime();
        this.isStatisticsEnabled = true
        this.testRandom(! this.isRandomListShow && ! this.isRandomSubListShow, this.isRandomListShow, this.isRandomSubListShow, null, isCross, true, callback)
      },
      testRandom: function (show, testList, testSubList, limit, isCross, isManual, callback) {
        this.isRandomEditable = false
        this.currentRandomIndex = -1
        this.sameIds = []
        this.missTruth = {}
        
        const isRecord = this.operate == OPERATE_TYPE_RECORD
        if (testList != true && testSubList != true) {
          this.testRandomProcess = ''
          this.testRandomWithText(show, callback)
        }
        else {
          var baseUrl = StringUtil.trim(this.getBaseUrl())
          if (baseUrl == '') {
            if (callback) {
              callback(true, 0, '请先输入有效的URL！')
            } else {
              alert('请先输入有效的URL！')
            }
            return
          }
          //开放测试
          // if (baseUrl.indexOf('/apijson.cn') >= 0 || baseUrl.indexOf('/39.108.143.172') >= 0) {
          //   alert('请把URL改成你自己的！\n例如 http://localhost:8080')
          //   return
          // }
          // if (baseUrl.indexOf('/apijson.org') >= 0) {
          //   alert('请把URL改成 http://apijson.cn:8080 或 你自己的！\n例如 http://localhost:8080')
          //   return
          // }

          const fullList = (testSubList ? App.randomSubs : App.randoms) || []
          var allCount = 0  // list.length
          const list = fullList // []
          // const list = isRecord ? fullList : []
          const stepIndexMap = this.stepIndexMap = this.stepIndexMap || []

          allCount = list.length;

          if (isRecord == false && allCount <= 0) {
            if (callback) {
              callback(true, 0, '请先获取随机配置\n点击[查看列表]按钮')
            } else {
              alert('请先获取随机配置\n点击[查看列表]按钮')
            }
            return
          }
          this.testRandomProcess = this.randomDoneCount >= allCount ? '' : ('正在准备...')

          App.randomAllCount = allCount
          App.randomDoneCount = 0

          var summaryItem = (testSubList ? this.currentRandomItem : this.currentRemoteItem) || {}
          if (isManual) {
            this.resetCount(summaryItem, true, testSubList, this.currentAccountIndex)
          } else {
            summaryItem.whiteCount = 0
            summaryItem.greenCount = 0
            summaryItem.blueCount = 0
            summaryItem.orangeCount = 0
            summaryItem.redCount = 0
          }
          summaryItem.totalCount = allCount

          const pkg = this.getPackage(this.host) || 'uiauto'
          const cls = this.getClass(this.host) || 'UIAutoApp'
          var methods = this.methods

          const inputList = []
          function parseCallback(inputList) {
            App.requestPost(false, baseUrl + '/method/invoke', {
              "package": pkg, // 'uiauto',
              "class": cls, // 'UIAutoApp',
              "constructor": 'getInstance',
              "method": isRecord ? 'prepareRecord' : 'prepareReplay',
              "methodArgs": isRecord ? ["boolean:true", "boolean:true", "boolean:true"] : [inputList, "int:0", "boolean:true", "boolean:true", {
                type: "JSONObject",
                value: (App.currentRemoteItem || {}).Flow || {}
              }]
            }, header, function (url_, res_, err_) {
              try {
                App.onResponse(url_, res_, err_)
                App.log('test  App.request >> res.data = ' + JSON.stringify(res_.data, null, '  '))
              } catch (e) {
                App.log('test  App.request >> } catch (e) {\n' + e.message)
              }

              if (res_.data == null || res_.data.code != 200) {
                alert('准备失败！' + (res_.data || {}).msg + '\n具体原因见右侧 JSON 结果及客户端日志')
                App.testRandomProcess = ''
                return
              }

              App.testRandomProcess = '正在' + (isRecord ? '录制' : '回放') + '...'
              // App.requestPost(false, '/method/invoke', {
              //   "package": pkg, // 'uiauto',
              //   "class": cls, // 'UIAutoApp',
              //   "constructor": 'getInstance',
              //   "method": 'onClickPlay',
              //   "static": false
              // }, header, function (url, res, err) {
              //   try {
              //     App.onResponse(url, res, err)
              //     App.log('test  App.request >> res.data = ' + JSON.stringify(res.data, null, '  '))
              //   } catch (e) {
              //     App.log('test  App.request >> } catch (e) {\n' + e.message)
              //   }

              if (isRecord) {
                App.loopEventList(list, inputList, allCount, 0, header, callback)
              } else {
                // setTimeout(function () {
                App.loopRandomTestResult(list, inputList, allCount, 0, header, null, isRecord ? function () {
                } : callback)
                // }, isRecord ? 1000 : 0)
              }
              // });

            });

            return true
          }

          var rspCount = 0;
          for (var i = 0; i < list.length; i++) {

            // for (var i = 0; i < (limit != null ? limit : list.length); i++) {  //limit限制子项测试个数
              const item = list[i]
              const input = item == null ? null : item.Input
              inputList[i] = input
              stepIndexMap[(input || {}).step || 0] = i
              if (input == null || StringUtil.isEmpty(input.config)) {
                rspCount ++
                if (rspCount == allCount) {
                  parseCallback(inputList)
                }
                continue
              }
              if (DEBUG) {
                this.log('test  input = ' + JSON.stringify(input, null, '  '))
              }

              const index = i

              const itemAllCount = input.count || 0
              // allCount += (itemAllCount - 1)  // 为什么减 1？因为初始化时 var allCount = list.length

              // UI 往上顶出屏幕
              // try {
              //   document.getElementById((testSubList ?  'randomSubItem' : 'randomItem') + index).scrollIntoView()
              // } catch (e) {
              //   console.log(e)
              // }

            ORDER_MAP = {}  //重置

            var method = input.method
            var type = input.type
            var json = input // this.getRequest(input.request, {})
            var url = input.url
            var header = this.getHeader(input.reqHeader || input.header)

            App[testSubList ? 'currentRandomSubIndex' : 'currentRandomIndex'] = index
              try {
                this.testRandomSingle(show, false, itemAllCount > 1 && ! testSubList, item, method, type, url, json, header, isCross, isManual, function (url, res, err) {
                  var data = null
                  if (res instanceof Object) {  // 可能通过 onTestResponse 返回的是 callback(true, 18, null)
                    data = res.data
                    try {
                      App.onResponse(url, res, err)
                      if (DEBUG) {
                        App.log('test  App.request >> res.data = ' + (data == null ? 'null' : JSON.stringify(data, null, '  ')))
                      }
                    } catch (e) {
                      App.log('test  App.request >> } catch (e) {\n' + e.message)
                    }
                  }

                  // App.compareResponse(res, allCount, list, index, item, data, true, App.currentAccountIndex, false, err, null, isCross, callback)
                  return true
                }, function (which, randomName, constConfig, constJson) {
                  inputList[index] = constJson
                  rspCount ++
                  if (rspCount == allCount) {
                    parseCallback(inputList)
                  }
                  return true
                }, function(count, isCross, isManual) {
                  return true
                })
              } catch (e) {
                this.compareResponse(null, allCount, list, index, item, data, true, this.currentAccountIndex, false, e, null, isCross, callback)
              }
            // }
          }
        }
      },

      loopEventList: function (list, inputList, allCount, offset, header, callback) {
        this.isRecording = true
        list = list || []
        var pkg = this.getPackage(this.host) || 'uiauto'
        var cls = this.getClass(this.host) || 'UIAutoApp'
        this.requestPost(false, '/method/invoke', {
          "static": true,
          "package": pkg, // 'uiauto',
          "class": cls, // 'UIAutoApp',
          "method": 'getEventList',
          "methodArgs": [{  // UIAutoApp app
            //   "type": "uiauto.UIAutoApp",
            //   "value": null  // TODO 可能要 {}
            // },{  // int limit
            "type": "int",
            "value": 20
          },{  // int offset
            "type": "int",
            "value": offset
          }]
        }, header, function (url, res, err) {
          try {
            App.onResponse(url, res, err)
            App.log('test  App.request >> res.data = ' + JSON.stringify(res.data, null, '  '))
          } catch (e) {
            App.log('test  App.request >> } catch (e2) {\n' + e.message)
          }

          var el = (res.data || {})['return']
          var count = el == null ? 0 : el.length
          if (count <= 0) {
            if (err == null && el instanceof Array && (res.data || {}).code == 200) {
              App.testRandomProcess = ''
              App.isRecording = false
              App.randomDoneCount = StringUtil.length(App.outputList)
              if (callback != null) {
                callback(true, allCount, '录制完成')
                return
              }
              alert("录制完成")
            }
            // else {
            //   const offset_ = offset
            //   setTimeout(function () {
            //     App.loopEventList(list, inputList, allCount, offset_, header, callback)
            //   }, 2000)
            // }
            // return;
          }

          for (var j = 0; j < count; j++) {
            const input = el[j]
            if (input == null) {
              continue
            }

            input.id = input.id || - j - allCount

            allCount ++
            list.push({
              Input: input
            })

            if (StringUtil.isEmpty(input.name)) {
              input.name = InputUtil.getShowContent(input)
            }
          }

          App.randoms = list
          const offset_ = list.length
          setTimeout(function () {
            App.loopRandomTestResult(list, inputList, allCount, StringUtil.length(App.outputList), header, true, function() {})
            App.loopEventList(list, inputList, allCount, offset_, header, callback)
          }, 1000)
        });
      },

      loopRandomTestResult: function (list, inputList, allCount, offset, header, once, callback) {
        list = list || []
        var pkg = this.getPackage(this.host) || 'uiauto'
        var cls = this.getClass(this.host) || 'UIAutoApp'
        this.requestPost(false, '/method/invoke', {
          "static": true,
          "package": pkg, // 'uiauto',
          "class": cls, // 'UIAutoApp',
          "method": 'getOutputList',
          "methodArgs": [{  // UIAutoApp app
          //   "type": "uiauto.UIAutoApp",
          //   "value": null  // TODO 可能要 {}
          // },{  // int limit
            "type": "int",
            "value": 10
          },{  // int offset
            "type": "int",
            "value": offset
          }]
        }, header, function (url, res, err) {
          try {
            App.onResponse(url, res, err)
            App.log('test  App.request >> res.data = ' + JSON.stringify(res.data, null, '  '))
          } catch (e) {
            App.log('test  App.request >> } catch (e2) {\n' + e.message)
          }

          // offset = Math.max(offset, App.outputList.length || 0)

          var ol = (res.data || {})['return']
          if (StringUtil.isEmpty(ol)) {
            if (ol instanceof Array && err == null && (res.data || {}).code == 200) {
              App.testRandomProcess = ''
              if (callback != null) {
                callback(true, allCount, '已完成单用例测试')
                return
              }
              alert("已完成单用例测试")
            }
            else if (once != true) {
              const offset_ = offset
              setTimeout(function () {
                App.loopRandomTestResult(list, inputList, allCount, offset_, header, once, callback)
              }, 2000)
            }
            return;
          }

          var outputList = App.outputList
          if (outputList == null || outputList.length <= 0) {
            App.outputList = outputList = ol || []
          } else {
            outputList.push(ol)
          }

          App.picDelayTime = 0
          for (var j = 0; j < ol.length; j++) {
            const oj = ol[j]
            var oInputId = oj == null ? null : oj.inputId || oj.randomId
            if (oInputId == null || oInputId == 0) { // oInputId <= 0
              continue
            }
            const ind = oj.step == null ? null : App.stepIndexMap[oj.step]
            if (! App.isRecording) {
              App.randomDoneCount = Math.max(App.randomDoneCount, ind || offset) // compareResponse 会再加 1 次
              App.testRandomProcess = ('正在测试: ' + App.randomDoneCount + '/')
            }

            // 部分非手动触发的事件(切换界面、HTTP 请求 Response 等) 导致位移不准确，必须全量匹配 var ind = j + offset
            for (var k = 0; k < list.length; k++) {
              const ik = list[k]
              const input = ik == null ? null : ik.Input;
              if (input != null && input.id == oInputId) {
                App.currentRandomIndex = k
                const resultIndex = k
                const response = oj // {TestRecord: oj, code: 200, msg: 'success'}
                response.code = response.code || 200
                // response.msg = response.msg || 'success'
                setTimeout(function () {  // 让图片切换更平滑，且保持和选项断言结果同时出现
                  App.compareResponse({data: {TestRecord: oj, code: 200, msg: 'success'}}, allCount, list, resultIndex, ik, response, true, App.currentAccountIndex, false, err)
                  // App.compareResponse(allCount, list, k, inputList[k], App.currentOutputList[k], true, App.currentAccountIndex, false, err)
                }, App.picDelayTime) // 200*resultIndex)

                var imgUrl = oj.screenshot
                if (StringUtil.isNotEmpty(imgUrl)) {
                  App.picDelayTime += 500
                  vAfter.src = ik.img = (imgUrl.indexOf('://') >= 0 ? '' : baseUrl) + '/download?filePath=' + encodeURI(imgUrl)
                }

                var tr = ik.TestRecord || {}
                var beforeUrl = StringUtil.trim(tr.screenshot)
                vBefore.src = (beforeUrl.indexOf('://') >= 0 ? '' : App.server) + '/download?filePath=' + encodeURI(beforeUrl)
                break
              }
            }
          }

          if (allCount < 0 || App.randomDoneCount < allCount) {
            if (once != true) {
              const offset_ = outputList.length
              setTimeout(function () {
                App.loopRandomTestResult(list, inputList, allCount, offset_, header, once, callback)
              }, 200)
            }
          }
          else if (allCount >= 0 && (once != true)) {
            if (callback != null) {
              callback(true, allCount, '已完成单用例测试')
              return
            }
            App.testRandomProcess = ''
            alert("已完成单用例测试")
          }
        });
      },
      
      /**事件配置，动态替换键值对
       * @param show
       * @param callback
       */
      testRandomSingle: function (show, testList, testSubList, item, method, type, url, json, header, isCross, isManual, callback, singleCallback, allCallback) {
        item = item || {}

        // 保证能调用自定义函数等 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        const scripts = this.scripts || {}
        const globalScript = (scripts.global || {})[0] || {}
        const accountScript = (scripts.account || {})[this.getCurrentAccountId() || 0] || {}
        const caseScript = (scripts.case || {})[this.getCurrentDocumentId() || 0] || {}

        var preScript = ''

        var globalPreScript = StringUtil.trim((globalScript.pre || {}).script)
        if (StringUtil.isNotEmpty(globalPreScript, true)) {
          preScript += globalPreScript + '\n\n'
        }

        var accountPreScript = StringUtil.trim((accountScript.pre || {}).script)
        if (StringUtil.isNotEmpty(accountPreScript, true)) {
          preScript += accountPreScript + '\n\n'
        }

        var casePreScript = StringUtil.trim((caseScript.pre || {}).script)
        if (StringUtil.isNotEmpty(casePreScript, true)) {
          preScript += casePreScript + '\n\n'
        }
        // 保证能调用自定义函数等 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


        var random = item.Input = item.Input || {}
        var subs = item['[]'] || []
        var existCount = subs.length
        subs = existCount <= 0 ? subs : parseJSON(JSON.stringify(subs))

        var count = random.count || 0
        var respCount = 0;

        for (var i = 0; i < count; i ++) {
          // var constConfig = i < existCount ? ((subs[i] || {}).Input || {}).config : this.getRandomConstConfig(random.config, random.id) //第1遍，把 key : expression 改为 key : value
          // var constJson = this.getRandomJSON(parseJSON(JSON.stringify(json)), constConfig, random.id) //第2遍，用新的 random config 来修改原 json

          const which = i;
          var rawConfig = testSubList && i < existCount ? ((subs[i] || {}).Input || {}).config : random.config
          // var keyPath = (StringUtil.isEmpty(vRandomKeyPath.value, true) ? 'image' : vRandomKeyPath.value)
          // rawConfig = (StringUtil.isEmpty(rawConfig, true) ? '' : rawConfig + '\n') + keyPath + ': '
          //     + JSON.stringify(this.host.indexOf('localhost') >= 0 || this.host.indexOf('127.0.0.1') >= 0 ? (item.img || this.img) : random.img)

          var cb = function (url, res, err) {
            if (callback != null) {
              callback(url, res, err, random)
            }
            else {
              App.onResponse(url, res, err)
            }
          };

          try {
            this.parseRandom(
              parseJSON(JSON.stringify(json)), header, rawConfig, random.id
              , ! testSubList, testSubList && i >= existCount, testSubList && i >= existCount
              , function (randomName, constConfig, constJson) {

                respCount ++;

                if (testSubList) {  //在原来已上传的基础上，生成新的
                  if (which >= existCount) {
                    //异步导致顺序错乱 subs.push({
                    subs[which] = {
                      Input: {
                        id: -i - 1, //表示未上传
                        toId: random.id == null ? 1 : random.id,  // 1 为了没选择测试用例时避免用 toId 判断子项错误
                        userId: random.userId,
                        flowId: random.flowId,
                        count: 1,
                        name: randomName || 'Temp ' + i,
                        config: constConfig
                      },
                      //不再需要，因为子项里前面一部分就是已上传的，而且这样更准确，交互更直观
                      // TestRecord: {  //解决子项始终没有对比标准
                      //   id: 0, //不允许子项撤回 tr.id, //表示未上传
                      //   userId: random.userId,
                      //   documentId: random.flowId,
                      //   testAccountId: tr.testAccountId,
                      //   randomId: -i - 1,
                      //   response: tr.response,
                      //   standard: tr.standard,
                      //   date: tr.date,
                      //   compare: tr.compare
                      // }
                    // })
                    };
                  }
                }
                else {
                  if (singleCallback != null && singleCallback(which, randomName, constConfig, constJson)) {
                    return
                  }

                  if (show == true) {
                    vInput.value = JSON.stringify(constJson, null, '    ');
                    App.send(false, cb, caseScript);
                  }
                  else {
                    var httpReq = {
                      "package": constJson.package || App.getPackage(url),
                      "class": constJson.class || App.getClass(url),
                      "this": constJson.this,
                      "constructor": constJson.constructor,
                      "classArgs": constJson.classArgs,
                      "method": constJson.name || App.getMethod(url),
                      "methodArgs": constJson.methodArgs,
                      "static": constJson.static
                    }
                    App.requestPost(false, '/method/invoke', httpReq, header, cb, caseScript);
                  }
                }

                if (testSubList && respCount >= count) {  // && which >= count - 1) {
                  if (App.currentRandomItem == null) {
                    App.currentRandomItem = {}
                  }
                  App.randomSubs = App.currentRandomItem.subs = subs
                  App.getCurrentRandomSummary().summaryType = 'total' // App.onClickSummary('total', true)
                  if (App.isRandomListShow == true) {
                    App.resetCount(item, true, false, App.currentAccountIndex)
                    item.subs = subs
                  }
                  if (respCount == count) {
                    if (allCallback != null && allCallback(count, isCross, isManual)) {
                      return
                    }

                    App.testRandom(false, false, true, count, isCross, isManual, callback)
                  }
                }

              },
              preScript
            );
          }
          catch (e) {
            cb(url, {}, e)
          }

        }  // for

      },

      resetParentCount: function (item, cri, isRandom, accountIndex) {
        if (cri == null || item == null) {
          return
        }

        if (item.whiteCount == null || item.whiteCount < 0) {
          item.whiteCount = 0
        }
        if (item.greenCount == null || item.greenCount < 0) {
          item.greenCount = 0
        }
        if (item.blueCount == null || item.blueCount < 0) {
          item.blueCount = 0
        }
        if (item.orangeCount == null || item.orangeCount < 0) {
          item.orangeCount = 0
        }
        if (item.redCount == null || item.redCount < 0) {
          item.redCount = 0
        }

        if (cri.whiteCount == null) {
          cri.whiteCount = 0
        }
        if (cri.greenCount == null) {
          cri.greenCount = 0
        }
        if (cri.blueCount == null) {
          cri.blueCount = 0
        }
        if (cri.orangeCount == null) {
          cri.orangeCount = 0
        }
        if (cri.redCount == null) {
          cri.redCount = 0
        }

        cri.whiteCount -= item.whiteCount
        cri.greenCount -= item.greenCount
        cri.blueCount -= item.blueCount
        cri.orangeCount -= item.orangeCount
        cri.redCount -= item.redCount
        // cri.totalCount -= item.totalCount

        // var isTestCase = isRandom != true && item.Flow != null && accountIndex < (this.accounts || []).length

        if (cri.whiteCount < 0) {
          cri.whiteCount = 0 // isTestCase ? item.whiteCount : 0
        }
        if (cri.greenCount < 0) {
          cri.greenCount = 0 // isTestCase ? item.greenCount : 0
        }
        if (cri.blueCount < 0) {
          cri.blueCount = 0 // isTestCase ? item.blueCount : 0
        }
        if (cri.orangeCount < 0) {
          cri.orangeCount = 0 // isTestCase ? item.orangeCount : 0
        }
        if (cri.redCount < 0) {
          cri.redCount = 0 // isTestCase ? item.redCount : 0
        }
        // if (cri.totalCount < 0) {
        //   cri.totalCount = 0
        // }

        cri.totalCount = cri.whiteCount + cri.greenCount + cri.blueCount + cri.orangeCount + cri.redCount
      },
      resetCount: function (item, isRandom, isSub, accountIndex) {
        if (item == null) {
          this.log('resetCount  randomItem == null >> return')
          return item
        }

        if (isRandom) {
          var cri = isSub ? this.currentRandomItem : this.currentRemoteItem
          if (cri != null && (cri != item || cri.id != item.id)) {
            this.resetParentCount(item, cri, isRandom, accountIndex)
          }

          cri = this.currentRandomItem
          if (isSub && cri != null && (cri != item || cri.id != item.id)) {
            this.resetParentCount(item, cri, isRandom, accountIndex)
          }
        }

        var accounts = this.accounts
        var num = accounts == null ? 0 : accounts.length
        if (accountIndex < num) { //  && accountIndex != this.currentAccountIndex) {
          var cs = this.getSummary(accountIndex)
          if (cs != null && (cs != item || cs.id != item.id)) {
            this.resetParentCount(item, cs, false, accountIndex)
          }
        }

        if (accountIndex < num) {
          var als = this.getAllSummary()
          // 不知道为啥总是不对
          // if (als != null && (als != item || als.id != item.id)) {
          //   this.resetParentCount(item, als, false, accountIndex)
          // }

          // 改用以下方式
          var whiteCount = 0
          var greenCount = 0
          var blueCount = 0
          var orangeCount = 0
          var redCount = 0
          // var totalCount = 0
          for (var i = -1; i < num; i++) {
            var cs = this.getSummary(i)
            if (cs == null) {
              continue
            }

            if (cs.whiteCount == null || cs.whiteCount < 0) {
              cs.whiteCount = 0
            }
            if (cs.greenCount == null || cs.greenCount < 0) {
              cs.greenCount = 0
            }
            if (cs.blueCount == null || cs.blueCount < 0) {
              cs.blueCount = 0
            }
            if (cs.orangeCount == null || cs.orangeCount < 0) {
              cs.orangeCount = 0
            }
            if (cs.redCount == null || cs.redCount < 0) {
              cs.redCount = 0
            }
            if (cs.totalCount == null || cs.totalCount < 0) {
              cs.totalCount = cs.whiteCount + cs.greenCount + cs.blueCount + cs.orangeCount + cs.redCount
            }

            whiteCount += cs.whiteCount
            greenCount += cs.greenCount
            blueCount += cs.blueCount
            orangeCount += cs.orangeCount
            redCount += cs.redCount
            // totalCount += cs.totalCount
          }

          als.whiteCount = whiteCount
          als.greenCount = greenCount
          als.blueCount = blueCount
          als.orangeCount = orangeCount
          als.redCount = redCount
          als.totalCount = whiteCount + greenCount + blueCount + orangeCount + redCount // totalCount
        }

        // var isTop = isRandom != true && item.Flow == null && item.Input == null && accountIndex < (this.accounts || []).length

        item.whiteCount = 0
        item.greenCount = 0
        item.blueCount = 0
        item.orangeCount = 0
        item.redCount = 0
        item.totalCount = 0
        return item
      },

      /**事件配置，动态替换键值对
       * @param show
       * @param callback
       */
      testRandomWithText: function (show, callback) {
        try {
          var count = this.testRandomCount || 0;
          this.isRandomSubListShow = count > 1;
          this.currentRandomItem = {
            Input: {
              toId: 0, // ((this.currentRandomItem || {}).Input || {}).id || 0,
              userId: (this.User || {}).id,
              count: count,
              name: this.randomTestTitle,
              config: vRandom.value
            },
            totalCount: count
          }

          this.testRandomSingle(show, false, this.isRandomSubListShow, this.currentRandomItem,
            this.isShowMethod() ? this.method : null, this.type, this.getUrl()
            , this.getRequest(vInput.value, {}), this.getHeader(vHeader.value), false, false, callback
          )
        }
        catch (e) {
          log(e)
          vSend.disabled = true

          this.view = 'error'
          this.error = {
            msg: e.message
          }

          this.isRandomShow = true
          vRandom.select()
        }
      },

      /**
       *  与 getRandomJSON 合并，返回一个
       *  {
       *    name: 'long 1, long 2', // 自动按 type0 value0, type1, value1 格式
       *    config: {}, //const config
       *    json: {} //const json
       *  }
       */
      /**参数注入，动态替换键值对
       * @param show
       * @param callback
       */
      parseRandom: function (json, head, config, randomId, generateJSON, generateConfig, generateName, callback, preScript, ctx) {
        var lines = config == null ? null : StringUtil.trim(config).split('\n')
        if (lines == null || lines.length <= 0) {
          // return null;
          callback('', '', json, head);
          return
        }
        const isUI = this.isRandomSubListShow

        json = json || {};
        head = head || {};

        baseUrl = this.getBaseUrl();

        var reqCount = lines.length; //有无效的行  lines.length;  //等待次数
        var respCount = 0;

        randomId = randomId || 0;
        var randomNameKeys = []
        var constConfigLines = [] //TODO 改为 [{ "rawPath": "User/id", "replacePath": "User/id@", "replaceValue": "RANDOM_INT(1, 10)", "isExpression": true }] ?

        // alert('< json = ' + JSON.stringify(json, null, '    '))

        for (let i = 0; i < reqCount; i ++) {
          const which = i;
          const lineItem = lines[i] || '';

          // remove comment   // 解决整体 trim 后第一行  // 被当成正常的 key 路径而不是注释
          const commentIndex = StringUtil.trim(lineItem).startsWith('//') ? 0 : lineItem.lastIndexOf(' //'); //  -1; // eval 本身支持注释 eval('1 // test') = 1 lineItem.indexOf(' //');
          const line = commentIndex < 0 ? lineItem : StringUtil.trim(lineItem.substring(0, commentIndex));

          if (line.length <= 0) {
            respCount ++;
            if (i >= lines.length - 1 && respCount >= reqCount) {
              var cn = randomNameKeys.join(', ')
              if (cn.length > 50) {
                cn = cn.substring(0, 30) + ' ..' + randomNameKeys.length + '.. ' + cn.substring(cn.length - 12)
              }
              callback(cn, constConfigLines.join('\n'), json, head);
            }
            continue;
          }

          const isHead = line.startsWith('- ');
          const line2 = isHead ? line.substring(2) : line;
          // path User/id  key id@
          const index = line2.indexOf(': '); //APIJSON Table:alias 前面不会有空格 //致后面就接 { 'a': 1} 报错 Unexpected token ':'   lastIndexOf(': '); // indexOf(': '); 可能会有 Comment:to
          const p_k = line2.substring(0, index);
          const bi = -1;  //没必要支持，用 before: undefined, after: .. 同样支持替换，反而这样导致不兼容包含空格的 key   p_k.indexOf(' ');
          const path = decodeURI(bi < 0 ? p_k : p_k.substring(0, bi)); // User/id

          const pathKeys = path.split('/')
          if (pathKeys == null || pathKeys.length <= 0) {
            throw new Error('参数注入 第 ' + (i + 1) + ' 行格式错误！\n字符 ' + path + ' 不符合 JSON 路径的格式 key0/key1/../targetKey !' +
                '\n每个随机变量配置都必须按照\n  key0/key1/../targetKey replaceKey: value  // 注释\n的格式！' +
                '\n注意冒号 ": " 左边 0 空格，右边 1 空格！其中 replaceKey 可省略。' +
                '\nkey: {} 中最外层常量对象 {} 必须用括号包裹为 ({})，也就是 key: ({}) 这种格式！' +
                '\nkey: 多行代码 必须用 function f() { var a = 1; return a; } f() 这种一行代码格式！');
          }

          const lastKeyInPath = pathKeys[pathKeys.length - 1]
          const customizeKey = bi > 0;
          const key = customizeKey ? p_k.substring(bi + 1) : lastKeyInPath;
          if (key == null || StringUtil.length(key, true) <= 0) {
            throw new Error('参数注入 第 ' + (i + 1) + ' 行格式错误！\n字符 ' + key + ' 不是合法的 JSON key!' +
                '\n每个随机变量配置都必须按照\n  key0/key1/../targetKey replaceKey: value  // 注释\n的格式！' +
                '\n注意冒号 ": " 左边 0 空格，右边 1 空格！其中 replaceKey 可省略。' +
                '\nkey: {} 中最外层常量对象 {} 必须用括号包裹为 ({})，也就是 key: ({}) 这种格式！' +
                '\nkey: 多行代码 必须用 function f() { var a = 1; return a; } f() 这种一行代码格式！');
          }

          // value RANDOM_DB
          const value = StringUtil.trim(line2.substring(index + ': '.length));

          var invoke = function (val, which, p_k, pathKeys, key, lastKeyInPath) {
            try {
              if (generateConfig) {
                var configVal;
                if (val instanceof Object) {
                  configVal = JSON.stringify(val);
                }
                else if (typeof val == 'string') {
                  configVal = '"' + StringUtil.trim(val) + '"';
                }
                else {
                  configVal = val;
                }
                constConfigLines[which] = (isHead ? '- ' : '') + p_k + ': ' + configVal;
              }

              if (generateName) {
                var s = val == undefined ? 'undefined' : (typeof val == 'string' && val != '' ? val : JSON.stringify(val)); // null 可以正常转为字符串
                if (val instanceof Array) {
                  valStr = val.length <= 1 ? s : '[' + val.length + ' .. ' + s.substring(1, s.length - 1) + ']';
                }
                else if (val instanceof Object) {
                  var kl = Object.keys(val).length;
                  valStr = kl <= 1 ? s : '{' + kl + ' .. ' + s.substring(1, s.length - 1) + '}';
                }
                else {
                  valStr = s;
                }

                if (valStr.length > 13) {
                  valStr = valStr.substring(0, 5) + '..';
                }
                randomNameKeys[which] = valStr;
              }

              if (generateJSON) {
                //先按照单行简单实现
                //替换 JSON 里的键值对 key: value
                var targetObj = isHead ? head : json;
                var parent = targetObj;
                var current = null;
                for (var j = 0; j < pathKeys.length - 1; j ++) {
                  current = parent[pathKeys[j]]
                  if (current == null) {
                    current = parent[pathKeys[j]] = {}
                  }
                  if (parent instanceof Object == false) {
                    throw new Error('参数注入 第 ' + (i + 1) + ' 行格式错误！路径 ' + path + ' 中' +
                        ' pathKeys[' + j + '] = ' + pathKeys[j] + ' 在实际请求 JSON 内对应的值不是对象 {} 或 数组 [] !');
                  }
                  parent = current;
                }

                if (current == null) {
                  current = targetObj;
                }
                // alert('< current = ' + JSON.stringify(current, null, '    '))

                // FIXME 还需要吗？之前的替换字段功能都废弃了，这个导致顺序变化
                if (key != lastKeyInPath || current.hasOwnProperty(key) == false) {
                  delete current[lastKeyInPath];
                }

                current[key] = val;
              }

            }
            catch (e) {
              throw new Error('第 ' + (which + 1) + ' 行随机配置 key: value 后的 value 不合法！ \nerr: ' + e.message)
            }

            respCount ++;
            if (respCount >= reqCount) {
              var cn = randomNameKeys.join(', ')
              if (cn.length > 50) {
                cn = cn.substring(0, 30) + ' ..' + randomNameKeys.length + '.. ' + cn.substring(cn.length - 12)
              }
              callback(cn, constConfigLines.join('\n'), json, head);
            }
          };


          const start = value.indexOf('(');
          const end = value.lastIndexOf(')');
          var _args = [];
          try { // JSON 中不能出现 undefined
            _args = parseJSON('[' + value.substring(start + 1, end) + ']');
          } catch (e) {
            console.log(e);
            _args = StringUtil.split(value.substring(start + 1, end), ', ');
          }
          const args = _args || [];

          var request4Db = function(tableName, which, p_k, pathKeys, key, lastKeyInPath, isRandom, isDesc, step, body) {
            // const tableName = JSONResponse.getTableName(pathKeys[pathKeys.length - 2]);
            vOutput.value = 'requesting value for ' + tableName + '/' + key + ' from database...';

            var min = StringUtil.trim(args[0]);
            var max = StringUtil.trim(args[1]);
            var table = StringUtil.trim(args[2]) || '';
            var column = StringUtil.trim(args[3]) || '';

            min = min == '' || min == 'null' || min == 'undefined' ? null : +min;
            max = max == '' || max == 'null' || max == 'undefined' ? null : +max;

            if ((table.startsWith('"') && table.endsWith('"')) || (table.startsWith("'") && table.endsWith("'"))) {
              table = table.substring(1, table.length - 1);
            }
            if ((column.startsWith('"') && column.endsWith('"')) || (column.startsWith("'") && column.endsWith("'"))) {
              column = column.substring(1, column.length - 1);
            }

            var sch_tbl = StringUtil.isEmpty(table, true) ? tableName : table;
            var ind = sch_tbl.indexOf('.');
            var schema = ind < 0 ? App.schema : sch_tbl.substring(0, ind);
            const finalTableName = StringUtil.firstCase(ind < 0 ? sch_tbl : sch_tbl.substring(ind + 1), true);
            const finalColumnName = StringUtil.isEmpty(column, true) ? lastKeyInPath : column;

            var tableReq = {
              '@column': isRandom ? finalColumnName : ('DISTINCT ' + finalColumnName),
              '@order': isRandom ? 'rand()' : (finalColumnName + (isDesc ? '-' : '+')),
              '@schema': StringUtil.isEmpty(schema, true) ? undefined : schema
            };
            tableReq[finalColumnName + '>='] = min;
            tableReq[finalColumnName + '<='] = max;
            try {
              body = parseJSON(body);
            } catch (e) {
              console.log(e);
            }

            var req = {};
            const listName = isRandom ? null : finalTableName + '-' + finalColumnName + '[]';
            const orderIndex = isRandom ? null : getOrderIndex(randomId, line, null);

            if (body != null && body[finalTableName] == null && (isRandom || body[listName] == null)) {
              tableReq = Object.assign(tableReq, body);
            }

            if (isRandom) {
              req[finalTableName] = tableReq;
            }
            else {
              // 从数据库获取时不考虑边界，不会在越界后自动循环
              var listReq = {
                count: 1, // count <= 100 ? count : 0,
                page: (step*orderIndex) % 100  //暂时先这样，APIJSON 应该改为 count*page <= 10000  //FIXME 上限 100 怎么破，lastKeyInPath 未必是 id
              };
              listReq[finalTableName] = tableReq;

              if (body != null && body[finalTableName] != null) {
                listReq = Object.assign(listReq, body);
              }
              req[listName] = listReq;
            }

            if (body != null && body[isRandom ? finalTableName : listName] != null) {
              req = Object.assign(req, body);
            }

            // reqCount ++;
            App.adminRequest('/get', req, {}, function (url, res, err) {
              // respCount ++;
              try {
                App.onResponse(url, res, err)
              } catch (e) {}

              var data = (res || {}).data || {}
              if (JSONResponse.isSuccess(data) != true) {
                respCount = -reqCount;
                vOutput.value = '参数注入 为第 ' + (which + 1) + ' 行\n  ' + p_k + '  \n获取数据库数据 异常：\n' + data.msg;
                alert(StringUtil.get(vOutput.value));
                return
                // throw new Error('参数注入 为\n  ' + tableName + '/' + key + '  \n获取数据库数据 异常：\n' + data.msg)
              }

              if (isRandom) {
                invoke((data[finalTableName] || {})[finalColumnName], which, p_k, pathKeys, key, lastKeyInPath);
              }
              else {
                var val = (data[listName] || [])[0];
                //越界，重新获取
                if (val == null && orderIndex > 0 && ORDER_MAP[randomId] != null && ORDER_MAP[randomId][line] != null) {
                  ORDER_MAP[randomId][line] = null;  //重置，避免还是在原来基础上叠加
                  request4Db(JSONResponse.getTableName(pathKeys[pathKeys.length - 2]), which, p_k, pathKeys, key, lastKeyInPath, false, isDesc, step, body);
                }
                else {
                  invoke(val, which, p_k, pathKeys, key, lastKeyInPath);
                }
              }

              // var list = data[listName] || [];
              //代码变化会导致缓存失效，而且不好判断，数据量大会导致页面很卡 ORDER_MAP[randomId][line].list = list;
              //
              // if (step == null) {
              //   invoke('randomIn(' + list.join() + ')');
              // }
              // else {
              //   invoke('orderIn(' + isDesc + ', ' + step*getOrderIndex(randomId, line, list.length) + list.join() + ')');
              // }

            })
          };



          //支持 1, "a" 这种原始值
          // if (start < 0 || end <= start) {  //(1) 表示原始值  start*end <= 0 || start >= end) {
          //   throw new Error('参数注入 第 ' + (i + 1) + ' 行格式错误！字符 ' + value + ' 不是合法的随机函数!');
          // }

          var toEval = value;
          if (start > 0 && end > start) {

            var funWithOrder = value.substring(0, start);
            var splitIndex = funWithOrder.indexOf('+');

            var isDesc = false;
            if (splitIndex < 0) {  // -(1+2) 这种是表达式，不能作为函数   splitIndex <= 0) {
              splitIndex = funWithOrder.indexOf('-');
              isDesc = splitIndex > 0;
            }

            var fun = splitIndex < 0 ? funWithOrder : funWithOrder.substring(0, splitIndex);

            if (fun.startsWith('ORDER_') && /^[_A-Z]+$/g.test(fun)) { // [ORDER_DB, ORDER_IN, ORDER_INT].indexOf(fun) >= 0) {  //顺序函数
              var stepStr = splitIndex < 0 ? null : funWithOrder.substring(splitIndex + 1, funWithOrder.length);
              var step = stepStr == null || stepStr.length <= 0 ? 1 : +stepStr; //都会自动忽略空格 Number(stepStr); //Number.parseInt(stepStr); //+stepStr;

              if (Number.isSafeInteger(step) != true || step <= 0
                  || (StringUtil.isEmpty(stepStr, false) != true && StringUtil.isNumber(stepStr) != true)
              ) {
                throw new Error('参数注入 第 ' + (i + 1) + ' 行格式错误！路径 ' + path + ' 中字符 ' + stepStr + ' 不符合跨步 step 格式！'
                    + '\n顺序整数 和 顺序取值 可以通过以下格式配置 升降序 和 跨步：'
                    + '\n  ORDER_DB+step(arg0, arg1...)\n  ORDER_DB-step(arg0, arg1...)'
                    + '\n  ORDER_INT+step(arg0, arg1...)\n  ORDER_INT-step(arg0, arg1...)'
                    + '\n  ORDER_IN+step(start, end)\n  ORDER_IN-step(start, end)'
                    + '\n其中：\n  + 为升序，后面没有 step 时可省略；\n  - 为降序，不可省略；' + '\n  step 为跨步值，类型为 正整数，默认为 1，可省略。'
                    + '\n+，-，step 前后都不能有空格等其它字符！');
              }

              if (fun == ORDER_DB) {
                request4Db(JSONResponse.getTableName(pathKeys[pathKeys.length - 2]), which, p_k, pathKeys, key, lastKeyInPath, false, isDesc, step, args[4]); //request4Db(key + (isDesc ? '-' : '+'), step);
                continue;
              }

              toEval = (fun == ORDER_IN ? 'orderIn' : (fun == ORDER_INT ? 'orderInt' : (fun == ORDER_BAD_BOOL ? 'orderBadBool' : (fun == ORDER_BAD_NUM
                      ? 'orderBadNum' : (fun == ORDER_BAD_STR ? 'orderBadStr' : (fun == ORDER_BAD_ARR ? 'orderBadArr' : (fun == ORDER_BAD_OBJ ? 'orderBadObj' : 'orderBad')))))))
                  + '(' + (fun == ORDER_BAD ? 'BADS, ' : '') + isDesc + ', ' + getOrderIndex(
                      randomId, line
                      , (fun == ORDER_INT || args == null ? 0 : args.length)
                      + (fun == ORDER_BAD_BOOL ? BAD_BOOLS.length : (fun == ORDER_BAD_NUM ? BAD_NUMS.length : (fun == ORDER_BAD_STR
                          ? BAD_STRS.length : (fun == ORDER_BAD_ARR ? BAD_ARRS.length : (fun == ORDER_BAD_OBJ ? BAD_OBJS.length : (fun == ORDER_BAD ? BADS.length : 0))))))
                      , step
                  ) + ', ' + value.substring(start + 1);
            }
            else {  //随机函数
              var as = StringUtil.split(value.substring(start + 1, end), ', ') || []
              var as0 = as[0] // key path
              var as1 = as[1] // default value
              var as2 = as[2] // GET /users
              var as3 = as[3] // account@host/index
              var isChain = StringUtil.isNotEmpty(as2)
              var chainArr = isChain ? '(((ctx || {}).ctx || {}).map || {})[' + as2 + ']' : null; // [GET /users] = { account@host: [], account: [], host: [] }
              if (isChain) {
                as2 = parseJSON(as2, as2, true)
                as3 = parseJSON(as3, as3, true)
                as.splice(2, 2)
              }
              var as3s = isChain ? StringUtil.trim(as3) : ''
              var accountHostIndexPath = isChain ? (StringUtil.isNumber(as3) ? '/' + as3 : (StringUtil.isEmpty(as3) ? '/' : as3s + (as3s.indexOf('/') < 0 ? '/' : ''))) : ''

              if (fun == NEXT_ARG || (isUI && fun == CUR_ARG)) {
                var source = isChain ? 'get4Path(' + chainArr + ', "' + accountHostIndexPath + '/arg")' : '((ctx || {}).next || {}).arg'
                toEval = 'get4Path(' + source + ', ' + (value == 'NEXT_ARG()' ? JSON.stringify(path) : '') + (isChain ? as.join(', ') + value.substring(end) : value.substring(start + 1));
              }
              else if (fun == PRE_DATA || (isUI && fun == CUR_DATA)) {
                var source = isChain ? 'get4Path(' + chainArr + ', "' + accountHostIndexPath + '/data", undefined, null, true)' : '((ctx || {}).pre || {}).data'
                toEval = 'get4Path(' + source + ', ' + (value == 'PRE_DATA()' ? JSON.stringify(path) : '') + (isChain ? as.join(', ') + value.substring(end) : value.substring(start + 1));
              }
              else if (fun == PRE_REQ) {
                var source = isChain ? 'get4Path(' + chainArr + ', "' + accountHostIndexPath + '/req", undefined, null, true)' : '((ctx || {}).pre || {}).req'
                toEval = 'get4Path(' + source + ', ' + (value == 'PRE_REQ()' ? JSON.stringify(path) : '') + (isChain ? as.join(', ') + value.substring(end) : value.substring(start + 1));
              }
              else if (fun == PRE_ARG) {
                var source = isChain ? 'get4Path(' + chainArr + ', "' + accountHostIndexPath + '/arg", undefined, null, true)' : '((ctx || {}).pre || {}).arg'
                toEval = 'get4Path(' + source + ', ' + (value == 'PRE_ARG()' ? JSON.stringify(path) : '') + (isChain ? as.join(', ') + value.substring(end) : value.substring(start + 1));
              }
              else if (fun == PRE_RES) {
                var source = isChain ? 'get4Path(' + chainArr + ', "' + accountHostIndexPath + '/res", undefined, null, true)' : '((ctx || {}).pre || {}).res'
                toEval = 'get4Path(' + source + ', ' + (value == 'PRE_RES()' ? JSON.stringify(path) : '') + (isChain ? as.join(', ') + value.substring(end) : value.substring(start + 1));
              }
              else if (fun == CUR_REQ) {
                toEval = 'get4Path(((ctx || {}).cur || {}).req, ' + (value == 'CUR_REQ()' ? JSON.stringify(path) : '') + value.substring(start + 1);
              }
              else if (fun == CUR_ARG) {
                toEval = 'get4Path(((ctx || {}).cur || {}).arg, ' + (value == 'CUR_ARG()' ? JSON.stringify(path) : '') + value.substring(start + 1);
              }
              else if (fun == CUR_RES) {
                toEval = 'get4Path(((ctx || {}).cur || {}).res, ' + (value == 'CUR_RES()' ? JSON.stringify(path) : '') + value.substring(start + 1);
              }
              else if (fun == CUR_DATA) {
                toEval = 'get4Path(((ctx || {}).cur || {}).data, ' + (value == 'CUR_DATA()' ? JSON.stringify(path) : '') + value.substring(start + 1);
              }
              else if (fun == CTX_GET) {
                toEval = 'get4Path((ctx || {}).ctx, ' + (value == 'CTX_GET()' ? JSON.stringify(path) : '') + value.substring(start + 1);
              }
              else if (fun == CTX_PUT) {
                as1 = parseJSON(as1, as1, true)

                if (StringUtil.isEmpty(as1, true) || as1 == 'PRE_DATA') {
                  as[1] = '((ctx || {}).pre || {}).data'
                }
                else if (as1 == 'PRE_REQ') {
                  as[1] = '((ctx || {}).pre || {}).req'
                }
                else if (as1 == 'PRE_ARG') {
                  as[1] = '((ctx || {}).pre || {}).arg'
                }
                else if (as1 == 'NEXT_ARG') {
                  as[1] = '((ctx || {}).next || {}).arg'
                }
                else if (as1 == 'PRE_RES') {
                  as[1] = '((ctx || {}).pre || {}).res'
                }
                else if (as1 == 'CUR_DATA') {
                  as[1] = '((ctx || {}).cur || {}).data'
                }
                else if (as1 == 'CUR_REQ') {
                  as[1] = '((ctx || {}).cur || {}).req'
                }
                else if (as1 == 'CUR_ARG') {
                  as[1] = '((ctx || {}).cur || {}).arg'
                }
                else if (as1 == 'CUR_RES') {
                  as[1] = '((ctx || {}).cur || {}).res'
                }

                if (as.length >= 1) {
                  as[0] = 'get4Path(' + as[1] + ', ' + StringUtil.trim(as[0]) + ')'
                  as.splice(1, 1)
                }
                toEval = 'put4Path((ctx || {}).ctx, ' + JSON.stringify(path) + ', ' + as.join(', ') + value.substring(end);
              }
              else {
                fun = funWithOrder;  //还原，其它函数不支持 升降序和跨步！

                if (fun == RANDOM_DB) {
                  request4Db(JSONResponse.getTableName(pathKeys[pathKeys.length - 2]), which, p_k, pathKeys, key, lastKeyInPath, true, null, null, args[4]); //'random()');
                  continue;
                }

                if (fun == RANDOM_IN) {
                  toEval = 'randomIn' + value.substring(start);
                }
                else if (fun == RANDOM_INT) {
                  toEval = 'randomInt' + value.substring(start);
                }
                else if (fun == RANDOM_NUM) {
                  toEval = 'randomNum' + value.substring(start);
                }
                else if (fun == RANDOM_STR) {
                  toEval = 'randomStr' + value.substring(start);
                }
                else if (fun == RANDOM_BAD) {
                  toEval = 'randomBad' + value.substring(start);
                }
                else if (fun == RANDOM_BAD_BOOL) {
                  toEval = 'randomBadBool' + value.substring(start);
                }
                else if (fun == RANDOM_BAD_NUM) {
                  toEval = 'randomBadNum' + value.substring(start);
                }
                else if (fun == RANDOM_BAD_STR) {
                  toEval = 'randomBadStr' + value.substring(start);
                }
                else if (fun == RANDOM_BAD_ARR) {
                  toEval = 'randomBadArr' + value.substring(start);
                }
                else if (fun == RANDOM_BAD_OBJ) {
                  toEval = 'randomBadObj' + value.substring(start);
                }
              }

            }

          }

          var isInject = true;
          var isPre = false; // 避免执行副作用代码 true;
          var isTest = false;
          var method = null;
          var type = null;
          var url = null;
          var req = json;
          var header = head;
          var res = {};
          var data = res.data;
          var err = null;
          try {
            var ret = eval(StringUtil.trim(preScript) + '\n;\n(' + toEval + ')')
            invoke(ret, which, p_k, pathKeys, key, lastKeyInPath);
          } catch (e) {
            throw new Error(e.message + '\n; 第 ' + i + ' 行：' + line)
          }

          // alert('> current = ' + JSON.stringify(current, null, '    '))
        }

      },

      moveSplit: function (rate) {
        // 自动往右移动，避免断言结果遮挡太多接口名称、URL
        rate = rate || 0.4;

        var split_obj = IS_BROWSER ? $('.splitx') : null
        var split_obj_left = split_obj == null ? 0 : parseInt(split_obj.css('left'))
        var width = split_obj_left <= 0 ? 0 : (window.innerWidth || 1280)
        if (width > 0) {
          var w = rate*width;
          // 构造事件比较麻烦，即便用 JQuery 也是
          // split_obj[0].dispatchEvent(new TouchEvent('', new class implements TouchEventInit{
          //   clientX: w
          // }()))

          var left_ele = $('.side-left')
          var right_ele = $('.side-right')

          // var left_width = left_ele.width()
          // var right_width = right_ele.width()
          //
          // var right_left = parseInt(right_ele.css('left'))

          split_obj.css('left', w)
          left_ele.width(w);
          right_ele.width(width - w).css('left', w);
          // right_ele.width(right_width - left_ele.width() + left_width).css('left', right_left + left_ele.width() - left_width);
        }
      },

      onClickSend: function () {
        this.isRandomTest = false
        this.send(false)
      },
      onClickTest: function (callback) {
        this.isRandomTest = false
        this.isStatisticsEnabled = true
        this.reportId = new Date().getTime()
        this.caseShowType = 1
        this.operate = OPERATE_TYPE_REPLAY

        this.isFullScreen = false
        this.moveSplit(0.6)

        var isCross = this.isCrossEnabled
        var accountIndex = isCross ? -1 : this.currentAccountIndex

        var accounts = this.accounts
        var num = accounts == null ? 0 : accounts.length

        var cases = this.isChainShow ? this.chainGroups : this.remotes
        var total = cases == null ? 0 : cases.length

        var als = this.getAllSummary()
        als = this.resetCount(als, false, false, num)
        als.totalCount = isCross ? (num + 1)*total : total

        if (isCross) {
          for (var i = -1; i < num; i++) {
            var cs = this.getSummary(i)
            cs = this.resetCount(cs, false, false, i)
            cs.totalCount = total
          }
        } else {
          var cs = this.getSummary(accountIndex)
          cs = this.resetCount(cs, false, false, accountIndex)
          cs.totalCount = total
        }

        this.coverage = {}
        this.requestPost(false, '/coverage/start', {}, {}, function (url, res, err) {
          try {
            App.onResponse(url, res, err)
            if (DEBUG) {
              App.log('test  App.request >> res.data = ' + JSON.stringify(res.data, null, '  '))
            }
          } catch (e) {
            App.log('test  App.request >> } catch (e) {\n' + e.message)
          }
          App.test(false, accountIndex, isCross, callback)
        })
      },
      /**回归测试
       * 原理：
       1.遍历所有上传过的测试用例（URL+请求JSON）
       2.逐个发送请求
       3.对比同一用例的先后两次请求结果，如果不一致，就在列表中标记对应的用例(× 蓝黄红色下载(点击下载两个文件) √)。
       4.如果这次请求结果正确，就把请求结果保存到和公司开发环境服务器的APIJSON Server，并取消标记

       compare: 新的请求与上次请求的对比结果
       0-相同，无颜色；
       1-对象新增字段或数组新增值，绿色；
       2-值改变，蓝色；
       3-对象缺少字段/整数变小数，黄色；
       4-code/值类型 改变，红色；
       */
      test: function (isRandom, accountIndex, isCross, callback) {
        var accounts = this.accounts || []
        // alert('test  accountIndex = ' + accountIndex)
        if (accountIndex == null) {
          accountIndex = -1 //isCross ? -1 : 0
        }

        if (isCross) {
          var isCrossDone = accountIndex >= accounts.length
          this.crossProcess = isCrossDone ? '交叉账号:已开启' : ('交叉账号: ' + (accountIndex + 1) + '/' + accounts.length)
          if (isCrossDone) {
            this.testProcess = (this.isMLEnabled ? '机器学习:已开启' : '机器学习:已关闭')
            this.testRandomProcess = ''
            if (accountIndex == accounts.length) {
              this.currentAccountIndex = accounts.length - 1  // -1 导致最后右侧显示空对象

              if (callback) {
                callback('已完成账号交叉测试: 退出登录状态 和 每个账号登录状态')
              } else {
                alert('已完成账号交叉测试: 退出登录状态 和 每个账号登录状态')
              }

              if (callback != this.autoTestCallback && typeof this.autoTestCallback == 'function') {
                this.autoTestCallback('已完成账号交叉测试: 退出登录状态 和 每个账号登录状态')
              }
            }
            return
          }

          if (callback != this.autoTestCallback && typeof this.autoTestCallback == 'function') {
            this.autoTestCallback('正在账号交叉测试 ')
          }
        }

        var baseUrl = StringUtil.trim(this.getBaseUrl())
        if (baseUrl == '') {
          if (callback) {
            callback('请先输入有效的URL！')
          } else {
            alert('请先输入有效的URL！')
          }
          return
        }
        //开放测试
        // if (baseUrl.indexOf('/apijson.cn') >= 0 || baseUrl.indexOf('/39.108.143.172') >= 0) {
        //   alert('请把URL改成你自己的！\n例如 http://localhost:8080')
        //   return
        // }
        // if (baseUrl.indexOf('/apijson.org') >= 0) {
        //   alert('请把URL改成 http://apijson.cn:8080 或 你自己的！\n例如 http://localhost:8080')
        //   return
        // }

        const list = (isRandom ? this.randoms : (this.isChainShow ? (
            this.isChainGroupShow() ? this.chainGroups : [this.chainGroups[this.currentChainGroupIndex]]
          ) : this.remotes)
        ) || []

        var allCount = list.length
        App.doneCount = 0
        App.deepAllCount = 0
        App.randomDoneCount = 0
        if (isRandom != true) {
          App.allCount = allCount
        }

        // var cs = this.getSummary(accountIndex)
        // if (isCross && cs.totalCount <= 0) {
        //   cs = this.resetCount(cs)
        //   cs.totalCount = allCount
        // }
        //
        // var als = this.getAllSummary()
        // if (als.totalCount <= 0) {
        //   als = this.resetCount(als)
        //   als.totalCount = allCount
        // }

        if (allCount <= 0) {
          if (callback) {
            callback('请先获取测试用例文档\n点击[查看用例列表]图标按钮')
          } else {
            alert('请先获取测试用例文档\n点击[查看用例列表]图标按钮')
          }
          return
        }

        if (this.isChainShow) {
            for (var i = 0; i < list.length; i++) {
                var item = list[i]
                var stepList = item['[]'] || []
                var stepCount = stepList == null ? 0 : stepList.length
                allCount += (stepCount - 1)
            }
        }

        if (isCross) {
          var account = accounts[this.currentAccountIndex]
          if (accountIndex < 0 && account != null) {  //退出登录已登录的账号
            account.isLoggedIn = true
          }
          var index = accountIndex < 0 ? this.currentAccountIndex : accountIndex
          this.onClickAccount(index, accounts[index], function (isLoggedIn, index, err) {
            // if (index >= 0 && isLoggedIn == false) {
            //   alert('第 ' + index + ' 个账号登录失败！' + (err == null ? '' : err.message))
            //   App.test(isRandom, accountIndex + 1)
            //   return
            // }
            App.showTestCase(true, false)
            App.startTest(list, allCount, isRandom, accountIndex, isCross, callback)
          })
        }
        else {
          this.startTest(list, allCount, isRandom, accountIndex, isCross, callback)
        }
      },

      startTestChain: function (list, allCount, index, item, prev, ctx, isRandom, accountIndex, isCross, callback) {
         if (list == null || index == null || index >= list.length) {
           return
         }

         var cur = item = item || {}
         cur.index = index
//         item.pre = pre // list[index - 1]

         var doc = item.Flow || {}
         var method = cur.method = doc.method
         var type = cur.type = doc.type
         var url = cur.url = doc.url
         var json = cur.arg = this.getRequest(doc.request, {})
         var req = cur.req = {
           method: method,
           url: url,
           header: doc.header,
           data: json
         }
         var header = cur.header = doc.header

         var accountInfo = this.getCurrentAccount() || {}
         const account = StringUtil.trim(accountInfo.account || accountInfo.phone || accountInfo.email)
//
//         this.parseRandom(json, rawConfig, random.id, true, false, function (randomName, constConfig, constJson) {
             this.startTestSingle(list, allCount, index, item, isRandom, accountIndex, isCross, callback
               , function(res, allCount, list, index, response, cmp, isRandom, accountIndex, justRecoverTest, isCross) {

               res = res || {}
               var config = res.config || {}
               var p = config.data || config.params
               try {
                 cur.arg = App.getRequest(p, {})
               } catch (e) {
                 if (typeof p != 'string' || p.indexOf('=') <= 0) {
                   throw e
                 }
                 cur.arg = getRequestFromURL('?' + p, true)
               }
               cur.req = {
                 method: method,
                 url: config.url,
                 header: config.header,
                 arg: cur.arg
               }
               cur.status = res.status
               cur.statusText = res.statusText
               cur.res = res
               cur.data = res.data

               var info = {
                 type: type,
                 req: req,
                 arg: cur.arg,
                 res: res,
                 data: res.data
               }
               var map = ctx.map = ctx.map || {}
               var m = map[method + ' ' + url] = map[method + ' ' + url] || {}

               const baseUrl = StringUtil.trim(App.getBaseUrl(config.url))
               const accountKey = account + '@' + baseUrl
               var arr = m[accountKey] = m[accountKey] || []
               arr.push(info)

               if (StringUtil.isNotEmpty(account)) {
                 var arr2 = m[account] = m[account] || []
                 arr2.push(info)
               }

               if (StringUtil.isNotEmpty(baseUrl)) {
                 var arr2 = m[baseUrl] = m[baseUrl] || []
                 arr2.push(info)
               }

               App.startTestChain(list, allCount, index + 1, list[index + 1], item, ctx, isRandom, accountIndex, isCross, callback)
             }, ctx)
             return true
//         })
      },

      isFullAssert: true,
      toTestDocIndexes: [],

      startTest: function (list, allCount, isRandom, accountIndex, isCross, callback) {
        this.testProcess = '正在测试: ' + 0 + '/' + allCount
        this.toTestDocIndexes = []

        if (callback != this.autoTestCallback && typeof this.autoTestCallback == 'function') {
          this.autoTestCallback(this.testProcess)
        }

        var isChainShow = this.isChainShow
        baseUrl = StringUtil.trim(this.getBaseUrl())

        var toTestDocIndexes = this.toTestDocIndexes = this.toTestDocIndexes || []

        for (var i = 0; i < list.length; i++) {
            const item = list[i]
            if (isChainShow) {
//               this.showTestCase(true, false, function(url, res, err) {
                  var stepList = item['[]'] // res.data['[]']
                  var stepCount = stepList == null ? 0 : stepList.length
                  this.startTestChain(stepList, stepCount, 0, stepCount <= 0 ? null : stepList[0], null, { Chain: item }, isRandom, accountIndex, isCross, callback)
//               }, item)
               continue
            }

            toTestDocIndexes.push(i)
            this.startTestSingle(list, allCount, i, item, isRandom, accountIndex, isCross, callback)
        }

      },

      startTestSingle: function (list, allCount, index, item, isRandom, accountIndex, isCross, callback, singleCallback, ctx) {
          this.isFullAssert = false

          App.deepDoneCount = 0;
          this.startRandomTest4Doc(list, this.toTestDocIndexes, 0, list.length, accountIndex, isCross)
      },

      compareResponse: function (res, allCount, list, index, item, response, isRandom, accountIndex, justRecoverTest, err, ignoreTrend, isCross, callback, singleCallback) {
        var it = item || {} //请求异步
        var cri = this.currentRemoteItem || {} //请求异步
        var d = (isRandom ? cri.Flow : it.Flow) || {} //请求异步
        var r = it.Input || {} // isRandom ? it.Input : null //请求异步
        var tr = it.TestRecord || {} //请求异步

        var bdt = tr.duration || 0
        it.durationBeforeShowStr = bdt <= 0 ? '' : (bdt < 1000 ? bdt + 'ms' : (bdt < 1000*60 ? (bdt/1000).toFixed(1) + 's' : (bdt <= 1000*60*60 ? (bdt/1000/60).toFixed(1) + 'm' : '>1h')))
        try {
          var durationInfo = response == null ? null : response['time:start|duration|end|parse|sql|model']
          it.durationInfo = durationInfo
          if (durationInfo == null) {
            throw new Error("response['time:start|duration|end|parse|sql|model'] is null!");
          }

          var di = durationInfo.substring(durationInfo.indexOf('|') + 1)
          it.duration = di.substring(0, di.indexOf('|') || di.length) || 0
          var dt = + it.duration
          it.duration = dt
          it.durationShowStr = dt <= 0 ? '' : (dt < 1000 ? dt + 'ms' : (dt < 1000*60 ? (dt/1000).toFixed(1) + 's' : (dt <= 1000*60*60 ? (dt/1000/60).toFixed(1) + 'm' : '>1h')))
          var min = tr.minDuration == null || tr.minDuration <= 0 ? 20 : tr.minDuration
          var max = tr.maxDuration == null || tr.maxDuration <= 0 ? 200 : tr.maxDuration
          it.durationColor = dt < min ? 'green' : (dt > 2*max ? 'red' : (dt > max + min ? 'orange' : (dt > max ? 'blue' : 'black')))
          it.durationHint = dt < min ? '很快：比以往 [' + min + 'ms, ' + max + 'ms] 最快还更快' : (dt > 2*max ? '非常慢：比以往 [' + min + 'ms, ' + max + 'ms] 最慢的两倍还更慢'
            : (dt > max + min ? '比较慢：比以往 [' + min + 'ms, ' + max + 'ms] 最快与最慢之和(平均值两倍)还更慢'
              : (dt > max ? '有点慢：比以往 [' + min + 'ms, ' + max + 'ms] 最慢还更慢' : '正常：在以往 [' + min + 'ms, ' + max + 'ms] 最快和最慢之间')))
        }
        catch (e) {
          // log(e)
          it.durationShowStr = it.durationShowStr || it.duration
          it.durationHint = it.durationHint || '最外层缺少字段 "time:start|duration|end|parse|sql|model"，无法对比耗时'
        }

        if (err != null) {
          var status = res == null ? null : res.status
          var rsp = (err.response || {}).data || {}
          var message = err.message
          var msg = StringUtil.trim(StringUtil.trim(rsp.code) + ' ' + StringUtil.trim(rsp.message || rsp.reason)) || err.message || '请求出错！'
          tr.compare = {
            code: JSONResponse.COMPARE_ERROR, //请求出错
            msg: msg,
            path: (status != null && status != 200 ? status + ' ' : '') + (StringUtil.isEmpty(message) || msg.indexOf(message) >= 0 ? '' : message)
          }
        }
        else {
          var isML = this.isMLEnabled
          var standardKey = isML ? 'standard' : 'response'
          var stdd = tr[standardKey]
          if (isRandom) {
            stdd = stdd || ((this.currentRemoteItem || {}).TestRecord || {})[standardKey]
          }

          var standard = typeof stdd != 'string' ? stdd : (StringUtil.isEmpty(stdd, true) ? null : parseJSON(stdd))
          tr.compare = JSONResponse.compareResponse(res, standard, this.removeDebugInfo(response) || {}, '', isML, null, null, ignoreTrend) || {}
          tr.compare.duration = it.durationHint
        }

        this.onTestResponse(res, allCount, list, index, it, d, r, tr, response, tr.compare || {}, isRandom, accountIndex, justRecoverTest, isCross, callback, singleCallback);
      },

      onTestResponse: function(res, allCount, list, index, it, d, r, tr, response, cmp, isRandom, accountIndex, justRecoverTest, isCross, callback, singleCallback) {
        this.isFullAssert = false
        tr = tr || {}
        cmp = cmp || {}
        tr.compare = cmp
        var status = res == null ? null : res.status

        var isSame = false
        it = it || {}
        var p = cmp.path
        it.compareType = cmp.code;
        it.compareMessage = (StringUtil.isEmpty(p, true) ? '' : p + '  ') + (cmp.msg || '查看结果')
        switch (it.compareType) {
          case JSONResponse.COMPARE_ERROR:
            it.compareColor = 'red'
            it.hintMessage = (status != null && status != 200 ? status + ' ' : '') + '请求出错！'
            break;
          case JSONResponse.COMPARE_NO_STANDARD:
            it.compareColor = 'green'
            it.hintMessage = '确认正确后点击[对的，纠正]'
            break;
          case JSONResponse.COMPARE_KEY_MORE:
          case JSONResponse.COMPARE_VALUE_MORE:
          case JSONResponse.COMPARE_EQUAL_EXCEPTION:
            it.compareColor = 'green'
            it.hintMessage = '新增字段/新增值 等'
            break;
          case JSONResponse.COMPARE_LENGTH_CHANGE:
          case JSONResponse.COMPARE_VALUE_CHANGE:
            it.compareColor = 'blue'
            it.hintMessage = '值改变 等'
            break;
          case JSONResponse.COMPARE_VALUE_EMPTY:
          case JSONResponse.COMPARE_KEY_LESS:
            it.compareColor = 'orange'
            it.hintMessage = '缺少字段/整数变小数 等'
            break;
          case JSONResponse.COMPARE_FORMAT_CHANGE:
          case JSONResponse.COMPARE_NUMBER_TYPE_CHANGE:
          case JSONResponse.COMPARE_TYPE_CHANGE:
          case JSONResponse.COMPARE_CODE_CHANGE:
          case JSONResponse.COMPARE_THROW_CHANGE:
            var code = response == null ? null : response[JSONResponse.KEY_CODE]
            it.compareColor = 'red'
            it.hintMessage = (code != null && code != JSONResponse.CODE_SUCCESS
             ? code + ' ' : (status != null && status != 200 ? status + ' ' : '')) + '状态码/异常/值类型 改变等'
            break;
          default:
            it.compareColor = 'white'
            it.hintMessage = '结果正确'
            isSame = tr.reportId != this.reportId
            break;
        }

        if (isRandom) {
          r = r || {}
          it.Input = r

          if (tr.id != null) {
            var sameIds = this.sameIds || []
            var ind = sameIds.indexOf(tr.id)
            if (isSame) {
              if (ind < 0) {
                sameIds.push(tr.id)
              }
            } else {
              if (ind >= 0) {
                sameIds.splice(ind, 1)
              }
            }
            this.sameIds = sameIds;
          }

          this.updateToRandomSummary(it, 1, accountIndex)
        }
        else {
          d = d || {}
          it.Flow = d

          this.updateToSummary(it, 1, accountIndex)
        }
        it.TestRecord = tr

        Vue.set(list, index, it)

        // var pic = ((response || {}).TestRecord || {}).screenshot
        // if (StringUtil.isEmpty(pic) != true) {
        //   vAfter.src = '/download?filePath=' + encodeURI(pic)
        // }

        if (justRecoverTest) {
          // callback(isRandom, allCount)
          return
        }

        if (isRandom) {
          App.randomDoneCount ++
        } else {
          App.doneCount ++
        }

        var doneCount = isRandom ? App.randomDoneCount : App.doneCount
        var isDone = doneCount >= allCount
        if (isRandom) {
          this.testRandomProcess = isDone ? '' : ('正在测试: ' + doneCount + '/' + allCount)
        } else {
          this.testProcess = isDone ? (this.isMLEnabled ? '机器学习:已开启' : '机器学习:已关闭') : '正在测试: ' + doneCount + '/' + allCount
        }

        if (isDone != true && callback != this.autoTestCallback && typeof this.autoTestCallback == 'function') {
          this.autoTestCallback('正在测试')
        }

        this.log('doneCount = ' + doneCount + '; d.name = ' + (isRandom ? r.name : d.name) + '; it.compareType = ' + it.compareType)

        var flowId = isRandom ? r.flowId : d.id
        if (this.tests == null) {
          this.tests = {}
        }

        var accountIndexStr = String(accountIndex)
        if (this.tests[accountIndexStr] == null) {
          this.tests[accountIndexStr] = {}
        }

        var tests = this.tests[accountIndexStr] || {}
        var t = tests[flowId]
        if (t == null) {
          t = tests[flowId] = {}
        }
        t[isRandom ? (r.id > 0 ? r.id : (r.toId + '' + r.id)) : 0] = response

        if (isRandom != true && it.compareColor != 'red') {
          if (this.toTestDocIndexes == null) {
            this.toTestDocIndexes = []
          }
          this.toTestDocIndexes.push(index);
        }

        this.tests[accountIndexStr] = tests
        if (DEBUG) {
          this.log('tests = ' + JSON.stringify(tests, null, '    '))
        }

        if (isDone && isRandom) {
          App.currentRandomIndex = -1
          App.isRandomShow = true
          App.isRandomListShow = true
          App.handleTest(false, 0, list[0], null, isRandom)  // , false, isCross)
        }

        // this.showTestCase(true)

        if (singleCallback != null && singleCallback(res, allCount, list, index, response, cmp, isRandom, accountIndex, justRecoverTest, isCross)) {
           return
        }

        if (doneCount >= allCount) {  // 导致不继续测试  App.doneCount == allCount) {
          if (callback != null && callback(isRandom, allCount)) {
            return
          }

          // alert('onTestResponse  accountIndex = ' + accountIndex)

          const deepAllCount = this.toTestDocIndexes == null ? 0 : this.toTestDocIndexes.length
          App.deepAllCount = deepAllCount
          if (isRandom != true && deepAllCount > 0 && ! this.isChainShow) { // 自动给非 红色 报错的接口跑事件配置
            App.deepDoneCount = 0;
            this.startRandomTest4Doc(list, this.toTestDocIndexes, 0, deepAllCount, accountIndex, isCross)
          } else if (isCross && doneCount == allCount && accountIndex <= this.accounts.length) {
            this.test(false, accountIndex + 1, isCross)
          }
        }
      },

      startRandomTest4Doc: function (list, indexes, position, deepAllCount, accountIndex, isCross) {
        const accInd = accountIndex
        var callback = function (isRandom, allCount, msg) {
          log("startRandomTest4Doc  callback isRandom = " + isRandom + "; allCount = " + allCount + "; msg = " + msg)
          if (App.randomDoneCount < App.randomAllCount) {
            return true
          }

          App.randomDoneCount = App.randomAllCount

          App.deepDoneCount ++
          const deepDoneCount = App.deepDoneCount
          const autoTestCallback = App.autoTestCallback

          App.testProcess = deepDoneCount < deepAllCount ? ('正在深度测试: ' + deepDoneCount + '/' + deepAllCount) : (App.isMLEnabled ? '机器学习:已开启' : '机器学习:已关闭')
          App.testRandomProcess = App.randomDoneCount >= App.randomAllCount ? '' : ('正在测试: ' + App.randomDoneCount + '/' + App.randomAllCount)

          setTimeout(function () {
            App.isTestCaseShow = true

            if (typeof autoTestCallback == 'function') {
              autoTestCallback('正在深度测试')
            }

            if (deepDoneCount < deepAllCount) {
              setTimeout(function () {
                App.startRandomTest4Doc(list, indexes, position + 1, deepAllCount, accInd, isCross)
              }, IS_NODE ? 200 : 1000)
            } else {
              App.testRandomProcess = ''

              if (isCross) {
                if (deepDoneCount == deepAllCount) {
                  App.test(false, accInd + 1, isCross)
                }
              } else {
                if (deepDoneCount == deepAllCount) {
                  alert('已完成回归测试')
                  if (typeof autoTestCallback == 'function') {
                    autoTestCallback('已完成回归测试')
                  }

                  App.requestPost(false, App.getBaseUrl() + '/coverage/report', {}, {}, function (url, res, err) {
                    try {
                      App.onResponse(url, res, err)
                      if (DEBUG) {
                        App.log('test  App.request >> res.data = ' + JSON.stringify(res.data, null, '  '))
                      }
                    } catch (e) {
                      App.log('test  App.request >> } catch (e) {\n' + e.message)
                    }

                    App.coverage = res.data
                    if (IS_BROWSER) {
                      setTimeout(function () {
                        var url = StringUtil.trim((App.coverage || {}).url)
                        if (StringUtil.isEmpty(url, false)) {
                          url = App.getBaseUrl() + "/htmlcov/index.html"
                        }
                        if (url.startsWith('/')) {
                          url = App.getBaseUrl() + url
                        }
                        window.open(url)
                      }, 2000)
                    }
                  })
                }
              }
            }
          }, IS_NODE ? 200 : 1000)

          return true
        }

        try {
          var index = indexes[position]
          var it = list[index] || {}

          if (IS_BROWSER) {
            try {
              document.getElementById('docItem' + index).scrollIntoView()
            } catch (e) {
              console.log(e)
            }
          }

          this.restoreRemote(index, it, false)

          this.randoms = []
          this.isRandomShow = true
          this.isRandomEditable = true
          this.isRandomListShow = false
          this.isRandomSubListShow = false
          this.showRandomList(true, it.Flow, false, function (url, res, err) {
            try {
              App.onRandomListResponse(true, false, url, res, err)
            } catch (e) {
              log(e)
            }

            App.testRandom(false, true, false, null, isCross, false, callback)
          })
        } catch (e2) {
          log(e2)
          callback(true, deepAllCount)
        }
      },

      getSummary: function (accountIndex) {
        if (accountIndex == -1) {
          return this.getLogoutSummary()
        }
        var accounts = this.accounts || []
        if (accountIndex == accounts.length) {
          return this.getAllSummary()
        }

        if (accountIndex < 0 || accountIndex >= accounts.length) {
          return {}
        }

        var ci = this.accounts[accountIndex]
        return ci || {}
      },

      getLogoutSummary: function () {
        return this.logoutSummary || {}
      },
      getCurrentSummary: function () {
        return this.getSummary(this.currentAccountIndex) || {}
      },
      getAllSummary: function () {
        return this.allSummary || {}
      },

      getCurrentRandomSummary: function () {
        return (this.isRandomSubListShow ? this.currentRandomItem : this.currentRemoteItem) || {}
      },

      getStatisticsShowStr: function (count, total) {
        if (count == null) {
          count = 0;
        }
        if (total == null) {
          total = 0;
        }

        var showType = this.statisticsShowType;
        if (showType == 0) {
          return '' + count;
        }
        return Math.round(total <= 0 ? 0 : count*1000/total)/10 + '%' + (showType == 1 ? '' : ' ' + count)
      },

      getLogoutSummaryTotalText: function () {
        return this.getStatisticsShowStr(this.getLogoutSummary().totalCount, this.getAllSummary().totalCount)
      },

      getLogoutSummaryWhiteText: function () {
        var summary = this.getLogoutSummary()
        return this.getStatisticsShowStr(summary.whiteCount, summary.totalCount)
      },
      getLogoutSummaryGreenText: function () {
        var summary = this.getLogoutSummary()
        return this.getStatisticsShowStr(summary.greenCount, summary.totalCount)
      },
      getLogoutSummaryBlueText: function () {
        var summary = this.getLogoutSummary()
        return this.getStatisticsShowStr(summary.blueCount, summary.totalCount)
      },
      getLogoutSummaryOrangeText: function () {
        var summary = this.getLogoutSummary()
        return this.getStatisticsShowStr(summary.orangeCount, summary.totalCount)
      },
      getLogoutSummaryRedText: function () {
        var summary = this.getLogoutSummary()
        return this.getStatisticsShowStr(summary.redCount, summary.totalCount)
      },

      getSummaryTotalText: function (index) {
        return this.getStatisticsShowStr(this.getSummary(index).totalCount, this.getAllSummary().totalCount)
      },
      getSummaryWhiteText: function (index) {
        var summary = this.getSummary(index)
        return this.getStatisticsShowStr(summary.whiteCount, summary.totalCount)
      },
      getSummaryGreenText: function (index) {
        var summary = this.getSummary(index)
        return this.getStatisticsShowStr(summary.greenCount, summary.totalCount)
      },
      getSummaryBlueText: function (index) {
        var summary = this.getSummary(index)
        return this.getStatisticsShowStr(summary.blueCount, summary.totalCount)
      },
      getSummaryOrangeText: function (index) {
        var summary = this.getSummary(index)
        return this.getStatisticsShowStr(summary.orangeCount, summary.totalCount)
      },
      getSummaryRedText: function (index) {
        var summary = this.getSummary(index)
        return this.getStatisticsShowStr(summary.redCount, summary.totalCount)
      },

      getCurrentSummaryTotalText: function () {
        return this.getStatisticsShowStr(this.getCurrentSummary().totalCount, this.getAllSummary().totalCount)
      },
      getCurrentSummaryWhiteText: function () {
        var summary = this.getCurrentSummary()
        return this.getStatisticsShowStr(summary.whiteCount, summary.totalCount)
      },
      getCurrentSummaryGreenText: function () {
        var summary = this.getCurrentSummary()
        return this.getStatisticsShowStr(summary.greenCount, summary.totalCount)
      },
      getCurrentSummaryBlueText: function () {
        var summary = this.getCurrentSummary()
        return this.getStatisticsShowStr(summary.blueCount, summary.totalCount)
      },
      getCurrentSummaryOrangeText: function () {
        var summary = this.getCurrentSummary()
        return this.getStatisticsShowStr(summary.orangeCount, summary.totalCount)
      },
      getCurrentSummaryRedText: function () {
        var summary = this.getCurrentSummary()
        return this.getStatisticsShowStr(summary.redCount, summary.totalCount)
      },

      getAllSummaryTotalText: function () {
        return this.getStatisticsShowStr(this.getAllSummary().totalCount, this.getAllSummary().totalCount)
      },
      getAllSummaryWhiteText: function () {
        var summary = this.getAllSummary()
        return this.getStatisticsShowStr(summary.whiteCount, summary.totalCount)
      },
      getAllSummaryGreenText: function () {
        var summary = this.getAllSummary()
        return this.getStatisticsShowStr(summary.greenCount, summary.totalCount)
      },
      getAllSummaryBlueText: function () {
        var summary = this.getAllSummary()
        return this.getStatisticsShowStr(summary.blueCount, summary.totalCount)
      },
      getAllSummaryOrangeText: function () {
        var summary = this.getAllSummary()
        return this.getStatisticsShowStr(summary.orangeCount, summary.totalCount)
      },
      getAllSummaryRedText: function () {
        var summary = this.getAllSummary()
        return this.getStatisticsShowStr(summary.redCount, summary.totalCount)
      },

      isSummaryShow: function (accountIndex) {
        if (accountIndex == -1) {
          return this.isLogoutSummaryShow()
        }
        var accounts = this.accounts
        if (accountIndex == accounts.length) {
          return this.isAllSummaryShow()
        }

        if (accountIndex < 0 || accountIndex >= accounts.length) {
          return false
        }

        // var ci = this.isTestCaseShow ? this.accounts[accountIndex] : null
        var ci = this.accounts[accountIndex]
        return ci != null && ci.totalCount != null && ci.totalCount > 0
      },
      isLogoutSummaryShow: function () {
        // var ci = this.isCrossEnabled != true ? null : this.logoutSummary
        var ci = this.logoutSummary
        return ci != null && ci.totalCount != null && ci.totalCount > 0
      },
      isCurrentSummaryShow: function () {
        return this.isSummaryShow(this.currentAccountIndex)
      },
      isAllSummaryShow: function () {
        // var ci = this.isCrossEnabled != true ? null : this.allSummary
        var ci = this.allSummary
        return ci != null && ci.totalCount != null && ci.totalCount > 0
      },
      isRandomSummaryShow: function () {
        var ci = this.isRandomListShow || this.isRandomSubListShow ? this.getCurrentRandomSummary() : null
        return ci != null && ci.totalCount != null && ci.totalCount > 0
      },

      updateSummary: function (item, change, key) {
        if (change == null || key == null) {
          return item
        }

        if (item == null) {
          item = {}
        }

        var count = item[key]
        if (count == null || count < 0) {
          count = 0
        }
        count += change

        item[key] = count

        // 对于 Random 进入子项再退出后有时显示居然不准
        // if (cri.totalCount == null) {
        //   cri.totalCount = 0
        // }
        // cri.totalCount += change
        // if (cri.totalCount < 0) {
        //   cri.totalCount = 0
        // }
        item.totalCount = item.whiteCount + item.greenCount + item.blueCount + item.orangeCount + item.redCount

        return item
      },

      //更新父级总览数据
      updateToSummary: function (item, change, accountIndex) {
        if (item == null || change == null) {
          return
        }

        var key = item.compareColor + 'Count'
        this.allSummary = this.updateSummary(this.allSummary, change, key)

        if (accountIndex == -1) {
          this.logoutSummary = this.updateSummary(this.logoutSummary, change, key)
        }
        else if (accountIndex >= 0 && accountIndex < this.accounts.length) {
          var accountItem = this.updateSummary(this.getSummary(accountIndex), change, key)
          this.accounts[accountIndex] = accountItem
        }
      },
      updateToRandomSummary: function (item, change, accountIndex) {
        var random = item == null || change == null ? null : item.Input
        if (random == null) {
          return
        }

        if (random.count == 1 || (random.id != null && random.id < 0)) {
          var key = item.compareColor + 'Count'
          this.updateToSummary(item, change, accountIndex)

          var curRandom = this.isRandomListShow || this.currentRandomItem == null ? null : this.currentRandomItem.Input
          var isTemp = curRandom != null && (curRandom.id == null || curRandom.id < 0)
          var cri = this.updateSummary(isTemp ? this.currentRandomItem : this.currentRemoteItem, change, key)  // this.getCurrentRandomSummary())

          if (isTemp) {
            this.currentRandomItem = cri
            this.updateSummary(this.currentRemoteItem, change, key)
          } else {
            this.currentRemoteItem = cri
            Vue.set(this.testCases, this.currentDocIndex, cri)
          }

          var toId = random.toId
          if (toId != null && toId > 0) {
            for (var i in this.randoms) {
              var toIt = this.randoms[i]
              if (toIt != null && toIt.Input != null && toIt.Input.id == toId) {

                var toRandom = toIt.Input
                var id = toRandom == null ? 0 : toRandom.id
                var count = id == null || id <= 0 ? 0 : toRandom.count
                if (count != null && count > 1) {
                  toIt = this.updateSummary(toIt, change, key)
                  Vue.set(this.randoms, i, toIt)
                }

                break
              }
            }
          }
        }
      },

      /**移除调试字段
       * @param obj
       */
      removeDebugInfo: function (obj) {
        if (obj != null) {
          delete obj["trace"]
          // 保留 delete obj["sql:generate|cache|execute|maxExecute"]
          // 保留 delete obj["depth:count|max"]
          delete obj["time"]
          delete obj["timestamp"]
          delete obj["time:start|duration|end"]
          delete obj["time:start|duration|end|parse|sql"]
          delete obj["time:start|duration|end|parse|sql|model"]
          delete obj["det-time:start|duration|end"]
          delete obj["pose-time:start|duration|end"]
          delete obj["seg-time:start|duration|end"]
          delete obj["obb-time:start|duration|end"]
          delete obj["ocr-time:start|duration|end"]
          // 保留 delete obj["throw"]
          // 保留 delete obj["trace:throw"]
          delete obj["trace:stack"]
          delete obj["stack"]
          delete obj["debug:info|help"]
        }
        return obj
      },

      showImgDiff: function (beforeImgUrl, afterImgUrl) {
        if (StringUtil.isEmpty(beforeImgUrl, true) || StringUtil.isEmpty(afterImgUrl, true)) {
          return
        }

        axios({
          method: "get",
          url: baseUrl + '/download?filePath=' + encodeURI(beforeImgUrl),
          responseType: 'arraybuffer'
        })
          .then(res => {
            const beforePic = res.data == null ? null : new Uint8ClampedArray(res.data)
            if (beforePic instanceof Uint8ClampedArray != true) {
              return
            }

            axios({
              method: "get",
              url: baseUrl + '/download?filePath=' + encodeURI(afterImgUrl),
              responseType: 'arraybuffer'
            })
              .then(res2 => {
                console.log("response: ", res2);
                var afterPic = res2.data == null ? null : new Uint8ClampedArray(res2.data)
                if (afterPic instanceof Uint8ClampedArray != true) {
                  return
                }

                [vDiffBefore, vDiffAfter].forEach(vDiff => {
                  var ctx = vDiff.getContext('2d');
                  var diffImgData = ctx.createImageData(1080, 2340) // new Uint8ClampedArray(4*2340*1080)

                  var numDiffPixels = ImgDiffUtil.pixelmatch(beforePic, afterPic, diffImgData.data, 1080, 2340, {threshold: 0.1, diffMask: true});
                  console.log('numDiffPixels = ' + numDiffPixels)

                  if (numDiffPixels <= 0 || diffImgData.byteLength <= 0) {
                    vDiff.style.display = 'none'
                  }
                  else {
                    vDiff.style.display = 'block'

                    // var ctx = vDiff.getContext('2d');
                    // var imageData = new ImageData(diffImgData, 1080, 2340);
                    // ctx.putImageData(imageData, 0, 0);
                    ctx.putImageData(diffImgData, 0, 0);
                  }

                })
              })
              .catch(error2 => {
                console.log("response: ", error2);
              });

          })
          .catch(error => {
            console.log("response: ", error);
          });
      },

      /**
       * @param index
       * @param item
       */
      downloadTest: function (index, item, isRandom) {
        item = item || {}
        var document;
        if (isRandom) {
          document = this.currentRemoteItem || {}
        }
        else {
          document = item.Flow = item.Flow || {}
        }
        var random = isRandom ? item.Input : null
        var testRecord = item.TestRecord = item.TestRecord || {}

        saveTextAs(
          '# APIJSON自动化回归测试-前\n主页: https://github.com/Tencent/APIJSON'
          + '\n\n接口名称: \n' + (document.version > 0 ? 'V' + document.version : 'V*') + ' ' + document.name
          + '\n返回结果: \n' + JSON.stringify(parseJSON(testRecord.response || '{}'), null, '    ')
          , '测试：' + document.name + '-前.txt'
        )

        /**
         * 浏览器不允许连续下载，saveTextAs也没有回调。
         * 在第一个文本里加上第二个文本的信息？
         * beyond compare会把第一个文件的后面一段与第二个文件匹配，
         * 导致必须先删除第一个文件内的后面与第二个文件重复的一段，再重新对比。
         */
        setTimeout(function () {
          var tests = App.tests[String(App.currentAccountIndex)] || {}
          saveTextAs(
            '# APIJSON自动化回归测试-后\n主页: https://github.com/Tencent/APIJSON'
            + '\n\n接口名称: \n' + (document.version > 0 ? 'V' + document.version : 'V*') + ' ' + document.name
            + '\n返回结果: \n' + JSON.stringify(tests[document.id][isRandom ? random.id : 0] || {}, null, '    ')
            , '测试：' + document.name + '-后.txt'
          )


          if (StringUtil.isEmpty(testRecord.standard, true) == false) {
            setTimeout(function () {
              saveTextAs(
                '# APIJSON自动化回归测试-标准\n主页: https://github.com/Tencent/APIJSON'
                + '\n\n接口名称: \n' + (document.version > 0 ? 'V' + document.version : 'V*') + ' ' + document.name
                + '\n测试结果: \n' + JSON.stringify(testRecord.compare || '{}', null, '    ')
                + '\n测试标准: \n' + JSON.stringify(parseJSON(testRecord.standard || '{}'), null, '    ')
                , '测试：' + document.name + '-标准.txt'
              )
            }, 5000)
          }

        }, 5000)

      },

      /**
       * @param index
       * @param item
       */
      handleTest: function (right, index, item, path, isRandom, isDuration, isCross) {
        item = item || {}
        const random = item.Input = item.Input || {}
        var document;
        var isChanged = false;
        if (isRandom) {
          this.currentRandomItem = item;
          var curIndex = this.isRandomSubListShow ? this.currentRandomSubIndex : this.currentRandomIndex;
          isChanged = curIndex != index;
          if (isChanged) {
            if (this.isRandomSubListShow) {
              this.currentRandomSubIndex = index
            } else {
              this.currentRandomIndex = index
              this.currentRandomSubIndex = -1
            }

            this.hoverIds = {};
            this.visiblePaths = [];
            this.missTruth = {};
            // this.sameIds = [];
            this.currentBbox = null;
          }

          if ((random.count || 0) > 1) {
            // this.currentRandomSubIndex = -1
            this.restoreRandom(index, item)
            this.randomSubs = (item.subs || item['[]']) || []
            this.isRandomSubListShow = true
            this.getCurrentRandomSummary().summaryType = 'total'
            return
          }

          document = this.currentRemoteItem || {}

          // vBefore.src = vDiff.src = vAfter.src = this.img = random.img;
        }
        else {
          this.currentDocIndex = index
          this.currentRemoteItem = item
          // this.currentRandomIndex = -1
          // this.currentRandomSubIndex = -1
          document = item.Flow = item.Flow || {}
        }

        this.isFullAssert = true

        var testRecord = item.TestRecord = item.TestRecord || {}
        var pathKeys = StringUtil.split(path, '/') || [];
        var pathNames = pathKeys.slice(0, pathKeys.length - 1);
        var lastKey = pathKeys[pathKeys.length - 1];

        var tests = this.tests[String(this.currentAccountIndex)] || {}
        var currentResponse = (tests[isRandom ? random.flowId : document.id] || {})[
          isRandom ? (random.id > 0 ? random.id : (random.toId + '' + random.id)) : 0
        ] || item.data

        var isBefore = item.showType == 'before'
        // FIXME 向前寻找最近的
        if (isRandom) {
          var isHttp = random.type == InputUtil.EVENT_TYPE_HTTP
          if (isHttp && StringUtil.isEmpty(this.prevOperate)) {
            this.prevOperate = (this.operate != OPERATE_TYPE_HTTP ? this.operate : (StringUtil.isEmpty(this.randoms) ? OPERATE_TYPE_RECORD : OPERATE_TYPE_REPLAY))
          }
          this.operate = isHttp ? OPERATE_TYPE_HTTP : this.prevOperate || this.operate

          var imgUrl = StringUtil.trim(JSONResponse.isObject(currentResponse) ? (currentResponse.TestRecord || currentResponse || {}).screenshot : null)
          vAfter.src = StringUtil.isEmpty(imgUrl) ? vAfter.src : (imgUrl.indexOf('://') >= 0 ? '' : baseUrl) + '/download?filePath=' + encodeURI(imgUrl)

          var beforeUrl = StringUtil.trim(testRecord.screenshot)
          vBefore.src = StringUtil.isEmpty(beforeUrl) ? vBefore.src : (beforeUrl.indexOf('://') >= 0 ? '' : this.server) + '/download?filePath=' + encodeURI(beforeUrl)

          item.img = isBefore ? vBefore.src : vAfter.src

          this.method = random.method
          var format = random.format
          this.type = HTTP_CONTENT_TYPES.includes(format) ? format : REQUEST_TYPE_PARAM
          vUrl.value = isHttp ? random.url || vUrl.value : vUrl.value
          vInput.value = isHttp ? random.request || vInput.value : vInput.value

          this.onChange(false)
        }

        if (pathKeys.length > 0) {
          var curRsp = StringUtil.isEmpty(testRecord.response) ? {} : parseJSON(testRecord.response);
          JSONResponse.setValByPath(curRsp, pathKeys, JSONResponse.getValByPath(currentResponse, pathKeys));
          currentResponse = curRsp;
        }

        const rawRspStr = currentResponse == null ? null : JSON.stringify(currentResponse)
        var rsp = parseJSON(rawRspStr)
        rsp = JSONResponse.array2object(rsp, 'methodArgs', ['methodArgs'], true)

        const list = isRandom ? (random.toId == null || random.toId <= 0 ? this.randoms : this.randomSubs) : this.testCases

        this.file = random.file;
        if (right != true) {
          item.showType = isBefore ? 'after' : 'before'
          Vue.set(list, index, item);

          var res = isBefore ? rawRspStr : testRecord.response
          if (isRandom && ! isBefore) {
            res = res || ((this.currentRemoteItem || {}).TestRecord || {}).response
          }

          this.view = 'code'
          this.jsoncon = res || ''

          if (isChanged && isRandom) {
            //FIXME 仅测试用
            var before = parseJSON(testRecord.response) || (DEBUG ? {
              bboxes: [{
                id: 1, label: '湘A580319X', score: 0.95, angle: 10, color: [255, 0, 70, 128], bbox: [100, 105, 400, 300]
              }, {
                id: 2, label: "person", score: 0.77, color: [131, 90, 120, 16], bbox: [125, 40, 200, 300]
              }, {
                id: 3, label: "car", score: 0.49, color: [62, 170, 0, 99], bbox: [800, 500, 400, 200]
              }, {
                id: 4, label: "person", score: Math.random(), color: [131, 90, 120, 16], bbox: [1920*Math.random(), 1080*Math.random(), 300*Math.random(), 800*Math.random()]
              }],
              lines: [
                [100, 100, 600, 600],
                [300, 200, 1000, 900],
                [1920*Math.random(), 1080*Math.random(), 1920*Math.random(), 1080*Math.random()]
              ],
              polygons: [{
                color: [162, 88, 109, 76],
                points: [
                  [100, 100], [500, 160], [900, 200], [400, 900],
                  [1920*Math.random(), 1080*Math.random()], [1920*Math.random(), 1080*Math.random()], [100, 100]
                ]
              }]
            } : {});

            var after = currentResponse || (DEBUG ? {
//            var after = {
              bboxes: [{
                id: 1, label: '湘A5883I9X', score: 0.6, angle: 11, color: [255, 0, 70, 128], bbox: [100, 105, 400, 300]
              }, {
                id: 2, label: '粤Y1702Y5ZQ', score: 0.79, angle: 360*Math.random(), color: [255, 0, 70, 128], bbox: [1920*Math.random(), 1080*Math.random(), 600*Math.random(), 300*Math.random()]
              }, {
                id: 3, label: "person", score: 0.82, color: [131, 90, 120, 16], bbox: [126, 39, 200, 300]
              }, {
                id: 4, label: "car", score: 0.49, color: [62, 170, 0, 99], bbox: [800, 500, 400, 200]
              }, {
                id: 5, label: "person", score: Math.random(), color: [131, 90, 120, 16], bbox: [1920*Math.random(), 1080*Math.random(), 300*Math.random(), 800*Math.random()]
              }, {
                id: 6, label: "person", score: Math.random(), color: [131, 90, 120, 16], bbox: [1920*Math.random(), 1080*Math.random(), 300*Math.random(), 800*Math.random()]
              }, {
                id: 7, label: "dog", score: Math.random(), color: [255*Math.random(), 255*Math.random(), 255*Math.random(), 100*Math.random()], bbox: [1920*Math.random(), 1080*Math.random(), 600*Math.random(), 400*Math.random()]
              }],
              lines: [
                [100, 100, 600, 600],
                [300, 200, 1200, 700],
                [1920*Math.random(), 1080*Math.random(), 1920*Math.random(), 1080*Math.random()],
                [1920*Math.random(), 1080*Math.random(), 1920*Math.random(), 1080*Math.random()]
              ],
              polygons: [
                [100, 100], [500, 160], [900, 200], [400, 900],
                [1920*Math.random(), 1080*Math.random()], [1920*Math.random(), 1080*Math.random()], [100, 100]
              ]
            } : {});

            const detection = this.detection || {};
            // var currentRandomItem = this.currentRandomItem = this.currentRandomItem || this.randoms[this.currentRandomIndex] || {};
            if (testRecord.total != null && testRecord.total > 0) {
              detection.total = testRecord.total;
            } else {
              testRecord.total = detection.total;
            }

            detection.sameIds = testRecord.sameIds || [];
            detection.missTruth = testRecord.missTruth || {};

            detection.beforeCorrect = testRecord.correct || 0;
            detection.beforeWrong = testRecord.wrong || 0;
            detection.beforeMiss = testRecord.miss || (testRecord.total || 0) - detection.beforeCorrect;
            detection.corrects = [];
            detection.wrongs = [];

            if (testRecord.score != null && testRecord.score > 0) {
              detection.afterThreshold = detection.beforeThreshold = testRecord.score;
            } else {
              testRecord.score = detection.beforeThreshold || detection.afterThreshold || 30;
            }

            if (testRecord.iou != null && testRecord.iou > 0) {
              detection.diffThreshold = testRecord.iou;
            } else {
              testRecord.iou = detection.diffThreshold || 90;
            }

            detection.beforeRecall = testRecord.recall;
            detection.beforePrecision = testRecord.precision;
            detection.beforeF1 = testRecord.f1;

            detection.before = {bboxes: testRecord.bboxes || JSONResponse.convertViewTree(before.viewTree, random) || {}};
            detection.after = {bboxes: JSONResponse.convertViewTree(after.viewTree, random) || {}};

            this.detection = detection;

            // var diff = this.processDiff();
            // this.processAutoMark();
            // this.drawAll();
            var diff = this.processDiffAndAutoMark();
            // var jsonDiff = JSONResponse.deepMerge(JSON.parse(JSON.stringify(before)), JSON.parse(JSON.stringify(after)));
            // this.draw('diffBefore'); // this.drawDetections(vDiffAfterCanvas, diff);
            // this.draw('diffAfter'); //

            var beforeRsp = before; // (StringUtil.isEmpty(testRecord.response, true) ? null : parseJSON(testRecord.response)) || {}
            var beforeImgUrl = (beforeRsp.TestRecord || {}).screenshot
            var afterImgUrl = ((currentResponse || {}).TestRecord || currentResponse || {}).screenshot

            var pic = isBefore ? afterImgUrl : beforeImgUrl
            if (StringUtil.isEmpty(pic)) {  // 往前寻找最近的截屏
              var ind = index
              if (list != null && list.length > ind) {
                while (ind > 0) {
                  ind --
                  var prevItem = list[ind] || {}
                  var prevInput = prevItem.Input
                  var prevInputId = prevInput == null ? null : prevInput.id
                  if (prevInputId != null) {
                    var beforeOutput = prevItem.TestRecord || {}
                    if (beforeOutput == null || beforeOutput.inputId != prevInputId) {
                      continue
                    }

                    var afterOutput = ((tests[prevInput.flowId] || {})[prevInput.toId <= 0 ? prevInputId : (prevInput.toId + '' + prevInput.id)] || {}).TestRecord
                    beforeImgUrl = beforeOutput.screenshot
                    afterImgUrl = (afterOutput || {}).screenshot
                    pic = isBefore ? afterImgUrl : beforeImgUrl
                  }
                }
              }
            }

            if (StringUtil.isEmpty(pic) != true) {
              this.showImgDiff(beforeImgUrl, afterImgUrl)
            }

            const bboxes = (detection.after || {}).bboxes || []
            const randoms = (this.isRandomSubListShow ? this.randomSubs : this.randoms) || []
            var count = index >= 0 ? StringUtil.length(randoms) : 0
            if (count > index && count >= 2) {
              for (let i = 0; i < bboxes.length; i++) {
                const bbox = bboxes[i]
                if (StringUtil.isEmpty(bbox)) {
                  continue
                }

                const inReqLinkExps = bbox.reqLinkExps = bbox.reqLinkExps || {} // a + b, a || b + c
                const inResLinkExps = bbox.resLinkExps = bbox.resLinkExps || {} // a + b, a || b + c
                const inReqLinkPaths = bbox.reqLinkPaths = bbox.reqLinkPaths || {}
                const inResLinkPaths = bbox.resLinkPaths = bbox.resLinkPaths || {}
                const inReqLinkConfigs = bbox.reqLinkConfigs = bbox.reqLinkConfigs || {}
                const inResLinkConfigs = bbox.resLinkConfigs = bbox.resLinkConfigs || {}

                const props = ['text', 'image', 'background']
                for (let j = 0; j < props.length; j ++) {
                  const prop = props[j]
                  const value = bbox[prop]
                  if (StringUtil.isEmpty(value)) {
                    continue
                  }

                  var isFindReq = true
                  for (let k in inReqLinkConfigs) {
                    if (k != null && k.startsWith(prop + ': ')) {
                      isFindReq = false
                      break
                    }
                  }

                  var isFindRes = true
                  for (let k in inResLinkConfigs) {
                    if (k != null && k.startsWith(prop + ': ')) {
                      isFindRes = false
                      break
                    }
                  }

                  const reqLinkPaths = {}
                  const resLinkPaths = {}
                  JSONResponse.findLinkPaths(bbox.viewIdName, prop, value, randoms, index, 20, reqLinkPaths, resLinkPaths, inReqLinkPaths[prop], inResLinkPaths[prop])

                  if (isFindReq) {
                    const reqLinkExp = inReqLinkPaths[prop] = JSONResponse.linkPaths2Exp(reqLinkPaths, value, inReqLinkExps[prop], true)
                    const reqLinkConfig = StringUtil.isEmpty(reqLinkExp) ? null : inReqLinkConfigs[prop + ': ' + reqLinkExp]

                    if (reqLinkConfig != null && StringUtil.isNotString(reqLinkConfig)) {
                      inReqLinkConfigs[prop + ': ' + reqLinkExp] = JSONResponse.linkPaths2Config(reqLinkPaths, true)
                    }
                  }

                  if (isFindRes) {
                    const resLinkExp = inResLinkPaths[prop] = JSONResponse.linkPaths2Exp(resLinkPaths, value, inResLinkExps[prop], false)
                    const resLinkConfig = StringUtil.isEmpty(resLinkExp) ? null : inResLinkConfigs[prop + ': ' + resLinkExp]

                    if (resLinkConfig != null && StringUtil.isNotString(resLinkConfig)) {
                      inResLinkConfigs[prop + ': ' + resLinkExp] = JSONResponse.linkPaths2Config(resLinkPaths, false)
                    }
                  }
                }
              }

            }

            if (random.type === InputUtil.EVENT_TYPE_TOUCH) {
              this.loadPropsAndCompare(index, document, item, random, detection, false)
            }
          }
        }
        else {
          var url;

          if (isBefore) { //撤回原来错误提交的校验标准
            if (isDuration) {
              alert('撤回上次的耗时需要删除上次的对比标准，请点左边 [错的，撤回] 按钮')
              return
            }

            url = this.server + '/delete'
            const req = {
              TestRecord: {
                id: testRecord.id, //TODO 权限问题？ item.userId,
              },
              tag: 'TestRecord'
            }

            this.adminRequest(url, req, {}, function (url, res, err) {
              App.onResponse(url, res, err)

              var data = res.data || {}
              if (JSONResponse.isSuccess(data) != true && testRecord!= null && testRecord.id != null) {
                alert('撤回最新的校验标准 异常：\n' + data.msg)
                return
              }

              if (isRandom) {
                App.updateToRandomSummary(item, -1, App.currentAccountIndex)
                // if (App.compareRandomIds instanceof Array) {
                //   var ind = App.compareRandomIds.indexOf(random.id);
                //   if (ind >= 0) {
                //     App.compareRandomIds.splice(ind, 1)
                //   }
                // }
              } else {
                App.updateToSummary(item, -1, App.currentAccountIndex)
              }

              if (isDuration) {
                item.durationColor = 'black'
                item.durationHint = '正常：在以往最快和最慢之间'
              }
              else {
                item.compareType = JSONResponse.COMPARE_NO_STANDARD
                item.compareMessage = '查看结果'
                item.compareColor = 'white'
                item.hintMessage = '没有校验标准！'
                item.TestRecord = null
              }

              App.updateTestRecord(0, list, index, item, currentResponse, isRandom, true, App.currentAccountIndex, isCross, true)
              App.summary();
            })
          }
          else { //上传新的校验标准
            // if (isRandom && random.id <= 0) {
            //   alert('请先上传这个配置！')
            //   App.currentRandomItem = random
            //   App.showExport(true, false, true)
            //   return
            // }
            var isML = this.isMLEnabled;  // 异常分支不合并内容，只记录 code, throw, msg 等关键信息

            var standard;
            var stddObj;

            var minDuration = testRecord.minDuration
            var maxDuration = testRecord.maxDuration
            if (isDuration) {
              if (item.duration == null) {  // 没有获取到
                alert('最外层缺少字段 "time:start|duration|end|parse|sql|model"，无法对比耗时！')
                return
              }
              else if (maxDuration == null && minDuration == null) {
                maxDuration = item.duration
                minDuration = Math.round(maxDuration*0.8)
              }
              else if (maxDuration == null && minDuration != null) {
                maxDuration = Math.max(minDuration, item.duration)
                testRecord.minDuration = Math.min(minDuration, item.duration)
              }
              else if (minDuration == null && maxDuration != null) {
                minDuration = Math.min(maxDuration, item.duration)
                testRecord.maxDuration = Math.max(maxDuration, item.duration)
              }
              else if (maxDuration > 0 && maxDuration < item.duration) {
                maxDuration = item.duration
              }
              else if (minDuration > 0 && minDuration > item.duration) {
                minDuration = item.duration
              }
              else {  // 已经在正常范围中，不需要纠错
                alert('耗时已经在正常范围中，不需要纠错！')
                return
              }
            }
            else {
              standard = (StringUtil.isEmpty(testRecord.standard, true) ? null : parseJSON(testRecord.standard)) || {}
              if (pathKeys.length <= 0) {
                stddObj = JSONResponse.updateFullStandard(standard, rsp, isML)
              } else if (isML) {
                stddObj = JSONResponse.updateStandardByPath(standard, pathNames, lastKey, rsp)
              }
            }

            const isNewRandom = isRandom && random.id <= 0
            const baseUrl = this.getBaseUrl()
            const userId = this.User.id
            const cri = this.currentRemoteItem || {}
            const chain = cri.Chain || {}
            const cgId = chain.groupId || 0
            const cId = chain.id || 0
            const detection = this.detection || {}
            const missTruth = this.missTruth || {}

            const bboxes = (detection.after || {}).bboxes || []

            //TODO 先检查是否有重复名称的！让用户确认！
            // if (isML != true) {
            url = this.server + '/post'
            const req = {
              Input: isNewRandom != true ? null : {
                toId: random.toId,
                // userId: userId,
                chainGroupId: cgId,
                chainId: cId,
                flowId: random.flowId || document.id,
                name: random.name,
                count: random.count,
                config: random.config
              },
              TestRecord: isDuration ? Object.assign(testRecord, {
                id: undefined,
                reportId: this.reportId,
                host: baseUrl,
                // userId: userId,
                testAccountId: this.getCurrentAccountId(),
                chainGroupId: cgId,
                chainId: cId,
                duration: item.duration,
                minDuration: minDuration,
                maxDuration: maxDuration,
                compare: JSON.stringify(testRecord.compare || {})
              }) : {
                // userId: userId,
                chainGroupId: cgId,
                chainId: cId,
                documentId: isNewRandom ? null : (isRandom ? (random.flowId || document.id) : document.id),
                randomId: isRandom && ! isNewRandom ? random.id : null,
                reportId: this.reportId,
                host: baseUrl,
                testAccountId: this.getCurrentAccountId(),
                total: detection.total,
                correct: detection.afterCorrect,
                wrong: detection.afterWrong,
                miss: detection.afterMiss,
                score: detection.afterThreshold,
                iou: detection.diffThreshold,
                recall: detection.afterRecall,
                precision: detection.afterPrecision,
                f1: detection.afterF1,
                corrects: detection.corrects,
                wrongs: detection.wrongs,
                sameIds: this.sameIds,
                missTruth: Object.keys(missTruth).length <= 0 ? null : JSON.stringify(missTruth),
                compare: JSON.stringify(testRecord.compare || {}),
                response: rawRspStr || (rawRspStr == null ? '' : (StringUtil.isString(random.response) ? random.response : JSON.stringify(random.response))),
                standard: isML ? JSON.stringify(stddObj) : null,
                bboxes: bboxes
              },
              tag: isNewRandom ? 'Input' : 'TestRecord'
            }
            // }
            // else {
            //   url = this.server + '/post/testrecord/ml'
            //   req = {
            //     documentId: document.id
            //   }
            // }
            const index_ = index
            this.adminRequest(url, req, {}, function (url, res, err) {
              App.onResponse(url, res, err)

              var data = res.data || {}
              if (JSONResponse.isSuccess(data) != true) {
                if (isML) {
                  alert('机器学习更新标准 异常：\n' + data.msg)
                }
              }
              else {
                if (isRandom) {
                  App.updateToRandomSummary(item, -1, App.currentAccountIndex)
                  App.uploadImage(index_)
                } else {
                  App.updateToSummary(item, -1, App.currentAccountIndex)
                }

                var testRecord = item.TestRecord || {}
                if (isDuration) {
                  item.durationColor = 'black'
                  item.durationHint = '正常：在以往最快和最慢之间'
                }
                else {
                  item.compareType = JSONResponse.COMPARE_EQUAL
                  item.compareMessage = '查看结果'
                  item.compareColor = 'white'
                  item.hintMessage = '结果正确'

                  testRecord.compare = {
                    code: 0,
                    msg: '结果正确'
                  }
                  testRecord.response = rawRspStr
                  // testRecord.standard = stdd
                }

                if (isRandom) {
                  var r = req == null ? null : req.Input
                  if (r != null && (data.Input || {}).id != null) {
                    r.id = data.Input.id
                    item.Input = r
                  }
                  if ((data.TestRecord || {}).id != null) {
                    var id = testRecord.id = data.TestRecord.id
                    if (r != null) {
                      testRecord.randomId = r.id
                    }

                    var sameIds = App.sameIds
                    var ind = sameIds == null ? -1 : sameIds.indexOf(id)
                    if (ind >= 0) {
                      sameIds.splice(ind, 1)
                    }
                  }

                  // var rid = random.id || r.id || testRecord.randomId;
                  // if (rid != null && rid > 0) {
                  //   if (App.compareRandomIds == null) {
                  //     App.compareRandomIds = [rid]
                  //   } else {
                  //     App.compareRandomIds.push(rid)
                  //   }
                  // }
                }
                item.TestRecord = testRecord


                // if (! isNewRandom) {
                //   if (isRandom) {
                //     App.showRandomList(true, App.currentRemoteItem)
                //   }
                //   else {
                //     App.showTestCase(true, false)
                //   }
                // }

                App.updateTestRecord(0, list, index, item, currentResponse, isRandom, true, App.currentAccountIndex, isCross, true)
                App.summary();
              }

            })

          }
        }
      },

      loadPropsAndCompare: function (index, document, item, input, detection, isSummary) {
        index = index || this.currentRandomIndex
        var subSearch = StringUtil.isEmpty(this.randomSubSearch, true)
            ? null : '%' + StringUtil.trim(this.randomSubSearch) + '%'
        this.adminRequest('/get', {
          '[]': {
            count: 0,
            Random: { // 人工核对上传后的 UI/Data 关联配置及表达式
              toId: input.id,
              '@order': 'date-',
              '@column': 'id,toId,userId,documentId,isRes,name,path,config',
              // 'name$': subSearch
            },
//             '[]': {
//               'count': 10,
//               'page': 0,
//               'Random': {
//                 'toId@': '[]/Random/id',
//                 // 'chainId': cId,
//                 documentId: input.id,
//                 '@order': "time-",
//                 // 'name$': subSearch
//               },
//               'TestRecord': {
//                 'randomId@': '/Input/id',
// //                  'testAccountId': this.getCurrentAccountId(),
//                 'host': StringUtil.isEmpty(baseUrl, true) ? null : baseUrl,
//                 '@order': 'date-'
//               }
//             }
          }
        }, {}, function (url, res, err) {
          App.onResponse(url, res, err)

          var data = (res || {}).data || {}
          if (JSONResponse.isSuccess(data) != true) {
            alert('获取最新的校验标准 异常：\n' + (data.msg || (err || {}).message || (err || {}).reason))
            return
          }

          var randoms = App.randomSubs = item.subs = item['[]'] = data['[]'] || []
          // var configMap = {}

          const corrects = detection.corrects = detection.corrects || []
          const wrongs = detection.wrongs = detection.wrongs || []
          const bboxes = (detection.after || {}).bboxes || []

          const head = ''
          const inputs = App.randoms || []
          const len = StringUtil.length(inputs)
          var redCount = 0
          var total = 0
          App.resetCount(item, true, true, App.currentAccountIndex)

          var pre = null // {}
          for (let i = 0; i < index; i++) {
            const inputItem = inputs[i]
            const input_ = inputItem == null ? null : inputItem.Input
            const type = input_ == null ? null : input_.type
            if (type !== InputUtil.EVENT_TYPE_HTTP) {
              continue
            }

            const data = parseJSON(input_.response, input_.response, true)
            pre = {
              // arg: ,
              data: data,
              pre: pre
            }
          }

          var next = null // {}
          for (let i = len - 1; i > index; i--) {
            const inputItem = inputs[i]
            const input_ = inputItem == null ? null : inputItem.Input
            const type = input_ == null ? null : input_.type
            if (type !== InputUtil.EVENT_TYPE_HTTP) {
              continue
            }

            const arg = parseJSON(input_.request, input_.request, true)
            next = {
              arg: arg,
              next: next
            }
          }

          const ctx = {
            pre: pre,
            next: next
            // req: ,
            // arg: ,
            // res: ,
            // data: ,
          }

          const preScript = StringUtil.trim(((App.scripts.global['0'] || {}).pre || {}).script)
              + '\n' + StringUtil.trim(((App.scripts.case[(document || {}).id] || {}).pre || {}).script)

          for (let i = 0; i < randoms.length; i++) {
            let randomItem = randoms[i]
            const random = randomItem == null ? null : randomItem.Random
            const path = random == null ? null : random.path
            const name = StringUtil.isEmpty(path) ? null : random.name
            if (StringUtil.isEmpty(name)) {
              continue
            }
            const ind = name.indexOf(': ')
            const key = name.substring(0, ind)
            if (StringUtil.isEmpty(key)) {
              random.name += ' // 无效配置 ！必须是 key: value 这种格式！key 只能是 text, image, background 等 View 属性名称！'
              continue
            }

            const config = random.config
            const tr = randomItem.TestRecord = {
              id: -i - 1,
              documentId: random.documentId,
              randomId: random.id
            }

            for (let j = 0; j < bboxes.length; j++) {
              const bbox = bboxes[j]
              const assertPath = bbox == null ? null : bbox.assertPath || bbox.viewPath
              if (! JSONResponse.isViewPathMatch(assertPath, path)) {
                continue
              }

              const reqLinkConfigs = bbox.reqLinkConfigs = bbox.reqLinkConfigs || {};
              const resLinkConfigs = bbox.resLinkConfigs = bbox.resLinkConfigs || {};
              if (random.isRes) {
                resLinkConfigs[name] = config
              } else {
                reqLinkConfigs[name] = config
              }

              const id = j // bbox.id || bbox.index || j
              try {
                App.parseRandom({}, head, config, random.id, true, false, false
                  , function (randomName, constConfig, constJson) {
                  // var first = true
                  // var script = '\nvar ['
                  var script = '\n'
                  for (var k in constJson) {
                    var v = constJson[k]
                    script += '\nvar ' + k + ' = ' + (typeof v == 'undefined' ? 'undefined' : JSON.stringify(v)) + ';'
                    // script += (first ? '' : ', ') + k
                    // first = false
                  }
                  // script += '] = ' + JSON.stringify(constJson)

                  App.parseRandom(constJson, head, name, random.id, true, false, false
                    , function (randomName2, constConfig2, constJson2) {
                        const val = constJson2 == null ? null : constJson2[key]
                        const transVal = JSONResponse.transform(key, val, bbox)
                        const compare = JSONResponse.compareWithBefore(transVal, bbox[key])
                        tr.compare = {
                            code: JSONResponse.COMPARE_VALUE_CHANGE,
                            path: assertPath,
                            msg: 'UI 没按数据渲染 ' + StringUtil.limitLength(transVal, 20) + (transVal === val ? '' : ' = transform(' + StringUtil.limitLength(val, 20) + ')')
                        }
                        var compareShowObj = JSONResponse.getCompareShowObj(tr.compare)
                        var color = randomItem.compareColor = compareShowObj.compareColor || ''
                        randomItem.compareType = compareShowObj.compareType
                        randomItem.compareMessage = compareShowObj.compareMessage
                        randomItem.hintMessage = compareShowObj.hintMessage

                        total += 1
                        item.totalCount = total

                        var count = item[color + 'Count'] || 0
                        item[color + 'Count'] = count + 1

                        if (compare.code > JSONResponse.COMPARE_EQUAL) { // FIXME 生成 transform 函数内部代码，用这个函数处理后再对比；image/background 需要图像对比
                          if (corrects.includes(id)) {
                            corrects.splice(id, 1)
                          }
                          if (! wrongs.includes(id)) {
                            wrongs.push(id)
                          }
                        }
                    }, preScript + script, ctx)
                  }, preScript, ctx)
              } catch (e) {
                console.log(e)
                // random.name += ' // 断言报错！' + e.message
                tr.compare = {
                  code: JSONResponse.COMPARE_ERROR,
                  path: assertPath,
                  msg: '断言报错！' + e.message
                }
                var compareShowObj = JSONResponse.getCompareShowObj(tr.compare)
                randomItem.compareColor = compareShowObj.compareColor
                randomItem.compareType = compareShowObj.compareType
                randomItem.compareMessage = compareShowObj.compareMessage
                randomItem.hintMessage = compareShowObj.hintMessage
                total += 1
                redCount += 1
                item.totalCount = total
                item.redCount = redCount

                // if (corrects.includes(id)) {
                //   corrects.splice(id, 1)
                // }
                // if (! wrongs.includes(id)) {
                //   wrongs.push(id)
                // }
              }
            }
          }

          // App.compareResponse(res, allCount, list, index, item, response, isRandom, accountIndex, true, err, ignoreTrend, isCross);
          App.drawAll()

          if (isSummary) {
            App.summary();
          }
        })
      },

      updateTestRecord: function (allCount, list, index, item, response, isRandom, ignoreTrend, accountIndex, isCross, isSummary) {
        item = item || {}
        var doc = (isRandom ? item.Input : item.Flow) || {}

        this.adminRequest('/get', {
          TestRecord: {
            documentId: isRandom ? doc.flowId : doc.id,
            randomId: isRandom ? doc.id : 0,
            testAccountId: this.getCurrentAccountId(),
            'invalid': 0,
            'host': this.getBaseUrl(),
            '@order': 'date-',
            '@column': 'id,userId,testAccountId,documentId,randomId,reportId,duration,minDuration,maxDuration,total,correct,wrong,miss,score,iou,recall,precision,f1,corrects,wrongs,sameIds,response,img,screenshot' + (this.isMLEnabled ? ',missTruth,standard' : ''),
            'standard{}': this.isMLEnabled ? (this.database == 'SQLSERVER' ? 'len(standard)>2' : 'length(standard)>2') : null  // '@having': this.isMLEnabled ? 'json_length(standard)>0' : null
          }
        }, {}, function (url, res, err) {
          App.onResponse(url, res, err)

          var data = (res || {}).data || {}
          if (JSONResponse.isSuccess(data) != true) {
            alert('获取最新的校验标准 异常：\n' + data.msg)
            return
          }

          item.TestRecord = data.TestRecord
          App.compareResponse(res, allCount, list, index, item, response, isRandom, accountIndex, true, err, ignoreTrend, isCross);

          if (isSummary) {
            App.summary();
          }
        })
      },

      //显示详细信息, :data-hint :data, :hint 都报错，只能这样
      setRequestHint: function(index, item, isRandom) {
        item = item || {}
        var d = isRandom ? item.Input : item.Flow;
        // var r = d == null ? null : (isRandom ? d.config : d.request);
        // this.$refs[isRandom ? 'randomTexts' : 'testCaseTexts'][index].setAttribute('data-hint', r == null ? '' : (isRandom ? r : JSON.stringify(this.getRequest(r), null, ' ')));

        if (isRandom) {
          var toId = (d == null ? null : d.toId) || 0
          var eles = this.$refs[toId <= 0 ? 'randomTexts' : 'randomSubTexts'];
          var ele = eles == null ? null : eles[index];
          if (ele == null) {
            return;
          }
          ele.setAttribute('data-hint', (d || {}).config == null ? '' : d.config);
        }
        else {
          var eles = this.$refs['testCaseTexts'];
          var ele = eles == null ? null : eles[index];
          if (ele == null) {
            return;
          }
          ele.setAttribute('data-hint', StringUtil.isEmpty(d.request, true) ? '' : JSON.stringify(this.getRequest(d.request, {}, true), null, ' '));
        }
      },

      //显示详细信息, :data-hint :data, :hint 都报错，只能这样
      setTestHint: function(index, item, isRandom, isDuration, isHandle) {
        item = item || {};
        var toId = isRandom ? ((item.Input || {}).toId || 0) : 0;
        var h = isDuration ? item.durationHint : (isHandle ? item.compareMessage : item.hintMessage);
        var eles = this.$refs['test' + (isRandom ? (toId <= 0 ? 'Random' : 'RandomSub') : '') + (isHandle ? 'Handle' : 'Result') + (isDuration ? 'Duration' : '') + 'Buttons'];
        var ele = eles == null ? null : eles[index];
        if (ele == null) {
          return;
        }
        ele.setAttribute('data-hint', h || '');
      },

      handleTestArg: function(hasTestArg, rawReq, delayTime, callback) {
        if (hasTestArg && IS_BROWSER) {
          vUrlComment.value = ""
          vComment.value = ""
          vWarning.value = ""
        }

        if (IS_BROWSER) {
          App.onChange(false)
        }

        if (hasTestArg && rawReq.send != "false" && rawReq.send != "null") {
          setTimeout(function () {
            if (rawReq.send == 'random') {
              App.onClickTestRandom(App.isCrossEnabled, callback)
            } else if (App.isTestCaseShow) {
              App.onClickTest(callback)
            } else {
              App.send(false, callback)
            }

            var url = vUrl.value || ''
            if (IS_BROWSER && (rawReq.jump == "true" || rawReq.jump == "null"
              || (rawReq.jump != "false" && App.isTestCaseShow != true && rawReq.send != 'random'
                && (url.endsWith("/get") || url.endsWith("/head"))
              )
            )) {
              setTimeout(function () {
                window.open(vUrl.value + "/" + encodeURIComponent(JSON.stringify(encode(parseJSON(vInput.value)))))
              }, 2000)
            }
          }, Math.max(2000, delayTime))
        }
      },

      autoTest: function(callback, delayTime, isTest, rawReq, setting) {
        this.autoTestCallback = callback

        if (delayTime == null) {
          delayTime = 0
        }

        if (isTest == null) {
          isTest = true
        }

        if (rawReq == null) {
          rawReq = {
            send: true,
            type: REQUEST_TYPE_JSON,
            url: 'http://localhost:8080/get'
          }
        }

        if (setting == null) {
          setting = StringUtil.isEmpty(rawReq.setting, true) ? null : parseJSON(StringUtil.trim(rawReq.setting, true))
        }

        if (setting == null) {
          setting = {
            isLocalShow: false,
            isTestCaseShow: true,
            isRandomShow: true,
            isRandomListShow: true,
            isRandomSubListShow: false,
            isMLEnabled: true,
            isCrossEnabled: true,
            // testCaseCount: 100,
            testCasePage: 0,
            // randomCount: 100,
            randomPage: 0,
          }
        }

        rawReq.setting = setting

        this.isLocalShow = setting.isLocalShow
        this.isTestCaseShow = setting.isTestCaseShow
        this.isRandomShow = setting.isRandomShow
        this.isRandomListShow = setting.isRandomListShow
        this.isRandomSubListShow = setting.isRandomSubListShow
        this.isMLEnabled = setting.isMLEnabled
        this.isCrossEnabled = setting.isCrossEnabled
        // this.testCaseCount = setting.testCaseCount
        this.testCasePage = setting.testCasePage
        // this.randomCount = setting.randomCount
        this.randomPage = setting.randomPage
        this.server = 'http://localhost:8080' // this.getBaseUrl()

        // if (this.isCrossEnabled) {
        //   this.currentAccountIndex = -1
        // }

        this.login(true, function (url, res, err) {
          if (setting.isRandomShow && setting.isRandomListShow) {
            delayTime += Math.min(5000, (App.isMLEnabled ? 50 : 20) * (setting.randomCount || App.randomCount) + 1000)
            App.isRandomShow = true
            App.isRandomEditable = true
            App.isRandomListShow = false
            App.isRandomSubListShow = false
            // App.showRandomList(false, setting.isRandomSubListShow ? App.currentRandomItem : null, setting.isRandomSubListShow)
            App.showRandomList(true, setting.isRandomSubListShow ? (App.currentRandomItem || {}).Input : (App.currentRemoteItem || {}).Flow, setting.isRandomSubListShow, function (url, res, err) {
              App.onRandomListResponse(true, setting.isRandomSubListShow, url, res, err)
              App.handleTestArg(isTest, rawReq, delayTime, callback)
            })
          }
          else {  // if (setting.isTestCaseShow) {
            delayTime += Math.min(5000, (App.isMLEnabled ? 30 : 10) * (setting.testCaseCount || App.testCaseCount) + 1000)

            // App.login(true)
            App.onLoginResponse(true, {
              type: 0, // 登录方式，非必须 0-密码 1-验证码
              phone: App.account,
              password: App.password,
              version: 1, // 全局默认版本号，非必须
              remember: vRemember.checked,
              format: false
            }, url, res, err)

            App.showTestCase(true, setting.isLocalShow, function (url, res, err) {
              App.onTestCaseListResponse(IS_BROWSER, url, res, err)
              App.isTestCaseShow = true
              App.handleTestArg(isTest, rawReq, delayTime, callback)
            })
          }

        })
      },

      toPathValuePairMap: function (json, path, map) {
        if (map == null) {
          map = {}
        }
        if (json == null) {
          return map
        }

        if (json instanceof Array) {
          for (var i = 0; i < json.length; i++) {
            var p = StringUtil.isEmpty(path) ? '' + i : path + '/' + i
            map = this.toPathValuePairMap(json[i], p, map)
          }
        }
        else if (json instanceof Object) {
          for (var k in json) {
            var p = StringUtil.isEmpty(path) ? k : path + '/' + k
            map = this.toPathValuePairMap(json[k], p, map)
          }
        }
        else {
          map[path == null ? '' : path] = json
        }

        return map
      },

      showOptions: function(target, text, before, after, isValue, filter) {
        currentTarget = target;
        isInputValue = isValue;
        selectionStart = target.selectionStart;
        selectionEnd = target.selectionEnd;
        App.selectIndex = -1;

        clearTimeout(handler);

        // var posX = 0, posY = 0;

        // var event = window.event;
        // if (event.pageX || event.pageY) {
        //   posX = event.pageX;
        //   posY = event.pageY;
        // }
        // else if (event.clientX || event.clientY) {
        //   posX = event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
        //   posY = event.clientY + document.documentElement.scrollTop + document.body.scrollTop;
        // }
        // else if (target.offsetHeight || target.offsetWidth) {
        //   // posX = target.offsetHeight;
        //   // posY = target.offsetWidth;
        // }
        //
        // vOption.style.left = posX + 'px';
        // vOption.style.top = posY + 'px';


        var options = App.options;
        if (options.length > 0 && StringUtil.isNotEmpty(filter, true)) {
          var newOptions = [];
          for (var i = 0; i < options.length; i++) {
            var opt = options[i];
            var name = opt == null ? null : opt.name;
            if (name != null && name.indexOf(filter) >= 0) {
              newOptions.push(opt);
            }
          }

          // App.options = [];
          App.options = newOptions;
        }
        else {
          App.options = [];

          var stringType = CodeUtil.getType4Language(App.language, "string")
          var objectType = CodeUtil.getType4Language(App.language, "object")
          var arrayType = CodeUtil.getType4Language(App.language, "array")
          var varcharType = CodeUtil.getType4Language(App.language, "varchar")
          var intType = CodeUtil.getType4Language(App.language, "int")
          var booleanType = CodeUtil.getType4Language(App.language, "boolean")
          var isReq = App.isEditResponse != true

          if (target == vHeader) {
            if (isValue != true) {
              App.options = [
                {
                  name: "Cookie",
                  type: stringType,
                  comment: "指定 Cookie"
                }, {
                  name: "Set-Cookie",
                  type: stringType,
                  comment: "设置 Cookie"
                }, {
                  name: "Add-Cookie",
                  type: stringType,
                  comment: "添加 Cookie"
                }, {
                  name: "Token",
                  type: stringType,
                  comment: "指定 Token"
                }, {
                  name: "Authorization",
                  type: stringType,
                  comment: "授权"
                }, {
                  name: "Authentication",
                  type: stringType,
                  comment: "鉴权"
                }, {
                  name: "Content-Type",
                  type: stringType,
                  comment: "数据类型"
                }, {
                  name: "Accept",
                  type: stringType,
                  comment: "接收格式"
                }, {
                  name: "Accept-Encoding",
                  type: stringType,
                  comment: "接收编码"
                }, {
                  name: "Accept-Language",
                  type: stringType,
                  comment: "接收语言"
                }, {
                  name: "Cache-Control",
                  type: stringType,
                  comment: "缓存控制"
                }, {
                  name: "Connection",
                  type: stringType,
                  comment: "连接控制"
                }, {
                  name: "Keep-Alive",
                  type: stringType,
                  comment: "保持连接"
                }];
            }
          }
          else if (target == vRandom || target == vScript) {
            if (target == vScript) {
              App.options = [
                {
                  name: "type",
                  type: stringType,
                  comment: '请求格式类型：PARAM, JSON, FORM, DATA'
                },{
                  name: "url",
                  type: stringType,
                  comment: '请求地址，例如 http://localhost:8080/get '
                },{
                  name: "req",
                  type: objectType,
                  comment: '请求参数，例如 { format: true, "User": { "id": 82001 } } '
                },{
                  name: "header",
                  type: objectType,
                  comment: '请求头，例如 Cookie: abc123 '
                }];

              if (isValue) {
                App.options.push({
                  name: "callback(url, res, err)",
                  type: objectType,
                  comment: '回调函数'
                })
                App.options.push({
                  name: "sendRequest(isAdminOperation, method, type, url, req, header, callback)",
                  type: objectType,
                  comment: '真正发送请求函数'
                })
                App.options.push({
                  name: "App.request(isAdminOperation, method, type, url, req, header, callback)",
                  type: objectType,
                  comment: '包装发送请求函数'
                })
                App.options.push({
                  name: "if () {\n    \n} else if () {\n    \n} else {\n    \n}",
                  type: objectType,
                  comment: '包装发送请求函数'
                })
                App.options.push({
                  name: "switch () {\n    case 1:\n        \n        break\n    case 2:\n        \n        break\n    default:\n        \n        break\n}",
                  type: objectType,
                  comment: '包装发送请求函数'
                })
                App.options.push({
                  name: "try {\n    \n} catch(e) {\n    console.log(e)\n}",
                  type: objectType,
                  comment: '包装发送请求函数'
                })
                App.options.push({
                  name: "{}", type: objectType, comment: '对象'
                })
                App.options.push({
                    name: "[]", type: arrayType, comment: '数组'
                })
                App.options.push({
                    name: "undefined", name: "undefined", comment: '未定义'
                })
              }
              else {
                App.options.push({
                  name: "callback",
                  type: objectType,
                  comment: '回调函数 function(url, res, err) {} '
                })
              }
            }

            if (isValue != true) {
              var standardObj = null;
              try {
                var currentItem = App.isTestCaseShow ? App.remotes[App.currentDocIndex] : App.currentRemoteItem;
                standardObj = parseJSON(((currentItem || {})[isReq ? 'Flow' : 'TestRecord'] || {}).standard);
              } catch (e3) {
                log(e3)
              }
              if (standardObj == null) {
                standardObj = JSONResponse.updateStandard({},
                  isReq ? App.getRequest(vInput.value) : App.jsoncon == null ? null : parseJSON(App.jsoncon)
                )
              }

              var method = App.isTestCaseShow ? ((App.currentRemoteItem || {}).Flow || {}).url : App.getMethod();
              var isRestful = ! JSONObject.isAPIJSONPath(method);
              var ind = method == null ? -1 : method.lastIndexOf('/');
              var ind2 = ind < 0 ? -1 : method.substring(0, ind).lastIndexOf('/');
              var table = method == null ? null : (ind < 0 ? method : (isRestful
                ? StringUtil.firstCase(method.substring(ind2+1, ind), true) : method.substring(ind+1))
              );

              var tableList = docObj == null ? null : docObj['[]']
              var isAPIJSONRouter = false // TODO

              var json = App.getRequest(vInput.value)
              var map = App.toPathValuePairMap(json) || {}
              for (var path in map) {
                if (StringUtil.isEmpty(path)) {
                  continue
                }

                var ks = StringUtil.split(path, '/')
                var tbl = ks.length < 2 ? table : ks[ks.length - 2]

                var v = map[path]
                var t = v == null ? null : CodeUtil.getType4Request(v)
                var k = ks[ks.length - 1]

                App.options.push({
                  name: target != vScript ? path : JSONResponse.formatKey(ks.join('_'), true, true, true, true, true, true),
                  type: t == null ? null : (t == 'string' ? stringType : (t == 'integer' ? intType : CodeUtil.getType4Language(App.language, t))),
                  comment: CodeUtil.getComment4Request(tableList, tbl, k, v, method, false, App.database, App.language
                    , isReq, ks, isRestful, standardObj, false, isAPIJSONRouter)
                })
              }
            }
            else if (target == vRandom) {
              App.options = [
                {
                  name: "ORDER_DB(-10, 100000, 'Comment', 'id')",
                  type: stringType,
                  comment: "从数据库顺序取值 function(min:Integer, max:Integer, table:String, column:String) 可使用 ORDER_DB+2(0, 100) 间隔 step = 2 位来升序取值"
                }, {
                  name: "ORDER_IN(true, 1, 'a')",
                  type: stringType,
                  comment: "从选项内顺序取值 function(val0:Any, val1:Any ...) 可使用 ORDER_INT-3(0, 100) 间隔 step = -3 位来降序取值"
                }, {
                  name: "ORDER_INT(-10, 100)",
                  type: stringType,
                  comment: "从范围内顺序取值 function(min:Integer, max:Integer) 可使用 ORDER_IN+(0, 100) 间隔 step = 1 位来升序取值"
                }, {
                  name: "RANDOM_DB(-10, 100000, 'Comment', 'id')",
                  type: stringType,
                  comment: "从数据库随机取值 function(min:Integer, max:Integer, table:String, column:String)"
                }, {
                  name: "RANDOM_IN(true, 1, 'a')",
                  type: stringType,
                  comment: "从选项内随机取值 function(val0:Any, val1:Any ...)"
                }, {
                  name: "RANDOM_INT(-10, 100)",
                  type: stringType,
                  comment: "从范围内随机取整数 function(min:Integer, max:Integer)"
                }, {
                  name: "RANDOM_NUM(-9.9, 99.99)",
                  type: stringType,
                  comment: "从范围内随机取小数 function(min:Number, max:Number, precision:Integer)"
                }, {
                  name: "RANDOM_STR()",
                  type: stringType,
                  comment: "从长度范围内随机取字符串 function(minLength:Integer, maxLength:Integer, regexp:String)"
                }, {
                  name: "undefined", type: "undefined", comment: '未定义'
                }, {
                  name: "Math.round(100*Math.random())", type: stringType, comment: '自定义代码'
                }
              ]

              if (App.isChainShow) {
                  App.options = [
                    {
                      name: "PRE_DATA('list/0/User/id')",
                      type: stringType,
                      comment: "从上个请求的返回结果中取值 function(path:String?, defaultVal:Any?, method_url:String?, account_host_index_path:String|Integer?, msg:String?) // 示例 method_url: 'GET /user/list' | 'POST /register', account_host_index_path: '13000082001@http://localhost/-1 | '13000082001' | 'http://localhost' | -1 "
                    }, {
                      name: "PRE_ARG('id')",
                      type: stringType,
                      comment: "从上个请求的参数中取值 function(path:String?, defaultVal:Any?, method_url:String?, account_host_index_path:String|Integer?, msg:String?) // 示例 method_url: 'GET /comments' | 'PUT /login', account_host_index_path: '123@mail.com@http://apijson.cn:8080/0 | '123@mail.com' | 'http://apijson.cn:8080' | 0 "
                    }, {
                      name: "CTX_GET('userId')",
                      type: stringType,
                      comment: "在上下文存放键值对 function(key:String?, defaultVal:Any?, msg:String?)"
                    }, {
                      name: "CTX_PUT('userId', 'User/id', 'CUR_DATA')",
                      type: stringType,
                      comment: "在上下文存放键值对 function(key:String, val:Any, from:String?, msg:String?)"
                    }, {
                      name: "PRE_RES('data/Comment[]/0/momentId')",
                      type: stringType,
                      comment: "从上个请求的 Response 对象中取值 function(path:String, defaultVal:Any?, method_url:String?, account_host_index_path:String|Integer?, msg:String?) // 示例 method_url: 'GET /token' | 'DELETE /log/{id}', account_host_index_path: '13000082002@http://localhost:8080/1 | '13000082002' | 'http://localhost:8080' | 1 "
                    }, {
                      name: "PRE_REQ('config/headers/token')",
                      type: stringType,
                      comment: "从上个请求 Request 对象中取值 function(path:String, defaultVal:Any?, method_url:String?, account_host_index_path:String|Integer?, msg:String?) // 示例 method_url: 'GET /user/{id}' | 'POST /moment', account_host_index_path: 'test@gmail.com@http://apijson.cn:9090/-2 | 'test@gmail.com' | 'http://apijson.cn:9090' | -2 "
                    }, {
                      name: "CUR_ARG('[]/page')",
                      type: stringType,
                      comment: "从当前请求的参数中取值 function(path:String, defaultVal:Any?, msg:String?)"
                    }, {
                      name: "CUR_REQ('config/data/[]/count')",
                      type: stringType,
                      comment: "从当前请求的 Request 对象中取值 function(path:String, defaultVal:Any?, msg:String?)"
                    }, {
                      name: "CUR_DATA('[]/count')",
                      type: stringType,
                      comment: "从当前请求的返回结果中取值 function(path:String?, defaultVal:Any?, msg:String?)"
                    // }, {
                    //   name: "CUR_RES('[]/count')",
                    //   type: stringType,
                    //   comment: "从当前请求的 Response 对象中取值 function(path:String, defaultVal:Any?, msg:String?)"
                    }
                  ].concat(App.options || [])
              }
            }
            else {
              App.options.push({
                name: isSingle ? "res.key" : "res['key']", type: stringType, comment: '从上个请求的结果中取值'
              })
            }
          }
          else if (target == vInput) { // FIXME 似乎没有必要
            // var quote = isSingle ? "'" : '"';
            //
            // var table = null;
            // var isArrayKey = false;
            // var isSubqueryKey = false;
            //
            // var prev = before;
            // while (prev != null && prev.length > 0) {
            //   var lastIndex = prev.lastIndexOf('{');
            //   prev = prev.substring(0, lastIndex).trimRight();
            //
            //   if (prev.endsWith(':')) {
            //     prev = prev.substring(0, prev.length - 1).trimRight();
            //     var endsWithDoubleQuote = prev.endsWith('"')
            //
            //     if (endsWithDoubleQuote || prev.endsWith("'")) {
            //       prev = prev.substring(0, prev.length - 1);
            //       lastIndex = prev.lastIndexOf('\n');
            //
            //       var lastLine = prev.substring(lastIndex + 1, prev.length);
            //       var ind = lastLine.lastIndexOf(endsWithDoubleQuote ? '"' : "'");
            //       table = ind < 0 ? null : lastLine.substring(ind + 1, lastLine.length);
            //
            //       if (App.isTableKey(table)) {
            //         break;
            //       }
            //       if (table != null && table.endsWith('[]')) {
            //         isArrayKey = true;
            //         break;
            //       }
            //       if (table != null && table.endsWith('@')) {
            //         isSubqueryKey = true;
            //         break;
            //       }
            //
            //       prev = lastIndex <= 0 ? '' : prev.substring(0, lastIndex);
            //     }
            //   }
            // }
            //
            // if (isValue) {
            //   var lastIndex = before.lastIndexOf('\n');
            //   var lastLine = before.substring(lastIndex + 1, before.length);
            //   lastIndex = lastLine.lastIndexOf(':');
            //   lastLine = lastIndex < 0 ? '' : lastLine.substring(0, lastIndex).trim();
            //
            //   var endsWithDoubleQuote = lastLine.endsWith('"')
            //   if (endsWithDoubleQuote || lastLine.endsWith("'")) {
            //     lastLine = lastLine.substring(0, lastLine.length - 1);
            //   }
            //   var ind = lastLine.lastIndexOf(endsWithDoubleQuote ? '"' : "'");
            //   var key = ind < 0 ? null : lastLine.substring(ind + 1, lastLine.length);
            //
            // } else {
            //
            // }
          }
        }

        if (App.options.length > 0) {
          vOption.focus();
        }
        else {
          target.focus();
        }
      }
    },
    watch: {
      jsoncon: function () {
        this.showJsonView()
      }
    },
    computed: {
      theme: function () {
        var th = this.themes[this.checkedTheme]
        var result = {}
        var index = 0;
        ['key', 'String', 'Number', 'Boolean', 'Null', 'link-link'].forEach(function(key) {
          result[key] = th[index]
          index++
        })
        return result
      }
    },
    mounted: function () {
      this.imgMap = {
        before: vBefore || document.getElementById("vBefore"),
        diffBefore: vDiffBefore || document.getElementById("vDiffBefore"),
        diffAfter: vDiffAfter || document.getElementById("vDiffAfter"),
        after: vAfter || document.getElementById("vAfter")
      };
      this.canvasMap = {
        before: vBeforeCanvas || document.getElementById("vBeforeCanvas"),
        diffBefore: vDiffBeforeCanvas || document.getElementById("vDiffBeforeCanvas"),
        diffAfter: vDiffAfterCanvas || document.getElementById("vDiffAfterCanvas"),
        after: vAfterCanvas || document.getElementById("vAfterCanvas")
      };
    },
    created: function  () {
      try { //可能URL_BASE是const类型，不允许改，这里是初始化，不能出错
        var url = this.getCache('', 'URL_BASE')
        if (StringUtil.isEmpty(url, true) == false) {
          URL_BASE = url
        }
        var database = this.getCache('', 'database')
        if (StringUtil.isEmpty(database, true) == false) {
          this.database = CodeUtil.database = database
        }
        var schema = this.getCache('', 'schema')
        if (StringUtil.isEmpty(schema, true) == false) {
          this.schema = CodeUtil.schema = schema
        }
        var language = this.getCache('', 'language')
        if (StringUtil.isEmpty(language, true) == false) {
          this.language = CodeUtil.language = language
        }

        var methods = this.getCache('', 'methods')
        this.methods = methods instanceof Array ? methods : StringUtil.split(methods, ',', true)
        // var types = this.getCache('', 'types')
        // this.types = (types instanceof Array ? types : StringUtil.split(types, ',', true)) || this.types

        var otherEnv = this.getCache('', 'otherEnv')
        if (StringUtil.isEmpty(otherEnv, true) == false) {
          this.otherEnv = otherEnv
        }
        var server = this.getCache('', 'server')
        if (StringUtil.isNotEmpty(server, true)) {
          this.server = server
        } else {
          this.server = window.location.origin
        }

        var thirdParty = this.getCache('', 'thirdParty')
        if (StringUtil.isEmpty(thirdParty, true) == false) {
          this.thirdParty = thirdParty
        }

        this.locals = this.getCache('', 'locals', [])

        this.isDelegateEnabled = this.getCache('', 'isDelegateEnabled', this.isDelegateEnabled)
        this.isEncodeEnabled = this.getCache('', 'isEncodeEnabled', this.isEncodeEnabled)
        this.isEnvCompareEnabled = this.getCache('', 'isEnvCompareEnabled', this.isEnvCompareEnabled)
        //预览了就不能编辑了，点开看会懵 this.isPreviewEnabled = this.getCache('', 'isPreviewEnabled', this.isPreviewEnabled)
        this.isStatisticsEnabled = false // 解决每次都查不到有效 Response this.getCache('', 'isStatisticsEnabled', this.isStatisticsEnabled)
        this.isHeaderShow = this.getCache('', 'isHeaderShow', this.isHeaderShow)
        this.isRandomShow = this.getCache('', 'isRandomShow', this.isRandomShow)
      } catch (e) {
        console.log('created  try { ' +
          '\nvar url = this.getCache(, url) ...' +
          '\n} catch (e) {\n' + e.message)
      }
      try { //这里是初始化，不能出错
        var projectHost = this.getCache('', 'projectHost')
        if (projectHost != null) {
            this.projectHost = projectHost
        }

        var projectHosts = this.getCache('', 'projectHosts')
        if (projectHosts != null && projectHosts.length >= 1) {
          this.projectHosts = projectHosts
        }
      } catch (e) {
        console.log('created  try { ' +
          '\nvar projectHosts = this.getCache("", projectHosts)' +
          '\n} catch (e) {\n' + e.message)
      }
      try { //这里是初始化，不能出错
        var accounts = this.getCache(URL_BASE, 'accounts')
        if (accounts != null && accounts.length >= 1) {
          this.accounts = accounts
          this.currentAccountIndex = this.getCache(URL_BASE, 'currentAccountIndex')
        }
      } catch (e) {
        console.log('created  try { ' +
          '\nvar accounts = this.getCache(URL_BASE, accounts)' +
          '\n} catch (e) {\n' + e.message)
      }
      try { //这里是初始化，不能出错
        var otherEnvTokenMap = this.getCache(this.otherEnv, 'otherEnvTokenMap')
        if (otherEnvTokenMap != null) {
          this.otherEnvTokenMap = otherEnvTokenMap
        }
      } catch (e) {
        console.log('created  try { ' +
            '\nvar otherEnvTokenMap = this.getCache(this.otherEnv, otherEnvTokenMap)' +
            '\n} catch (e) {\n' + e.message)
      }
      try { //这里是初始化，不能出错
        var otherEnvCookieMap = this.getCache(this.otherEnv, 'otherEnvCookieMap')
        if (otherEnvCookieMap != null) {
          this.otherEnvCookieMap = otherEnvCookieMap
        }
      } catch (e) {
        console.log('created  try { ' +
            '\nvar otherEnvCookieMap = this.getCache(this.otherEnv, otherEnvCookieMap)' +
            '\n} catch (e) {\n' + e.message)
      }

      try { //可能URL_BASE是const类型，不允许改，这里是初始化，不能出错
        this.User = this.getCache(this.server, 'User', {})
        this.isCrossEnabled = this.getCache(this.server, 'isCrossEnabled', this.isCrossEnabled)
        this.isMLEnabled = this.getCache(this.server, 'isMLEnabled', this.isMLEnabled)
        this.crossProcess = this.isCrossEnabled ? '交叉账号:已开启' : '交叉账号:已关闭'
        this.testProcess = this.isMLEnabled ? '机器学习:已开启' : '机器学习:已关闭'
        // this.host = this.getBaseUrl()

        this.page = this.getCache(this.server, 'page', this.page)
        this.count = this.getCache(this.server, 'count', this.count)

        this.caseGroupPage = this.getCache(this.server, 'caseGroupPage', this.caseGroupPage)
        this.caseGroupCount = this.getCache(this.server, 'caseGroupCount', this.caseGroupCount)
        this.caseGroupSearch = this.getCache(this.server, 'caseGroupSearch', this.caseGroupSearch)
        this.caseGroupPages = this.getCache(this.server, 'caseGroupPages', this.caseGroupPages)
        this.caseGroupCounts = this.getCache(this.server, 'caseGroupCounts', this.caseGroupCounts)
        this.caseGroupSearches = this.getCache(this.server, 'caseGroupSearches', this.caseGroupSearches)

        this.testCasePage = this.getCache(this.server, 'testCasePage', this.testCasePage)
        this.testCaseCount = this.getCache(this.server, 'testCaseCount', this.testCaseCount)
        this.testCasePages = this.getCache(this.server, 'testCasePages', this.testCasePages)
        this.testCaseCounts = this.getCache(this.server, 'testCaseCounts', this.testCaseCounts)
        this.testCaseSearches = this.getCache(this.server, 'testCaseSearches', this.testCaseSearches)

        this.randomPage = this.getCache(this.server, 'randomPage', this.randomPage)
        this.randomCount = this.getCache(this.server, 'randomCount', this.randomCount)
        this.randomSubPage = this.getCache(this.server, 'randomSubPage', this.randomSubPage)
        this.randomSubCount = this.getCache(this.server, 'randomSubCount', this.randomSubCount)

        this.delegateId = this.getCache(this.server, 'delegateId', this.delegateId)
        this.otherEnvDelegateId = this.getCache(this.server, 'otherEnvDelegateId', this.otherEnvDelegateId)

        CodeUtil.thirdPartyApiMap = this.getCache(this.thirdParty, 'thirdPartyApiMap')
      } catch (e) {
        console.log('created  try { ' +
          '\nthis.User = this.getCache(this.server, User, {})' +
          '\n} catch (e) {\n' + e.message)
      }

      try {
        var accounts = this.accounts
        var num = accounts == null ? 0 : accounts.length
        for (var i = -1; i <= num; i++) {
          this.resetCount(this.getSummary(i), false, false, i)
        }
      } catch (e) {
        console.log('created  try { ' +
          '\nthis.User = this.getCache(this.server, User, {})' +
          '\n} catch (e) {\n' + e.message)
      }

      //无效，只能在index里设置 vUrl.value = this.getCache('', 'URL_BASE')

      this.listHistory()
//      if (this.isScriptShow) {
        this.changeScriptType()
        this.listScript()
//      }

      var isLoggedIn = this.User != null && this.User.id != null && this.User.id > 0
      if (isLoggedIn) {
          this.listProjectHost()

          if (this.caseShowType != 1 && this.casePaths.length <= 0 && this.caseGroups.length <= 0) {
            this.selectCaseGroup(-1, null)
          }
      }

      var rawReq = getRequestFromURL()
      if (rawReq == null || (StringUtil.isEmpty(rawReq.type, true) && StringUtil.isEmpty(rawReq.reportId, true))) {
        this.transfer()

        if (isLoggedIn) {
          setTimeout(function () {
            App.showTestCase(true, false)  // 本地历史仍然要求登录  this.User == null || this.User.id == null)
          }, 1000)
        }
      }
      else {
        setTimeout(function () {
          isSingle = ! isSingle

          var hasTestArg = false  // 避免 http://localhost:63342/APIAuto/index.html?_ijt=fh8di51h7qip2d1s3r3bqn73nt 这种无意义参数
          if (StringUtil.isNotEmpty(rawReq.method, true)) {
            hasTestArg = true
            App.method = StringUtil.toUpperCase(rawReq.method, true)
            if (App.methods == null) {
               App.methods = [App.method]
            }
            else if (App.methods.indexOf(App.method) < 0) {
              App.methods.push(App.method)
            }
          }
          if (StringUtil.isNotEmpty(rawReq.type, true)) {
            hasTestArg = true
            // App.type = StringUtil.toUpperCase(rawReq.type, true)
            // if (App.types == null) {
            //    App.types = [App.type]
            // }
            // else if (App.types.indexOf(App.type) < 0) {
            //   App.types.push(App.type)
            // }
          }

          if (StringUtil.isNotEmpty(rawReq.url, true)) {
            hasTestArg = true
            vUrl.value = StringUtil.trim(rawReq.url)
          }

          var decode = false
          if (StringUtil.isNotEmpty(rawReq.decode, true)) {
            hasTestArg = true
            decode = rawReq.decode == 'true'
          }

          if (StringUtil.isNotEmpty(rawReq.json, true)) {
            hasTestArg = true
            vInput.value = StringUtil.trim(decode ? rawReq.json.replaceAll('\\\\', '') : rawReq.json)
          }

          if (StringUtil.isNotEmpty(rawReq.header, true)) {
            hasTestArg = true
            vHeader.value = StringUtil.trim(rawReq.header, true)
            App.isHeaderShow = true
          }

          if (StringUtil.isNotEmpty(rawReq.random, true)) {
            hasTestArg = true
            vRandom.value = StringUtil.trim(rawReq.random, true)
            App.isRandomShow = true
            App.isRandomListShow = false
          }

          if (StringUtil.isNotEmpty(rawReq.reportId, true)) {
            try {
              App.reportId = + StringUtil.trim(rawReq.reportId, true)
              if (Number.isNaN(App.reportId)) {
                throw new Error('URL query 中 reportId= 的值必须是 0 以上整数！')
              }
              App.isStatisticsEnabled = true
              App.isRandomShow = true
              App.isRandomListShow = false
              App.showTestCase(true, false)
            } catch (e) {
              App.onResponse(null, {}, e)
              alert(e)
            }
          }

          var delayTime = 0

          // URL 太长导致截断和乱码
          if (StringUtil.isNotEmpty(rawReq.setting, true)) {
            var save = rawReq.save == 'true'
            try {
              var setting = parseJSON(StringUtil.trim(rawReq.setting, true)) || {}

              if ((setting.count != null && setting.count != App.count)
                || (setting.page != null && setting.page != App.page)
                || (setting.search != null && setting.search != App.search)) {
                delayTime += Math.min(5000, 30*(setting.count) + 1000)
                App.setDoc("");
                App.getDoc(function (d) {
                  App.setDoc(d);
                })
              }

              for (var k in setting) {
                var v = k == null ? null : setting[k]
                if (v == null) {
                  continue
                }
                App[k] = v  // App.$data[k] = app[k]

                if (save) {
                  App.saveCache('', k, v)
                }
              }

              if (setting.isTestCaseShow || (setting.isRandomShow && setting.isRandomListShow)) {
                var isTest = hasTestArg
                hasTestArg = false

                App.autoTest(null, delayTime, isTest, rawReq, setting)
              }
            } catch (e) {
              log(e)
            }
          }

          App.handleTestArg(hasTestArg, rawReq, delayTime)
        }, 2000)

      }


      // 快捷键 CTRL + I 格式化 JSON
      document.addEventListener('keydown', function(event) {
        // alert(event.key) 小写字母 i 而不是 KeyI
        if (this.isFullScreen && (event.key == 'Esc' || event.keyCode == 27)) {
          this.isFullScreen = false;
          return;
        }

        var target = event.target;
        if ([vAskAI, vSearch, vTestCaseSearch, vCaseGroupSearch
          , vChainGroupSearch, vChainGroupAdd, vChainAdd
          , vAfterTotal, vAfterThreshold, vRandomKeyPath
        ].indexOf(target) >= 0) {
          return
        }

        var keyCode = event.keyCode;
        var isEnter = keyCode === 13;
        if (isEnter && target == vUrl) {
          App.send(false);
          event.preventDefault();
          return
        }
        var isDel = keyCode === 8 || keyCode === 46; // backspace 和 del
        var isChar = (keyCode >= 48 && keyCode <= 90) || (keyCode >= 106 && keyCode <= 111) || (keyCode >= 186 && keyCode <= 222);

        currentTarget = target;

        if (keyCode === 27) {  // ESC
          if (document.activeElement == vOption || App.options.length > 0) {
            App.options = [];
            if (target != null) {
              target.focus();
            }
            return;
          }
        }
        else if (keyCode === 40 || keyCode === 38) {  // 方向键 上 和 下
          if (document.activeElement == vOption || App.options.length > 0) {
            // currentTarget = target;
            if (keyCode === 38) {
              if (App.selectIndex >= 0) {
                App.selectIndex --
                App.selectInput(App.selectIndex < 0 ? null : App.options[App.selectIndex], App.selectIndex)
              }
            } else if (App.selectIndex < App.options.length) {
              App.selectIndex ++
              App.selectInput(App.selectIndex >= App.options.length ? null : App.options[App.selectIndex], App.selectIndex)
            }

            // var options = document.activeElement == vOption || App.options.length > 0 ? App.options : null; // vOption.options : null;
            // if (options != null) {
            //   for (var i = 0; i < options.length; i++) {
            //     var opt = options[i]
            //     if (opt != null && (opt.selected || i == App.selectIndex)) {
            //       if (keyCode === 38) {
            //         if (i > 0) {
            //           opt.selected = false
            //           options[i - 1].selected = true
            //           App.selectInput(App.options[i - 1], i - 1)
            //         }
            //       } else {
            //         if (i < options.length - 1) {
            //           opt.selected = false
            //           options[i + 1].selected = true
            //           App.selectInput(App.options[i + 1], i + 1)
            //         }
            //       }
            //
            //       break
            //     }
            //   }

            event.preventDefault();
            return;
            // }
          }
        }
        else if (isEnter || isDel) { // enter || delete
          if (document.activeElement == vOption || App.options.length > 0) { // hasFocus is undefined   vOption.hasFocus()) {

            var options = vOption.options || App.options
            if (options != null && options.length > 0) {
              if (isDel) {
                var selectionStart = target.selectionStart;
                var selectionEnd = target.selectionEnd;

                var text = StringUtil.get(target.value);
                var before = text.substring(0, selectionStart);
                var after = text.substring(selectionEnd);

                App.showOptions(target, text, before, after);
              }
              else {
                event.preventDefault();

                for (var i = 0; i < options.length; i++) {
                  var opt = options[i]
                  if (opt != null && (opt.selected || i == App.selectIndex)) {
                    // currentTarget = target;
                    App.selectInput(App.options[i], i, true);
                    return;
                  }
                }

                App.selectIndex - 1;
                App.options = [];
              }
            }

            return;
          }

          if (target == vUrl) {
          }
          else if (target != null && target != vOption) {
            var selectionStart = target.selectionStart;
            var selectionEnd = target.selectionEnd;

            var text = StringUtil.get(target.value);
            var before = text.substring(0, selectionStart);
            var after = text.substring(selectionEnd);

            var firstIndex = isEnter ? after.indexOf('\n') : -1;
            var firstLine = firstIndex <= 0 ? '' : after.substring(0, firstIndex);
            var tfl = firstLine.trimLeft();

            var hasRight = tfl.length > 0;
            if (isEnter && hasRight != true) {
              var aft = after.substring(firstIndex + 1);
              var fi = aft.indexOf('\n');
              tfl = fi < 0 ? aft : aft.substring(0, fi);
            }

            // var lastLineStart = isEnter && tfl.length > 0 ? -1 : before.lastIndexOf('\n') + 1;
            var lastLineStart = before.lastIndexOf('\n') + 1;
            var lastLine = lastLineStart < 0 ? '' : before.substring(lastLineStart);

            var prefixEnd = 0;
            for (var i = 0; i < lastLine.length; i++) {
              if (lastLine.charAt(i).trim().length > 0) {
                if (isDel) {
                  prefixEnd = 0;
                }
                break;
              }

              prefixEnd += 1;
            }


            var prefix = prefixEnd <= 0 ? '' : lastLine.substring(0, prefixEnd);

            var isStart = false;
            var isEnd = false;
            var hasPadding = false;
            var hasComma = false;
            var isVar = false;
            var isJSON = false;
            var hasNewKey = null;
            if (isEnter) {
              isEnd = tfl.startsWith(']') || tfl.startsWith('}')
              var tll = lastLine.trimRight();
              if (target != vInput) {
                hasNewKey = ! hasRight;
              }
              else if (isEnd || hasRight || tll.endsWith('[')) {
                hasNewKey = false;
              }
              // else if (tll.indexOf('":') > 1 || tll.indexOf("':") > 1) {
              else if (tll.indexOf(':') > 1) { // (target == vInput ? 1 : 0) || (target == vScript && tll.indexOf('=') > 0)) {
                hasNewKey = true;
              }
              // else {
              //   var ind = tll.indexOf(':')
              //   if (ind > 0) {
              //     var ind2 = tll.indexOf('"')
              //   }
              // }

              var lastInd = tfl.lastIndexOf('(');
              isVar = target == vScript && (before.indexOf('{') < 0 || (lastInd > 0 && tfl.endsWith('{') && tfl.lastIndexOf(')') > lastInd));
              isJSON = target == vInput || (isVar != true && target == vScript);

              isStart = tll.endsWith('{') || tll.endsWith('[');
              hasPadding = hasRight != true && isStart;

              tll = before.trimRight();
              hasComma = isJSON && isStart != true && isEnd != true && hasRight != true && tll.endsWith(',') != true;
              if (hasComma) {
                for (var i = before.length; i >= 0; i--) {
                  if (before.charAt(i).trim().length > 0) {
                    break;
                  }

                  selectionStart -= 1;
                }

                before = tll + ',';
                selectionStart += 1;
              }

              if (hasNewKey == null) {
                hasNewKey = tll.endsWith('{');
              }

            }

            if (prefix.length > 0 || (isEnter && target != vInput)) {
              if (isEnter) {
                // if (target == vScript) {
                //   hasNewKey = false // TODO 把全局定义的 function, variable 等放到 options。 var value = fun()
                //   target.value = before + '\n' + prefix + (hasPadding ? '    ' : '')
                //     + (isEnd ? after : (hasRight ? (hasPadding ? tfl : tfl) : '') + '\n' + after.substring(firstIndex + 1)
                //     );
                //   target.selectionEnd = target.selectionStart = selectionStart + prefix.length + 1 + (hasPadding ? 4 : 0);
                // }
                // else {
                  var newText = before + '\n' + prefix + (hasPadding ? '    ' : '')
                    + (hasNewKey ? (isJSON != true ? (isVar ? 'var ' : '') : (isSingle ? "''" : '""'))
                      + (isVar ? ' = ' : ': ') + (target == vHeader ? '' : 'null') + (hasComma || isEnd || isJSON != true ? '' : ',') : '')
                    + (isEnd ? after : (hasRight ? (hasPadding ? tfl.trimLeft() : tfl) : '') + '\n' + after.substring(firstIndex + 1)
                    );
                  target.value = newText
                  if (target == vScript) { // 不这样会自动回滚
                    App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = newText
                  }

                  target.selectionEnd = target.selectionStart = selectionStart + prefix.length + (hasComma && isJSON ? 1 : 0)
                    + (hasNewKey ? 1 : 0) + (hasPadding ? 4 : 0) + (isVar ? 4 : (isJSON ? 1 : 0));
                // }
                event.preventDefault();

                if (hasNewKey) {
                  App.showOptions(target, text, before, after);
                  if (target == vInput) {
                    inputted = target.value;
                  }
                  return;
                }
              }
              else if (isDel) {
                var newStr = (selectionStart == selectionEnd ? StringUtil.get(before.substring(0, lastLineStart - 1) + ' ') : before) + after;
                target.value = newStr;
                if (target == vScript) { // 不这样会自动回滚
                  App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = newStr
                }

                target.selectionEnd = target.selectionStart = selectionStart == selectionEnd ? lastLineStart - 1 : selectionStart;
                event.preventDefault();
              }

              if (target == vInput) {
                inputted = target.value;
              }
            }
          }
        }
        else if (target != null && keyCode === 9) {  // Tab 加空格
          try {
            var selectionStart = target.selectionStart;
            var selectionEnd = target.selectionEnd;

            var text = StringUtil.get(target.value);
            var before = text.substring(0, selectionStart);
            var after = text.substring(selectionEnd);

            var ind = before.lastIndexOf('\n');
            var start = ind < 0 ? 0 : ind + 1;
            ind = after.indexOf('\n');
            var end = ind < 0 ? text.length : selectionEnd + ind - 1;

            var selection = text.substring(start, end);
            var lines = StringUtil.split(selection, '\n');

            var newStr = text.substring(0, start);

            var prefix = '    ';
            var prefixLen = prefix.length;
            for (var i = 0; i < lines.length; i ++) {
              var l = lines[i] || '';
              if (i > 0) {
                newStr += '\n';
              }

              newStr += prefix + l;
              if (i <= 0) {
                selectionStart += prefixLen;
              }
              selectionEnd += prefixLen;
            }

            newStr += text.substring(end);

            target.value = newStr;
            if (target == vScript) { // 不这样会自动回滚
              App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = newStr
            }

            event.preventDefault();
            if (target == vInput) {
              inputted = newStr;
            }

            target.selectionStart = selectionStart;
            target.selectionEnd = selectionEnd;
          } catch (e) {
            log(e)
          }
        }
        else if ((event.ctrlKey || event.metaKey) && keyCode == 83) { // Ctrl + S 保存
          App.showSave(true)
          event.preventDefault()
        }
        else if (target != null && (event.ctrlKey || event.metaKey) && ([68, 73, 191].indexOf(keyCode) >= 0 || (isChar != true && event.shiftKey != true))) {
          var selectionStart = target.selectionStart;
          var selectionEnd = target.selectionEnd;

          // 这里拿不到 clipboardData  if (keyCode === 86) {

          if (keyCode === 73) {  // Ctrl + 'I'  格式化
            try {
              if (target == vInput) {
                var json = JSON.stringify(JSON5.parse(vInput.value), null, '    ');
                vInput.value = inputted = isSingle ? App.switchQuote(json) : json;
              }
              else {
                var lines = StringUtil.split(target.value, '\n');
                var newStr = '';

                for (var i = 0; i < lines.length; i ++) {
                  var l = StringUtil.trim(lines[i]) || '';
                  if (l.startsWith('//')) {
                   continue;
                  }

                  var ind = l.lastIndexOf(' //');
                  l = ind < 0 ? l : StringUtil.trim(l.substring(0, ind));

                  if (target == vHeader || target == vRandom) {
                    ind = l.indexOf(':');
                    if (ind >= 0) {
                      var left = target == vHeader ? StringUtil.trim(l.substring(0, ind)) : l.substring(0, ind);
                      l = left + ': ' + StringUtil.trim(l.substring(ind + 1));
                    }
                  }

                  if (l.length > 0) {
                    newStr += '\n' + l;
                  }
                }

                newStr = StringUtil.trim(newStr);
                target.value = newStr;
                if (target == vScript) { // 不这样会自动回滚
                  App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = newStr
                }
              }
            } catch (e) {
              log(e)
            }
          }
          else if (target != null && keyCode === 191) {  // Ctrl + '/' 注释与取消注释
            try {
              var text = StringUtil.get(target.value);
              var before = text.substring(0, selectionStart);
              var after = text.substring(selectionEnd);

              var ind = before.lastIndexOf('\n');
              var start = ind < 0 ? 0 : ind + 1;
              ind = after.indexOf('\n');
              var end = ind < 0 ? text.length : selectionEnd + ind - 1;

              var selection = text.substring(start, end);
              var lines = StringUtil.split(selection, '\n');

              var newStr = text.substring(0, start);

              var commentSign = '//'
              var commentSignLen = commentSign.length

              for (var i = 0; i < lines.length; i ++) {
                var l = lines[i] || '';
                if (i > 0) {
                  newStr += '\n';
                }

                if (StringUtil.trim(l).startsWith(commentSign)) {
                  var ind = l.indexOf(commentSign);
                  var suffix = l.substring(ind + commentSignLen);
                  if (suffix.startsWith(' ')) {
                    suffix = suffix.substring(1);
                    if (i <= 0) {
                      selectionStart -= 1;
                    }
                    selectionEnd -= 1;
                  }

                  newStr += StringUtil.get(l.substring(0, ind)) + StringUtil.get(suffix)
                  if (i <= 0) {
                    selectionStart -= commentSignLen;
                  }
                  selectionEnd -= commentSignLen;
                }
                else {
                  newStr += commentSign + ' ' + l;
                  if (i <= 0) {
                    selectionStart += commentSignLen + 1;
                  }
                  selectionEnd += commentSignLen + 1;
                }
              }

              newStr += text.substring(end);

              target.value = newStr;
              if (target == vScript) { // 不这样会自动回滚
                App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = newStr
              }

              if (target == vInput) {
                inputted = newStr;
              }
            } catch (e) {
              log(e)
            }
          }
          else if (target != null && keyCode == 68) {  // Ctrl + 'D' 删除行
            try {
              var text = StringUtil.get(target.value);
              var before = text.substring(0, selectionStart);
              var after = text.substring(selectionEnd);

              var lastIndex = before.lastIndexOf('\n');
              var firstIndex = after.indexOf('\n');

              var newStr = (lastIndex < 0 ? '' : before.substring(0, lastIndex)) + '\n' + after.substring(firstIndex + 1);
              target.value = newStr;
              if (target == vScript) { // 不这样会自动回滚
                App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = newStr
              }

              selectionEnd = selectionStart = lastIndex + 1;
              event.preventDefault();

              if (target == vInput) {
                inputted = newStr;
              }
            } catch (e) {
              log(e)
            }
          }

          target.selectionStart = selectionStart;
          target.selectionEnd = selectionEnd;
        }
        else if (target != null && (event.shiftKey || isChar)) {
          if (isChar && App.options.length > 0) {
            var key = StringUtil.get(event.key);

            var selectionStart = target.selectionStart;
            var selectionEnd = target.selectionEnd;

            var text = StringUtil.get(target.value);
            var before = text.substring(0, selectionStart);
            var after = text.substring(selectionEnd);
            var selection = text.substring(selectionStart, selectionEnd);
            text = before + (isInputValue && selection == 'null' ? '' : selection) + key + after;

            target.value = text;
            if (target == vScript) { // 不这样会自动回滚
              App.scripts[App.scriptType][App.scriptBelongId][App.isPreScript ? 'pre' : 'post'].script = text
            }

            target.selectionStart = selectionStart;
            target.selectionEnd = (isInputValue && selection == 'null' ? selectionStart : selectionEnd) + key.length;
            event.preventDefault();

            App.showOptions(target, text, before, after, isInputValue, key);
          }

          return;
        }

        App.selectIndex = -1;
        App.options = [];
      })
    }
  }

  if (IS_BROWSER) {
    App = new Vue(App)
    window.App = App
  }
  else {
    var methods = App.methods
    if (methods instanceof Object && (methods instanceof Array == false)) {
      App = Object.assign(App, methods)
    }
    App.autoTest = App.autoTest || methods.autoTest

    var data = App.data
    if (data instanceof Object && (data instanceof Array == false)) {
      App = Object.assign(App, data)
    }

    module.exports = {getRequestFromURL, App}
  }

})()

// APIJSON >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

