mcpField
S
script-includes

script-includes

github.com/happy-technologies-llc开发4143 次安装2 天前更新
代码仓库

关于 script-includes

面向中高级 ServiceNow 开发者的技能,讲解如何构建可复用、可测试的服务端 Script Include,涵盖类结构、GlideAjax、继承、作用域、设计模式和 ATF 测试。

主要功能

  1. 01基于类的 Script Include 设计
  2. 02GlideAjax 客户端与服务端通信
  3. 03类扩展与继承
  4. 04作用域与全局访问控制
  5. 05Singleton、Factory 和 Strategy 模式
  6. 06ATF 单元测试

适用场景

  1. 01在业务规则和工作流间复用逻辑
  2. 02向客户端脚本开放服务端方法
  3. 03扩展平台类或自定义类
  4. 04判断何时使用 Script Include 或业务规则

MCP 相关

指南展示了使用 SN-Create-Record 等 MCP 工具在 ServiceNow 中创建 Script Include 记录。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/happy-technologies-llc/happy-platform-skills --skill script-includes

安装后在 Claude Code 中输入/script-includes确认已加载。

详细介绍

Script Include 开发

概述

Script Include 是 ServiceNow 中可复用服务器端代码的基础。本技能涵盖:

  • 基于类的 Script Include 架构
  • 可由客户端调用的 Script Include(GlideAjax 模式)
  • 扩展和继承现有类
  • 原型模式和面向对象设计
  • 实用程序库和辅助函数
  • 何时使用 Script Include 与业务规则
  • 作用域与全局范围的注意事项
  • 设计模式(Singleton、Factory、Strategy)
  • 使用 ATF 对 Script Include 进行单元测试

使用 Script Include 的场景:

  • 需要在多个业务规则、工作流或 UI 操作中复用逻辑
  • 需要进行单元测试的复杂业务逻辑
  • 客户端与服务器之间的通信(GlideAjax)
  • 扩展或重写开箱即用的功能
  • 创建服务层和 API

适用人群: 构建可维护、可测试且可复用代码的中高级 ServiceNow 开发人员。

前置条件

  • 角色: admin、scriptinclude_create 或作用域应用开发人员
  • 知识:
    • JavaScript 基础(对象、原型、闭包)
    • ServiceNow 脚本基础(GlideRecord、gs)
    • 对面向对象编程概念的基本了解
  • 访问权限: sys_script_include 表
  • 相关技能:
    • admin/script-sync - 本地开发工作流
    • admin/update-set-management - 跟踪更改
    • admin/application-scope - 作用域开发

表架构

sys_script_include 架构
Tool: SN-Get-Table-Schema
Parameters:
  table_name: sys_script_include

关键字段:

字段类型用途
name字符串类名(必须与类定义一致)
api_name字符串完整 API 名称(scope.ClassName)
script脚本JavaScript 类定义
client_callable布尔值通过 GlideAjax 公开
active布尔值启用/禁用
access选项public、package_private、private
sys_scope引用应用程序作用域
description字符串文档说明

流程

阶段 1:基于类的基本 Script Include
步骤 1.1:创建基本 Script Include

使用 MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentUtils"
    api_name: "global.IncidentUtils"
    description: "Utility methods for incident management"
    active: true
    client_callable: false
    access: "public"
    script: |
      var IncidentUtils = Class.create();
      IncidentUtils.prototype = {
          initialize: function() {
              // Constructor - called when new instance created
          },

          /**
           * Calculate incident priority based on impact and urgency
           * @param {GlideRecord} incidentGR - Incident GlideRecord
           * @returns {Number} Calculated priority (1-5)
           */
          calculatePriority: function(incidentGR) {
              var impact = parseInt(incidentGR.impact);
              var urgency = parseInt(incidentGR.urgency);

              // Priority matrix: Lower number = higher priority
              var priorityMatrix = {
                  '1-1': 1, '1-2': 2, '1-3': 3,
                  '2-1': 2, '2-2': 3, '2-3': 4,
                  '3-1': 3, '3-2': 4, '3-3': 5
              };

              var key = impact + '-' + urgency;
              return priorityMatrix[key] || 4;
          },

          /**
           * Check if incident is a P1 (Critical)
           * @param {GlideRecord} incidentGR - Incident GlideRecord
           * @returns {Boolean} True if P1
           */
          isCritical: function(incidentGR) {
              return incidentGR.priority == 1;
          },

          /**
           * Get assignment group based on category
           * @param {String} category - Incident category
           * @returns {String} sys_id of assignment group
           */
          getAssignmentGroup: function(category) {
              var groupMapping = {
                  'network': 'Network Support',
                  'hardware': 'Hardware Support',
                  'software': 'Software Support',
                  'database': 'Database Team'
              };

              var groupName = groupMapping[category] || 'Service Desk';

              var gr = new GlideRecord('sys_user_group');
              gr.addQuery('name', groupName);
              gr.query();

              if (gr.next()) {
                  return gr.sys_id.toString();
              }
              return '';
          },

          type: 'IncidentUtils'
      };
步骤 1.2:了解类结构

Class.create() 模式:

