关于 mobile-development
面向 ServiceNow 移动开发的技能,介绍移动应用、屏幕、卡片、通知、离线规则和操作的配置,并为围绕 ServiceNow 记录构建移动体验提供 ES5 示例。
主要功能
- 01移动应用和屏幕配置
- 02列表卡片设置与操作
- 03推送通知配置
- 04离线同步规则与队列处理
- 05移动端专用操作
适用场景
- 01配置 IT 支持移动应用
- 02在移动列表中展示事件记录
- 03通知用户已分配的事件
- 04通过离线数据支持工作
MCP 相关
介绍用于查询移动配置、测试移动脚本和查找配置项目的 MCP 工具。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ serac-labs/ serac --skill mobile-development安装后在 Claude Code 中输入/mobile-development确认已加载。
详细介绍
ServiceNow 移动开发
移动开发支持打造具备离线功能的原生移动体验。
移动架构
Mobile App Configuration
├── App Screens
│ ├── List Views
│ ├── Detail Views
│ └── Card Builders
├── Push Notifications
├── Offline Rules
└── Mobile Actions
关键表
| 表 | 用途 |
|---|---|
sys_sg_mobile_app | 移动应用配置 |
sys_sg_screen | 移动屏幕 |
sys_sg_card_builder | 卡片构建器 |
sys_push_notification | 推送配置 |
sys_sg_offline_rule | 离线规则 |
移动应用配置(ES5)
创建移动应用
// Create mobile app configuration (ES5 ONLY!)
var app = new GlideRecord("sys_sg_mobile_app")
app.initialize()
app.setValue("name", "IT Support")
app.setValue("description", "Mobile app for IT support tasks")
// App settings
app.setValue("active", true)
app.setValue("version", "1.0.0")
// Branding
app.setValue("primary_color", "#1976D2")
app.setValue("secondary_color", "#FFFFFF")
app.setValue("icon", "attachment_sys_id")
// Default screen
app.setValue("home_screen", homeScreenSysId)
// Roles
app.setValue("roles", "itil")
app.insert()
配置移动屏幕
// Create mobile screen (ES5 ONLY!)
var screen = new GlideRecord("sys_sg_screen")
screen.initialize()
screen.setValue("name", "My Incidents")
screen.setValue("mobile_app", mobileAppSysId)
screen.setValue("type", "list") // list, record, custom
// Data source
screen.setValue("table", "incident")
screen.setValue("filter", "assigned_to=javascript:gs.getUserID()^active=true")
// Display
screen.setValue("title", "My Incidents")
screen.setValue("icon", "list")
// Ordering
screen.setValue("order", 100)
screen.insert()
卡片构建器(ES5)
创建卡片配置
// Create card builder for list display (ES5 ONLY!)
var card = new GlideRecord("sys_sg_card_builder")
card.initialize()
card.setValue("name", "Incident Card")
card.setValue("table", "incident")
// Card layout
card.setValue("primary_field", "number")
card.setValue("secondary_field", "short_description")
card.setValue("tertiary_field", "priority")
// Additional fields
card.setValue(
"fields",
JSON.stringify([
{ field: "caller_id", label: "Caller" },
{ field: "state", label: "Status" },
{ field: "opened_at", label: "Opened" },
]),
)
// Visual indicators
card.setValue("color_field", "priority")
card.setValue(
"color_mapping",
JSON.stringify({
1: "#D32F2F", // Critical - Red
2: "#F57C00", // High - Orange
3: "#FBC02D", // Moderate - Yellow
4: "#388E3C", // Low - Green
5: "#1976D2", // Planning - Blue
}),
)
card.insert()
自定义卡片操作
// Add actions to card (ES5 ONLY!)
function addCardAction(cardSysId, actionDef) {
var action = new GlideRecord("sys_sg_card_action")
action.initialize()
action.setValue("card_builder", cardSysId)
action.setValue("label", actionDef.label)
action.setValue("icon", actionDef.icon)
action.setValue("order", actionDef.order)
// Action type
action.setValue("action_type", actionDef.type) // script, navigate, share
// Script action (ES5 ONLY!)
if (actionDef.type === "script") {
action.setValue("script", actionDef.script)
}
action.insert()
}
// Example actions
addCardAction(cardSysId, {
label: "Acknowledge",
icon: "check",
order: 100,
type: "script",
script:
"(function(gr) {\n" +
" gr.state = 2; // In Progress\n" +
' gr.work_notes = "Acknowledged via mobile";\n' +
" gr.update();\n" +
' gs.addInfoMessage("Incident acknowledged");\n' +
"})(current);",
})
推送通知(ES5)
配置推送通知
// Create push notification config (ES5 ONLY!)
var push = new GlideRecord("sys_push_notification")
push.initialize()
push.setValue("name", "High Priority Incident Assigned")
push.setValue("description", "Notify when high priority incident assigned")
// Target table and condition
push.setValue("table", "incident")
push.setValue("condition", "priority<=2^assigned_to.changes()")
// Notification content
push.setValue("title", "High Priority Incident Assigned")
push.setValue("body", "${number}: ${short_description}")
// Recipients
push.setValue("recipient_type", "field")
push.setValue("recipient_field", "assigned_to")
// Deep link
push.setValue("deep_link", true)
push.setValue("link_url", "/incident/${sys_id}")
push.setValue("active", true)
push.insert()
以编程方式发送推送通知
// Send push notification (ES5 ONLY!)
function sendPushNotification(userSysId, message) {
try {
var push = new sn_notification.PushNotification()
push.setTitle(message.title)
push.setBody(message.body)
if (message.data) {
push.setData(message.data)
}
if (message.deepLink) {
push.setDeepLink(message.deepLink)
}
push.send(userSysId)
return { success: true }
} catch (e) {
gs.error("Push notification failed: " + e.message)
return { success: false, error: e.message }
}
}
// Example
sendPushNotification(userSysId, {
title: "Task Assigned",
body: "You have a new task assigned",
deepLink: "/task/" + taskSysId,
})
离线功能(ES5)
配置离线规则
// Create offline sync rule (ES5 ONLY!)
var rule = new GlideRecord("sys_sg_offline_rule")
rule.initialize()
rule.setValue("name", "My Open Incidents")
rule.setValue("mobile_app", mobileAppSysId)
rule.setValue("table", "incident")
// Sync filter
rule.setValue("filter", "assigned_to=javascript:gs.getUserID()^active=true")
// Fields to sync
rule.setValue("fields", "number,short_description,description,priority,state,caller_id,opened_at")
// Related records
rule.setValue("include_references", true)
rule.setValue("reference_fields", "caller_id,assignment_group")
// Sync limits
rule.setValue("max_records", 100)
// Update frequency
rule.setValue("sync_frequency", "on_demand") // on_demand, periodic
rule.setValue("active", true)
rule.insert()
处理离线数据
// Check for offline changes on sync (ES5 ONLY!)
function processOfflineChanges(userId) {
var offlineQueue = new GlideRecord("sys_sg_offline_queue")
offlineQueue.addQuery("user", userId)
offlineQueue.addQuery("processed", false)
offlineQueue.orderBy("created_on")
offlineQueue.query()
var results = { processed: 0, errors: [] }
while (offlineQueue.next()) {
try {
var tableName = offlineQueue.getValue("table")
var recordSysId = offlineQueue.getValue("record")
var changes = JSON.parse(offlineQueue.getValue("changes"))
// Apply changes
var gr = new GlideRecord(tableName)
if (gr.get(recordSysId)) {
for (var field in changes) {
if (changes.hasOwnProperty(field)) {
gr.setValue(field, changes[field])
}
}
gr.update()
results.processed++
}
// Mark as processed
offlineQueue.processed = true
offlineQueue.update()
} catch (e) {
results.errors.push({
record: offlineQueue.getValue("record"),
error: e.message,
})
}
}
return results
}
移动操作(ES5)
创建移动操作
// Create mobile-specific action (ES5 ONLY!)
var action = new GlideRecord("sys_sg_action")
action.initialize()
action.setValue("name", "Scan Barcode")
action.setValue("label", "Scan Asset")
action.setValue("description", "Scan barcode to find asset")
// Action type
action.setValue("type", "native") // native, script, link
action.setValue("native_action", "barcode_scan")
// Available on
action.setValue("screens", screenSysIds)
// Callback script (ES5 ONLY!)
action.setValue(
"callback_script",
"(function(result) {\n" +
" if (!result.value) return;\n" +
" \n" +
' var asset = new GlideRecord("alm_asset");\n' +
' asset.addQuery("asset_tag", result.value);\n' +
" asset.query();\n" +
" \n" +
" if (asset.next()) {\n" +
" // Navigate to asset\n" +
' sn_mobile.navigate("record", {\n' +
' table: "alm_asset",\n' +
" sys_id: asset.getUniqueValue()\n" +
" });\n" +
" } else {\n" +
' gs.addErrorMessage("Asset not found: " + result.value);\n' +
" }\n" +
"})(scanResult);",
)
action.insert()
基于位置的操作
// Get user location for mobile (ES5 ONLY!)
// Available in mobile context
function getCurrentLocation() {
try {
var location = sn_mobile.getLocation()
return {
latitude: location.latitude,
longitude: location.longitude,
accuracy: location.accuracy,
}
} catch (e) {
return null
}
}
// Use location for nearby assets
function findNearbyAssets(latitude, longitude, radiusMeters) {
var assets = []
var gr = new GlideRecord("alm_asset")
gr.addNotNullQuery("location.latitude")
gr.query()
while (gr.next()) {
var assetLat = parseFloat(gr.location.latitude)
var assetLon = parseFloat(gr.location.longitude)
var distance = calculateDistance(latitude, longitude, assetLat, assetLon)
if (distance <= radiusMeters) {
assets.push({
sys_id: gr.getUniqueValue(),
name: gr.getDisplayValue(),
distance: Math.round(distance),
})
}
}
return assets.sort(function (a, b) {
return a.distance - b.distance
})
}
MCP 工具集成
可用工具
| 工具 | 用途 |
|---|---|
snow_query_table | 查询移动配置 |
snow_execute_script | 测试移动脚本 |
snow_artifact_manage (action='find') | 查找配置 |
示例工作流
// 1. Query mobile apps
await snow_query_table({
table: "sys_sg_mobile_app",
query: "active=true",
fields: "name,description,version,roles",
})
// 2. Get push notification configs
await snow_query_table({
table: "sys_push_notification",
query: "active=true",
fields: "name,table,condition,title",
})
// 3. Check offline rules
await snow_query_table({
table: "sys_sg_offline_rule",
query: "active=true",
fields: "name,table,filter,max_records",
})
最佳实践
- 离线优先 - 针对网络连接问题进行设计
- 精简数据 - 仅同步必要字段
- 合理推送 - 不要发送过多通知
- 原生功能 - 使用摄像头、GPS、条形码
- 卡片设计 - 让关键信息一目了然
- 性能 - 针对移动设备进行优化
- 测试 - 在实际设备上进行测试
- 仅限 ES5 - 不使用现代 JavaScript 语法
评论
支持 Markdown