关于 script-includes
面向中高级 ServiceNow 开发者的技能,讲解如何构建可复用、可测试的服务端 Script Include,涵盖类结构、GlideAjax、继承、作用域、设计模式和 ATF 测试。
主要功能
- 01基于类的 Script Include 设计
- 02GlideAjax 客户端与服务端通信
- 03类扩展与继承
- 04作用域与全局访问控制
- 05Singleton、Factory 和 Strategy 模式
- 06ATF 单元测试
适用场景
- 01在业务规则和工作流间复用逻辑
- 02向客户端脚本开放服务端方法
- 03扩展平台类或自定义类
- 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_include | POST | 创建脚本包含 |
/api/now/table/sys_script_include/{id} | PATCH | 更新脚本包含 |
/api/now/table/sys_script_include | GET | 查询脚本包含 |
最佳实践
代码组织
- 每个文件一个类 - 让版本控制更整洁
- 使用有意义的名称 - 使用
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
请确认:
active为 truename与类定义完全一致- 从正确的作用域调用
GlideAjax 返回空值
症状: getAnswer() 返回空值或 undefined
原因: 方法未返回值,或未设置为 client_callable
解决方案:
- 确认
client_callable为 true - 确认方法返回一个值(字符串)
- 检查
isPublic()是否包含你的方法 - 检查系统日志中是否有服务器错误
无法扩展类
症状: 无法使用父类方法 原因: 扩展语法不正确 解决方案:
// WRONG
ChildClass.prototype = ParentClass.prototype;
// CORRECT
ChildClass.prototype = Object.extendsObject(ParentClass, {
// child methods
type: 'ChildClass'
});
无法访问作用域类
症状: 无法实例化作用域内的脚本包含项 原因: 访问修饰符或作用域问题 解决方案:
- 检查
access字段(跨作用域访问时应为 'public') - 使用完整 API 名称:
new x_scope.ClassName() - 确认调用代码位于允许的作用域中
示例
示例 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- 在目录中使用脚本包含项