var ClassName = Class.create();  // Create class constructor
ClassName.prototype = {          // Define prototype (methods)

    initialize: function(param1, param2) {
        // Constructor - runs when 'new ClassName(p1, p2)' called
        this.param1 = param1;
        this.param2 = param2;
    },

    methodOne: function() {
        // Instance method - has access to 'this'
        return this.param1;
    },

    methodTwo: function(arg) {
        // Another instance method
        return arg + this.param2;
    },

    type: 'ClassName'  // Required: Must match class name
};

在业务规则或其他脚本中使用:

// Create instance
var utils = new IncidentUtils();

// Call methods
var priority = utils.calculatePriority(current);
var isCritical = utils.isCritical(current);
var groupId = utils.getAssignmentGroup(current.category);
步骤 1.3:构造函数参数

带构造函数的 Script Include:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentHandler"
    description: "Handles incident operations with configurable options"
    active: true
    client_callable: false
    script: |
      var IncidentHandler = Class.create();
      IncidentHandler.prototype = {
          /**
           * Initialize handler with incident record
           * @param {GlideRecord} incidentGR - The incident to handle
           * @param {Object} options - Configuration options
           */
          initialize: function(incidentGR, options) {
              this.incident = incidentGR;
              this.options = options || {};
              this.log = new GSLog('com.company.incident', 'IncidentHandler');

              // Default options
              this.autoAssign = this.options.autoAssign !== false;
              this.sendNotifications = this.options.sendNotifications !== false;
          },

          /**
           * Process the incident based on its state
           * @returns {Boolean} Success status
           */
          process: function() {
              if (!this.incident || !this.incident.isValidRecord()) {
                  this.log.error('Invalid incident record');
                  return false;
              }

              if (this.autoAssign && this._needsAssignment()) {
                  this._assignToGroup();
              }

              if (this.sendNotifications) {
                  this._notifyStakeholders();
              }

              return true;
          },

          // Private method (convention: prefix with underscore)
          _needsAssignment: function() {
              return this.incident.assignment_group.nil();
          },

          _assignToGroup: function() {
              var utils = new IncidentUtils();
              var groupId = utils.getAssignmentGroup(this.incident.category);
              if (groupId) {
                  this.incident.assignment_group = groupId;
              }
          },

          _notifyStakeholders: function() {
              // Notification logic
              gs.eventQueue('incident.processed', this.incident);
          },

          type: 'IncidentHandler'
      };

用法:

// With options
var handler = new IncidentHandler(current, {
    autoAssign: true,
    sendNotifications: false
});
handler.process();

// Default options
var handler2 = new IncidentHandler(current);
handler2.process();
阶段 2:可由客户端调用的 Script Include(GlideAjax)
步骤 2.1:创建可由客户端调用的 Script Include

可由客户端调用的 Script Include 可实现客户端脚本与服务器之间的通信。

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentAjax"
    description: "AJAX methods for incident client scripts"
    active: true
    client_callable: true
    access: "public"
    script: |
      var IncidentAjax = Class.create();
      IncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

          /**
           * Get incident details for client display
           * Called from client: new GlideAjax('IncidentAjax').addParam('sysparm_name', 'getIncidentDetails')
           */
          getIncidentDetails: function() {
              var incidentId = this.getParameter('sysparm_incident_id');
              var result = {};

              var gr = new GlideRecord('incident');
              if (gr.get(incidentId)) {
                  result.number = gr.number.toString();
                  result.short_description = gr.short_description.toString();
                  result.priority = gr.priority.toString();
                  result.priority_display = gr.priority.getDisplayValue();
                  result.state = gr.state.toString();
                  result.state_display = gr.state.getDisplayValue();
                  result.assigned_to = gr.assigned_to.getDisplayValue();
                  result.assignment_group = gr.assignment_group.getDisplayValue();
              }

              return JSON.stringify(result);
          },

          /**
           * Validate if user can close incident
           * Security check performed server-side
           */
          canCloseIncident: function() {
              var incidentId = this.getParameter('sysparm_incident_id');

              var gr = new GlideRecord('incident');
              if (!gr.get(incidentId)) {
                  return 'false';
              }

              // Check if current user can close
              if (!gr.canWrite()) {
                  return 'false';
              }

              // Check incident state allows closure
              if (gr.state == 6 || gr.state == 7) {  // Already closed/cancelled
                  return 'false';
              }

              // Check if required fields are filled
              if (gr.resolution_code.nil() || gr.close_notes.nil()) {
                  return 'false';
              }

              return 'true';
          },

          /**
           * Get related incidents count
           * Used for UI display without full query
           */
          getRelatedIncidentsCount: function() {
              var callerId = this.getParameter('sysparm_caller_id');
              var excludeId = this.getParameter('sysparm_exclude_id');

              var ga = new GlideAggregate('incident');
              ga.addQuery('caller_id', callerId);
              ga.addQuery('active', true);
              if (excludeId) {
                  ga.addQuery('sys_id', '!=', excludeId);
              }
              ga.addAggregate('COUNT');
              ga.query();

              if (ga.next()) {
                  return ga.getAggregate('COUNT');
              }
              return '0';
          },

          /**
           * Get assignment groups for category (for dropdown)
           */
          getAssignmentGroups: function() {
              var category = this.getParameter('sysparm_category');
              var groups = [];

              // Get groups based on category (simplified example)
              var gr = new GlideRecord('sys_user_group');
              gr.addQuery('active', true);
              gr.addQuery('type', '!=', '');  // Has type defined
              gr.orderBy('name');
              gr.setLimit(50);
              gr.query();

              while (gr.next()) {
                  groups.push({
                      sys_id: gr.sys_id.toString(),
                      name: gr.name.toString()
                  });
              }

              return JSON.stringify(groups);
          },

          /**
           * SECURITY: Define which methods are callable
           * Methods NOT listed here cannot be called from client
           */
          isPublic: function() {
              return true;  // All methods in this class are public
          },

          type: 'IncidentAjax'
      });
步骤 2.2:客户端脚本调用 GlideAjax

客户端脚本(同步 — 避免在生产环境中使用):

// NOT RECOMMENDED - blocks UI
function getIncidentSync(incidentId) {
    var ga = new GlideAjax('IncidentAjax');
    ga.addParam('sysparm_name', 'getIncidentDetails');
    ga.addParam('sysparm_incident_id', incidentId);
    ga.getXMLWait();  // SYNCHRONOUS - blocks browser

    var answer = ga.getAnswer();
    return JSON.parse(answer);
}

客户端脚本(异步 — 推荐):

// RECOMMENDED - non-blocking
function getIncidentAsync(incidentId, callback) {
    var ga = new GlideAjax('IncidentAjax');
    ga.addParam('sysparm_name', 'getIncidentDetails');
    ga.addParam('sysparm_incident_id', incidentId);
    ga.getXMLAnswer(function(answer) {
        var result = JSON.parse(answer);
        callback(result);
    });
}

// Usage in client script
getIncidentAsync(g_form.getValue('sys_id'), function(incident) {
    console.log('Incident: ' + incident.number);
    g_form.setValue('work_notes', 'Related: ' + incident.number);
});

带错误处理的客户端脚本:

function getIncidentWithErrorHandling(incidentId) {
    var ga = new GlideAjax('IncidentAjax');
    ga.addParam('sysparm_name', 'getIncidentDetails');
    ga.addParam('sysparm_incident_id', incidentId);

    ga.getXML(function(response) {
        // Check for errors
        var answer = response.responseXML.documentElement.getAttribute('answer');

        if (!answer) {
            g_form.addErrorMessage('Error retrieving incident details');
            return;
        }

        try {
            var result = JSON.parse(answer);
            // Process result
            console.log('Got incident: ', result);
        } catch (e) {
            g_form.addErrorMessage('Error parsing response: ' + e.message);
        }
    });
}
步骤 2.3:保护可由客户端调用的方法

方法级安全性:

var SecureIncidentAjax = Class.create();
SecureIncidentAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /**
     * Public method - callable by any user
     */
    getPublicData: function() {
        return JSON.stringify({ status: 'ok' });
    },

    /**
     * Restricted method - requires specific role
     */
    getAdminData: function() {
        // Check role before processing
        if (!gs.hasRole('admin')) {
            return JSON.stringify({ error: 'Access denied' });
        }

        // Process admin-only request
        return JSON.stringify({ adminData: 'sensitive info' });
    },

    /**
     * Method requiring record-level access
     */
    updateIncident: function() {
        var incidentId = this.getParameter('sysparm_incident_id');
        var newValue = this.getParameter('sysparm_value');

        var gr = new GlideRecord('incident');
        if (!gr.get(incidentId)) {
            return JSON.stringify({ error: 'Not found' });
        }

        // Check ACL - does user have write access?
        if (!gr.canWrite()) {
            return JSON.stringify({ error: 'Write access denied' });
        }

        // Safe to update
        gr.short_description = newValue;
        gr.update();

        return JSON.stringify({ success: true });
    },

    /**
     * Control which methods are exposed
     */
    isPublic: function() {
        var methodName = this.getParameter('sysparm_name');
        var publicMethods = ['getPublicData', 'getAdminData', 'updateIncident'];
        return publicMethods.indexOf(methodName) !== -1;
    },

    type: 'SecureIncidentAjax'
});
阶段 3:扩展现有类
步骤 3.1:扩展开箱即用的 Script Include

扩展 AbstractAjaxProcessor(最常见):

var MyAjaxProcessor = Class.create();
MyAjaxProcessor.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    // Your methods here
    type: 'MyAjaxProcessor'
});

在服务器端扩展 GlideAjax:

var EnhancedIncidentUtils = Class.create();
EnhancedIncidentUtils.prototype = Object.extendsObject(IncidentUtils, {

    initialize: function(incidentGR) {
        // Call parent constructor
        IncidentUtils.prototype.initialize.call(this);
        this.incident = incidentGR;
    },

    /**
     * Override parent method with enhanced logic
     */
    calculatePriority: function(incidentGR) {
        var gr = incidentGR || this.incident;

        // Call parent implementation first
        var basePriority = IncidentUtils.prototype.calculatePriority.call(this, gr);

        // Enhance with additional logic
        if (this._isVIPCaller(gr)) {
            basePriority = Math.max(1, basePriority - 1);  // Increase priority
        }

        return basePriority;
    },

    /**
     * New method specific to enhanced class
     */
    _isVIPCaller: function(incidentGR) {
        var caller = incidentGR.caller_id;
        if (caller.nil()) {
            return false;
        }
        return caller.vip == true;
    },

    /**
     * Get full incident analysis
     */
    getAnalysis: function() {
        return {
            priority: this.calculatePriority(),
            isCritical: this.isCritical(this.incident),
            isVIP: this._isVIPCaller(this.incident)
        };
    },

    type: 'EnhancedIncidentUtils'
});
步骤 3.2:扩展平台类

扩展 RESTMessageV2:

var EnhancedRESTMessage = Class.create();
EnhancedRESTMessage.prototype = Object.extendsObject(sn_ws.RESTMessageV2, {

    initialize: function(messageName, methodName) {
        sn_ws.RESTMessageV2.prototype.initialize.call(this, messageName, methodName);
        this.log = new GSLog('com.company.rest', 'EnhancedRESTMessage');
    },

    /**
     * Execute with automatic retry on failure
     */
    executeWithRetry: function(maxRetries) {
        var retries = maxRetries || 3;
        var lastError = null;

        for (var i = 0; i < retries; i++) {
            try {
                var response = this.execute();
                if (response.getStatusCode() >= 200 && response.getStatusCode() < 300) {
                    return response;
                }
                lastError = 'HTTP ' + response.getStatusCode();
            } catch (e) {
                lastError = e.message;
                this.log.warn('Retry ' + (i + 1) + ' failed: ' + lastError);
            }

            // Wait before retry (exponential backoff)
            if (i < retries - 1) {
                gs.sleep(1000 * Math.pow(2, i));
            }
        }

        throw new Error('All retries failed: ' + lastError);
    },

    /**
     * Execute and parse JSON response
     */
    executeAndParseJSON: function() {
        var response = this.execute();
        var body = response.getBody();

        try {
            return JSON.parse(body);
        } catch (e) {
            this.log.error('Failed to parse JSON: ' + body);
            throw new Error('Invalid JSON response');
        }
    },

    type: 'EnhancedRESTMessage'
});
步骤 3.3:为自定义层级结构创建基类

基类:

var BaseServiceHandler = Class.create();
BaseServiceHandler.prototype = {

    initialize: function(tableName) {
        this.tableName = tableName;
        this.log = new GSLog('com.company.handlers', this.type);
    },

    /**
     * Get record by sys_id
     * @param {String} sysId - Record sys_id
     * @returns {GlideRecord} Record or null
     */
    getRecord: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            return gr;
        }
        return null;
    },

    /**
     * Abstract method - must be overridden
     */
    process: function(record) {
        throw new Error('process() must be implemented by subclass');
    },

    /**
     * Abstract method - must be overridden
     */
    validate: function(record) {
        throw new Error('validate() must be implemented by subclass');
    },

    /**
     * Common logging method
     */
    logAction: function(action, recordId) {
        this.log.info(action + ' on ' + this.tableName + ': ' + recordId);
    },

    type: 'BaseServiceHandler'
};

具体实现:

var IncidentServiceHandler = Class.create();
IncidentServiceHandler.prototype = Object.extendsObject(BaseServiceHandler, {

    initialize: function() {
        // Call parent constructor with table name
        BaseServiceHandler.prototype.initialize.call(this, 'incident');
    },

    /**
     * Override abstract method
     */
    process: function(record) {
        if (!this.validate(record)) {
            return false;
        }

        // Incident-specific processing
        this._assignToGroup(record);
        this._calculatePriority(record);
        this.logAction('Processed incident', record.sys_id);

        return true;
    },

    /**
     * Override abstract method
     */
    validate: function(record) {
        if (record.short_description.nil()) {
            this.log.error('Validation failed: missing short description');
            return false;
        }
        return true;
    },

    _assignToGroup: function(record) {
        // Implementation
    },

    _calculatePriority: function(record) {
        // Implementation
    },

    type: 'IncidentServiceHandler'
});
阶段 4:设计模式
步骤 4.1:Singleton 模式

当应用程序需要共享且有且仅有一个实例时使用。

var ConfigurationManager = Class.create();
ConfigurationManager.prototype = {

    initialize: function() {
        this._loadConfiguration();
    },

    _loadConfiguration: function() {
        this.config = {};

        var gr = new GlideRecord('sys_properties');
        gr.addQuery('name', 'STARTSWITH', 'com.company.');
        gr.query();

        while (gr.next()) {
            var key = gr.name.toString().replace('com.company.', '');
            this.config[key] = gr.value.toString();
        }
    },

    get: function(key) {
        return this.config[key];
    },

    set: function(key, value) {
        this.config[key] = value;

        // Persist to database
        var gr = new GlideRecord('sys_properties');
        gr.addQuery('name', 'com.company.' + key);
        gr.query();

        if (gr.next()) {
            gr.value = value;
            gr.update();
        } else {
            gr.initialize();
            gr.name = 'com.company.' + key;
            gr.value = value;
            gr.insert();
        }
    },

    type: 'ConfigurationManager'
};

/**
 * Singleton accessor
 * Usage: var config = ConfigurationManager.getInstance();
 */
ConfigurationManager.getInstance = function() {
    if (!ConfigurationManager._instance) {
        ConfigurationManager._instance = new ConfigurationManager();
    }
    return ConfigurationManager._instance;
};

// Clear singleton (for testing or refresh)
ConfigurationManager.clearInstance = function() {
    ConfigurationManager._instance = null;
};

用法:

// Get singleton instance
var config = ConfigurationManager.getInstance();

// Use configuration
var apiKey = config.get('api_key');
var timeout = config.get('timeout') || '30000';

// Update configuration
config.set('last_sync', gs.nowDateTime());
步骤 4.2:Factory 模式

当需要创建对象,但不指定确切的类时使用。

var NotificationFactory = Class.create();
NotificationFactory.prototype = {

    initialize: function() {
        // Register notification types
        this.types = {
            'email': EmailNotification,
            'sms': SMSNotification,
            'slack': SlackNotification,
            'teams': TeamsNotification
        };
    },

    /**
     * Create notification handler based on type
     * @param {String} type - Notification type
     * @param {Object} options - Configuration options
     * @returns {Object} Notification handler
     */
    create: function(type, options) {
        var NotificationClass = this.types[type.toLowerCase()];

        if (!NotificationClass) {
            throw new Error('Unknown notification type: ' + type);
        }

        return new NotificationClass(options);
    },

    /**
     * Register custom notification type
     */
    register: function(type, handlerClass) {
        this.types[type.toLowerCase()] = handlerClass;
    },

    /**
     * Get available types
     */
    getAvailableTypes: function() {
        return Object.keys(this.types);
    },

    type: 'NotificationFactory'
};

// Base notification class
var BaseNotification = Class.create();
BaseNotification.prototype = {
    initialize: function(options) {
        this.options = options || {};
    },
    send: function(recipient, message) {
        throw new Error('send() must be implemented');
    },
    type: 'BaseNotification'
};

// Email implementation
var EmailNotification = Class.create();
EmailNotification.prototype = Object.extendsObject(BaseNotification, {
    send: function(recipient, message) {
        var email = new GlideEmailOutbound();
        email.setSubject(this.options.subject || 'Notification');
        email.setFrom(this.options.from || '[email protected]');
        email.addAddress(recipient);
        email.setBody(message);
        email.send();
        return true;
    },
    type: 'EmailNotification'
});

// SMS implementation
var SMSNotification = Class.create();
SMSNotification.prototype = Object.extendsObject(BaseNotification, {
    send: function(recipient, message) {
        // SMS API integration
        var rest = new sn_ws.RESTMessageV2('SMS_API', 'send');
        rest.setStringParameterNoEscape('to', recipient);
        rest.setStringParameterNoEscape('message', message);
        var response = rest.execute();
        return response.getStatusCode() == 200;
    },
    type: 'SMSNotification'
});

用法:

var factory = new NotificationFactory();

// Create email notification
var emailHandler = factory.create('email', {
    subject: 'Important Alert',
    from: '[email protected]'
});
emailHandler.send('[email protected]', 'Your incident was updated.');

// Create SMS notification
var smsHandler = factory.create('sms', {});
smsHandler.send('+1234567890', 'P1 Alert: Check your email.');

// Use multiple notification types
var types = ['email', 'slack'];
types.forEach(function(type) {
    var handler = factory.create(type, {});
    handler.send(recipient, message);
});
步骤 4.3:Strategy 模式

当需要可互换的算法时使用。

var IncidentAssigner = Class.create();
IncidentAssigner.prototype = {

    initialize: function() {
        this.strategies = {
            'round_robin': new RoundRobinStrategy(),
            'least_busy': new LeastBusyStrategy(),
            'skills_based': new SkillsBasedStrategy(),
            'random': new RandomStrategy()
        };
        this.currentStrategy = 'round_robin';
    },

    /**
     * Set assignment strategy
     */
    setStrategy: function(strategyName) {
        if (!this.strategies[strategyName]) {
            throw new Error('Unknown strategy: ' + strategyName);
        }
        this.currentStrategy = strategyName;
    },

    /**
     * Assign incident using current strategy
     */
    assign: function(incidentGR) {
        var strategy = this.strategies[this.currentStrategy];
        var assignee = strategy.findAssignee(incidentGR);

        if (assignee) {
            incidentGR.assigned_to = assignee;
            return true;
        }
        return false;
    },

    type: 'IncidentAssigner'
};

// Strategy interface
var AssignmentStrategy = Class.create();
AssignmentStrategy.prototype = {
    findAssignee: function(incidentGR) {
        throw new Error('findAssignee() must be implemented');
    },
    type: 'AssignmentStrategy'
};

// Round Robin implementation
var RoundRobinStrategy = Class.create();
RoundRobinStrategy.prototype = Object.extendsObject(AssignmentStrategy, {
    findAssignee: function(incidentGR) {
        var groupId = incidentGR.assignment_group.toString();
        if (!groupId) return null;

        // Get last assigned user for this group
        var lastAssigned = gs.getProperty('round_robin.' + groupId, '');

        // Get all active members
        var members = [];
        var gm = new GlideRecord('sys_user_grmember');
        gm.addQuery('group', groupId);
        gm.addQuery('user.active', true);
        gm.query();

        while (gm.next()) {
            members.push(gm.user.toString());
        }

        if (members.length === 0) return null;

        // Find next user in rotation
        var lastIndex = members.indexOf(lastAssigned);
        var nextIndex = (lastIndex + 1) % members.length;
        var nextUser = members[nextIndex];

        // Save for next rotation
        gs.setProperty('round_robin.' + groupId, nextUser);

        return nextUser;
    },
    type: 'RoundRobinStrategy'
});

// Least Busy implementation
var LeastBusyStrategy = Class.create();
LeastBusyStrategy.prototype = Object.extendsObject(AssignmentStrategy, {
    findAssignee: function(incidentGR) {
        var groupId = incidentGR.assignment_group.toString();
        if (!groupId) return null;

        var leastBusy = null;
        var lowestCount = Infinity;

        var gm = new GlideRecord('sys_user_grmember');
        gm.addQuery('group', groupId);
        gm.addQuery('user.active', true);
        gm.query();

        while (gm.next()) {
            var userId = gm.user.toString();
            var count = this._getOpenIncidentCount(userId);

            if (count < lowestCount) {
                lowestCount = count;
                leastBusy = userId;
            }
        }

        return leastBusy;
    },

    _getOpenIncidentCount: function(userId) {
        var ga = new GlideAggregate('incident');
        ga.addQuery('assigned_to', userId);
        ga.addQuery('active', true);
        ga.addAggregate('COUNT');
        ga.query();

        if (ga.next()) {
            return parseInt(ga.getAggregate('COUNT'));
        }
        return 0;
    },
    type: 'LeastBusyStrategy'
});

用法:

var assigner = new IncidentAssigner();

// Configure strategy based on system property
var strategy = gs.getProperty('incident.assignment.strategy', 'round_robin');
assigner.setStrategy(strategy);

// Assign incident
if (assigner.assign(current)) {
    gs.info('Assigned to: ' + current.assigned_to.getDisplayValue());
} else {
    gs.warn('No assignee found');
}
阶段 5:作用域与全局 Script Include
步骤 5.1:全局 Script Include

特征:

  • 可在所有作用域中使用
  • 无需命名空间前缀
  • 可访问所有平台 API
  • 存在名称冲突风险
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "GlobalUtils"
    api_name: "global.GlobalUtils"
    sys_scope: "global"
    access: "public"
    script: |
      var GlobalUtils = Class.create();
      GlobalUtils.prototype = {
          initialize: function() {},

          formatDate: function(gdt) {
              // Available to all scopes
              return gdt.getDisplayValue();
          },

          type: 'GlobalUtils'
      };
步骤 5.2:作用域脚本包含

特征:

  • 隔离在应用程序作用域内
  • 添加作用域名称作为前缀(例如,x_company_app.ClassName)
  • 由 access 字段控制访问权限
  • 更安全的命名空间管理
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "ScopedUtils"
    api_name: "x_company_app.ScopedUtils"
    sys_scope: "[scoped_app_sys_id]"
    access: "public"
    script: |
      var ScopedUtils = Class.create();
      ScopedUtils.prototype = {
          initialize: function() {},

          /**
           * This method is accessible from other scopes
           * because access is 'public'
           */
          publicMethod: function() {
              return 'Accessible anywhere';
          },

          type: 'ScopedUtils'
      };
步骤 5.3:访问修饰符
访问级别作用域访问权限用例
public所有作用域共享实用工具、API
package_private仅限相同作用域内部辅助函数
private相同作用域,不支持继承受保护的实用工具

从全局作用域调用作用域脚本包含:

// From global scope, need full API name
var utils = new x_company_app.ScopedUtils();
var result = utils.publicMethod();

从作用域调用全局脚本包含:

// From scoped app, global is accessible
var globalUtils = new GlobalUtils();
var result = globalUtils.formatDate(new GlideDateTime());
阶段 6:何时使用脚本包含与业务规则
决策矩阵
场景使用脚本包含使用业务规则
可重复使用的逻辑(多个位置)是否
单个表、单个触发器否是
客户端与服务端通信是(GlideAjax)否
需要单元测试是可以,但更困难
复杂算法/计算是提取到脚本包含中
简单字段验证否是
工作流/Flow 活动是不适用
计划作业逻辑提取到脚本包含中不适用
跨表操作是可以
应避免的反模式

错误:在业务规则中重复逻辑

// Business Rule 1 on Incident
if (current.priority == 1 && current.assignment_group.nil()) {
    current.assignment_group = getDefaultGroup('incident');
}

// Business Rule 2 on Change - DUPLICATE LOGIC!
if (current.priority == 1 && current.assignment_group.nil()) {
    current.assignment_group = getDefaultGroup('change_request');
}

正确:提取到脚本包含中

// Script Include: AssignmentUtils
var AssignmentUtils = Class.create();
AssignmentUtils.prototype = {
    initialize: function() {},

    ensureAssignment: function(record, tableName) {
        if (record.priority == 1 && record.assignment_group.nil()) {
            record.assignment_group = this.getDefaultGroup(tableName);
        }
    },

    getDefaultGroup: function(tableName) {
        // Lookup logic
    },

    type: 'AssignmentUtils'
};

// Business Rule - Simple delegation
var utils = new AssignmentUtils();
utils.ensureAssignment(current, 'incident');
阶段 7:对脚本包含进行单元测试
步骤 7.1:为脚本包含创建 ATF 测试
Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_test
  data:
    name: "Test IncidentUtils - calculatePriority"
    description: "Unit tests for IncidentUtils.calculatePriority method"
    active: true
步骤 7.2:创建测试步骤

测试步骤 1:测试 P1 计算

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_step
  data:
    test: [test_sys_id]
    order: 100
    step_config: "atf_test_step_config:run_server_side_script"
    description: "Test P1 priority calculation"
    inputs:
      script: |
        (function(outputs, steps, params, stepResult, assertEqual) {
            // Arrange
            var incident = new GlideRecord('incident');
            incident.newRecord();
            incident.impact = 1;
            incident.urgency = 1;

            // Act
            var utils = new IncidentUtils();
            var priority = utils.calculatePriority(incident);

            // Assert
            assertEqual({
                name: 'Priority should be 1 for high impact + high urgency',
                shouldbe: 1,
                value: priority
            });

        })(outputs, steps, params, stepResult, assertEqual);

测试步骤 2:测试 P3 计算

Tool: SN-Create-Record
Parameters:
  table_name: sys_atf_step
  data:
    test: [test_sys_id]
    order: 200
    step_config: "atf_test_step_config:run_server_side_script"
    description: "Test P3 priority calculation"
    inputs:
      script: |
        (function(outputs, steps, params, stepResult, assertEqual) {
            // Arrange
            var incident = new GlideRecord('incident');
            incident.newRecord();
            incident.impact = 2;
            incident.urgency = 2;

            // Act
            var utils = new IncidentUtils();
            var priority = utils.calculatePriority(incident);

            // Assert
            assertEqual({
                name: 'Priority should be 3 for medium impact + medium urgency',
                shouldbe: 3,
                value: priority
            });

        })(outputs, steps, params, stepResult, assertEqual);
步骤 7.3:测试 Mock 模式

可 Mock 的脚本包含:

var IncidentService = Class.create();
IncidentService.prototype = {

    initialize: function(dependencies) {
        // Dependency injection for testability
        this.notificationService = dependencies && dependencies.notificationService
            ? dependencies.notificationService
            : new NotificationService();

        this.assignmentUtils = dependencies && dependencies.assignmentUtils
            ? dependencies.assignmentUtils
            : new AssignmentUtils();
    },

    processNewIncident: function(incidentGR) {
        // Assign
        this.assignmentUtils.autoAssign(incidentGR);

        // Notify
        if (incidentGR.priority == 1) {
            this.notificationService.notifyOnCall(incidentGR);
        }

        return true;
    },

    type: 'IncidentService'
};

使用 Mock 的 ATF 测试:

(function(outputs, steps, params, stepResult, assertEqual) {

    // Create mock notification service
    var mockNotificationService = {
        notifyCalled: false,
        lastIncident: null,
        notifyOnCall: function(incidentGR) {
            this.notifyCalled = true;
            this.lastIncident = incidentGR;
        }
    };

    // Create mock assignment utils
    var mockAssignmentUtils = {
        autoAssign: function(incidentGR) {
            incidentGR.assigned_to = 'mock_user_id';
        }
    };

    // Inject mocks
    var service = new IncidentService({
        notificationService: mockNotificationService,
        assignmentUtils: mockAssignmentUtils
    });

    // Arrange
    var incident = new GlideRecord('incident');
    incident.newRecord();
    incident.priority = 1;
    incident.short_description = 'Test incident';

    // Act
    service.processNewIncident(incident);

    // Assert
    assertEqual({
        name: 'Notification should be called for P1',
        shouldbe: true,
        value: mockNotificationService.notifyCalled
    });

    assertEqual({
        name: 'Incident should be assigned',
        shouldbe: 'mock_user_id',
        value: incident.assigned_to.toString()
    });

})(outputs, steps, params, stepResult, assertEqual);

工具使用摘要

MCP 工具
工具用途
SN-Create-Record创建新的脚本包含
SN-Update-Record更新现有脚本包含
SN-Query-Table查找脚本包含
SN-Get-Record获取脚本包含详情
SN-Get-Record获取本地开发所需的脚本和新鲜度元数据
SN-Update-Record在远程新鲜度检查后推送已审核的脚本
SN-Execute-Background-Script测试脚本包含
SN-Get-Table-Schema探索 sys_script_include 字段

Happy Platform MCP 5.1 没有自动监视或同步工具。进行本地开发时,使用 SN-Get-Record 获取 script、sys_updated_on 和 sys_mod_count;在本地编辑; 再次获取这些字段以检测冲突;然后仅使用 SN-Update-Record 推送已审核的 script 字段。每次调用都要明确指定目标 instance。

REST API
端点方法用途
/api/now/table/sys_script_includePOST创建脚本包含
/api/now/table/sys_script_include/{id}PATCH更新脚本包含
/api/now/table/sys_script_includeGET查询脚本包含

最佳实践

代码组织
  • 每个文件一个类 - 让版本控制更整洁
  • 使用有意义的名称 - 使用 CustomerOrderProcessor,而不是 Utils2
  • 保持命名一致 - 类使用 PascalCase,方法使用 camelCase
  • 私有方法 - 添加下划线前缀:_internalMethod
  • JSDoc 注释 - 为所有公共方法编写文档
性能
  • 延迟加载 - 仅在需要时加载依赖项
  • 缓存 - 缓存开销较大的查找(GlideRecord 查询)
  • 避免全局状态 - 使用实例变量或参数
  • 尽量减少 GlideRecord 查询 - 尽可能批量处理
安全性
  • 验证输入 - 切勿信任 GlideAjax 中的客户端数据
  • 检查权限 - 操作前使用 canRead()、canWrite()
  • 限制暴露范围 - 仅在必要时将方法标记为 client_callable
  • 实现 isPublic() - 将允许的方法列入白名单
测试
  • 先编写测试 - TDD 有助于改进设计
  • 使用依赖注入 - 便于模拟
  • 测试边界情况 - 空值、空字符串、边界条件
  • 保持测试快速 - 模拟开销较大的操作

故障排除

找不到脚本包含项

症状: TypeError: ClassName is not a constructor 原因: 脚本包含项未启用,或名称不匹配 解决方案:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_include
  query: name=ClassName
  fields: name,api_name,active,sys_scope.name

请确认:

  1. active 为 true
  2. name 与类定义完全一致
  3. 从正确的作用域调用
GlideAjax 返回空值

症状: getAnswer() 返回空值或 undefined 原因: 方法未返回值,或未设置为 client_callable 解决方案:

  1. 确认 client_callable 为 true
  2. 确认方法返回一个值(字符串)
  3. 检查 isPublic() 是否包含你的方法
  4. 检查系统日志中是否有服务器错误
无法扩展类

症状: 无法使用父类方法 原因: 扩展语法不正确 解决方案:

// WRONG
ChildClass.prototype = ParentClass.prototype;

// CORRECT
ChildClass.prototype = Object.extendsObject(ParentClass, {
    // child methods
    type: 'ChildClass'
});
无法访问作用域类

症状: 无法实例化作用域内的脚本包含项 原因: 访问修饰符或作用域问题 解决方案:

  1. 检查 access 字段(跨作用域访问时应为 'public')
  2. 使用完整 API 名称:new x_scope.ClassName()
  3. 确认调用代码位于允许的作用域中

示例

示例 1:完整 CRUD 服务
var CRUDService = Class.create();
CRUDService.prototype = {

    initialize: function(tableName) {
        this.tableName = tableName;
        this.log = new GSLog('com.company.crud', 'CRUDService');
    },

    create: function(data) {
        var gr = new GlideRecord(this.tableName);
        gr.initialize();

        for (var field in data) {
            if (data.hasOwnProperty(field) && gr.isValidField(field)) {
                gr.setValue(field, data[field]);
            }
        }

        var sysId = gr.insert();
        this.log.info('Created ' + this.tableName + ': ' + sysId);
        return sysId;
    },

    read: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            return this._toObject(gr);
        }
        return null;
    },

    update: function(sysId, data) {
        var gr = new GlideRecord(this.tableName);
        if (!gr.get(sysId)) {
            return false;
        }

        for (var field in data) {
            if (data.hasOwnProperty(field) && gr.isValidField(field)) {
                gr.setValue(field, data[field]);
            }
        }

        gr.update();
        this.log.info('Updated ' + this.tableName + ': ' + sysId);
        return true;
    },

    delete: function(sysId) {
        var gr = new GlideRecord(this.tableName);
        if (gr.get(sysId)) {
            gr.deleteRecord();
            this.log.info('Deleted ' + this.tableName + ': ' + sysId);
            return true;
        }
        return false;
    },

    findBy: function(query) {
        var results = [];
        var gr = new GlideRecord(this.tableName);
        gr.addEncodedQuery(query);
        gr.query();

        while (gr.next()) {
            results.push(this._toObject(gr));
        }

        return results;
    },

    _toObject: function(gr) {
        var obj = {};
        var fields = new GlideRecordUtil().getFields(gr);

        for (var i = 0; i < fields.size(); i++) {
            var field = fields.get(i);
            obj[field] = gr.getValue(field);
        }

        return obj;
    },

    type: 'CRUDService'
};
示例 2:完整 GlideAjax 服务
var FormHelperAjax = Class.create();
FormHelperAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    getUserDetails: function() {
        var userId = this.getParameter('sysparm_user_id');
        var result = { success: false };

        var gr = new GlideRecord('sys_user');
        if (gr.get(userId)) {
            result.success = true;
            result.data = {
                sys_id: gr.sys_id.toString(),
                name: gr.name.toString(),
                email: gr.email.toString(),
                department: gr.department.getDisplayValue(),
                manager: gr.manager.getDisplayValue(),
                phone: gr.phone.toString()
            };
        } else {
            result.error = 'User not found';
        }

        return JSON.stringify(result);
    },

    getCIDetails: function() {
        var ciId = this.getParameter('sysparm_ci_id');
        var result = { success: false };

        var gr = new GlideRecord('cmdb_ci');
        if (gr.get(ciId)) {
            result.success = true;
            result.data = {
                sys_id: gr.sys_id.toString(),
                name: gr.name.toString(),
                sys_class_name: gr.sys_class_name.getDisplayValue(),
                operational_status: gr.operational_status.getDisplayValue(),
                support_group: gr.support_group.getDisplayValue()
            };
        } else {
            result.error = 'CI not found';
        }

        return JSON.stringify(result);
    },

    validateEmail: function() {
        var email = this.getParameter('sysparm_email');
        var result = { valid: false };

        // Basic email regex
        var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        result.valid = emailPattern.test(email);

        if (result.valid) {
            // Check if email exists in system
            var gr = new GlideRecord('sys_user');
            gr.addQuery('email', email);
            gr.query();
            result.exists = gr.hasNext();
        }

        return JSON.stringify(result);
    },

    isPublic: function() {
        return true;
    },

    type: 'FormHelperAjax'
});

相关技能

  • admin/script-sync - 本地开发和版本控制
  • admin/update-set-management - 跟踪脚本更改
  • admin/application-scope - 作用域应用开发
  • admin/script-execution - 测试脚本
  • catalog/variable-management - 在目录中使用脚本包含项

参考资料

评论

支持 Markdown