关于 document-management
document-management 是面向 ServiceNow 文档处理的技能,涵盖附件、模板、文档生成和审批等操作。它提供脚本示例,并说明如何通过工具上传、下载、列出和删除附件。
主要功能
- 01管理记录附件
- 02创建模板并生成文档
- 03从 HTML 生成 PDF
- 04处理文档审批与版本
- 05检查附件权限及限制
适用场景
- 01为 ServiceNow 记录上传或下载文件
- 02按模板生成文档或 PDF
- 03管理文档审批与版本更新
MCP 相关
说明可供 MCP 客户端使用的 ServiceNow 工具,包括附件上传、下载、查询和删除,以及表查询和脚本执行。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ serac-labs/ serac --skill document-management安装后在 Claude Code 中输入/document-management确认已加载。
详细介绍
ServiceNow 文档管理
文档管理用于处理附件、模板和文档生成。
文档架构
Record
├── Attachments (sys_attachment)
│ └── Attachment Data (sys_attachment_doc)
├── Generated Documents
└── Document Templates
主要表
| 表 | 用途 |
|---|---|
sys_attachment | 附件元数据 |
sys_attachment_doc | 附件内容 |
sys_report_template | 报告模板 |
dms_document | 文档记录 |
附件(ES5)
上传附件
// Attach file to record (ES5 ONLY!)
function attachFile(tableName, recordSysId, fileName, contentType, content) {
var attachment = new GlideSysAttachment()
// Content can be base64 encoded string
var attachmentSysId = attachment.write(tableName, recordSysId, fileName, contentType, content)
return attachmentSysId
}
// Example
var base64Content = "SGVsbG8gV29ybGQh" // Base64 encoded
attachFile("incident", incidentSysId, "notes.txt", "text/plain", base64Content)
读取附件
// Read attachment content (ES5 ONLY!)
function getAttachmentContent(attachmentSysId) {
var attachment = new GlideSysAttachment()
var content = attachment.getContent(attachmentSysId)
return content
}
// Get attachment as base64
function getAttachmentBase64(attachmentSysId) {
var attachment = new GlideSysAttachment()
var bytes = attachment.getBytes(attachmentSysId)
// Convert to base64
var base64 = GlideBase64.encode(bytes)
return base64
}
列出附件
// Get all attachments for record (ES5 ONLY!)
function getRecordAttachments(tableName, recordSysId) {
var attachments = []
var gr = new GlideRecord("sys_attachment")
gr.addQuery("table_name", tableName)
gr.addQuery("table_sys_id", recordSysId)
gr.query()
while (gr.next()) {
attachments.push({
sys_id: gr.getUniqueValue(),
file_name: gr.getValue("file_name"),
content_type: gr.getValue("content_type"),
size_bytes: gr.getValue("size_bytes"),
created_on: gr.getValue("sys_created_on"),
created_by: gr.sys_created_by.getDisplayValue(),
})
}
return attachments
}
复制附件
// Copy attachments between records (ES5 ONLY!)
function copyAttachments(sourceTable, sourceSysId, targetTable, targetSysId) {
var attachment = new GlideSysAttachment()
attachment.copy(sourceTable, sourceSysId, targetTable, targetSysId)
}
// Copy specific attachment
function copyOneAttachment(attachmentSysId, targetTable, targetSysId) {
var source = new GlideRecord("sys_attachment")
if (!source.get(attachmentSysId)) {
return null
}
var attachment = new GlideSysAttachment()
var content = attachment.getBytes(attachmentSysId)
return attachment.write(
targetTable,
targetSysId,
source.getValue("file_name"),
source.getValue("content_type"),
content,
)
}
文档模板(ES5)
创建文档模板
// Create document template (ES5 ONLY!)
var template = new GlideRecord("sys_report_template")
template.initialize()
template.setValue("name", "Incident Summary Report")
template.setValue("table", "incident")
template.setValue("description", "Summary report for incident resolution")
// Template content with placeholders
template.setValue(
"template",
"<html>\n" +
"<head><title>Incident Summary</title></head>\n" +
"<body>\n" +
"<h1>Incident Summary Report</h1>\n" +
"<h2>${number}</h2>\n" +
"<p><strong>Description:</strong> ${short_description}</p>\n" +
"<p><strong>Caller:</strong> ${caller_id.name}</p>\n" +
"<p><strong>Priority:</strong> ${priority}</p>\n" +
"<p><strong>State:</strong> ${state}</p>\n" +
"<p><strong>Resolution:</strong> ${close_notes}</p>\n" +
"<p><strong>Resolved By:</strong> ${resolved_by.name}</p>\n" +
"<p><strong>Resolved On:</strong> ${resolved_at}</p>\n" +
"</body>\n" +
"</html>",
)
template.insert()
根据模板生成文档
// Generate document from template (ES5 ONLY!)
function generateDocumentFromTemplate(templateName, recordSysId) {
// Get template
var template = new GlideRecord("sys_report_template")
if (!template.get("name", templateName)) {
return null
}
// Get record
var tableName = template.getValue("table")
var gr = new GlideRecord(tableName)
if (!gr.get(recordSysId)) {
return null
}
// Process template
var content = template.getValue("template")
content = processTemplateVariables(content, gr)
return content
}
function processTemplateVariables(template, gr) {
// Replace ${field} and ${field.property} patterns
var pattern = /\$\{([^}]+)\}/g
var match
while ((match = pattern.exec(template)) !== null) {
var fieldPath = match[1]
var value = getFieldValue(gr, fieldPath)
template = template.replace(match[0], value)
}
return template
}
function getFieldValue(gr, fieldPath) {
var parts = fieldPath.split(".")
var value = gr
for (var i = 0; i < parts.length; i++) {
if (!value) return ""
if (i < parts.length - 1) {
// Reference field - get referenced record
value = value[parts[i]].getRefRecord()
} else {
// Final field
if (value.isValidField && value.isValidField(parts[i])) {
value = value[parts[i]].getDisplayValue() || value.getValue(parts[i])
} else {
value = ""
}
}
}
return value || ""
}
PDF 生成(ES5)
生成 PDF
// Generate PDF from HTML (ES5 ONLY!)
function generatePDF(htmlContent, fileName) {
try {
var pdfCreator = new sn_pdfgeneratorutils.PDFGenerationAPI()
var pdfBytes = pdfCreator.convertToPDF(htmlContent)
// Return as base64
return {
success: true,
content: GlideBase64.encode(pdfBytes),
fileName: fileName || "document.pdf",
}
} catch (e) {
gs.error("PDF generation failed: " + e.message)
return {
success: false,
error: e.message,
}
}
}
// Generate and attach PDF to record
function generateAndAttachPDF(templateName, recordSysId, fileName) {
// Generate HTML from template
var html = generateDocumentFromTemplate(templateName, recordSysId)
if (!html) {
return { success: false, error: "Template generation failed" }
}
// Convert to PDF
var pdf = generatePDF(html, fileName)
if (!pdf.success) {
return pdf
}
// Get table name from template
var template = new GlideRecord("sys_report_template")
template.get("name", templateName)
var tableName = template.getValue("table")
// Attach PDF
var attachmentSysId = attachFile(tableName, recordSysId, fileName, "application/pdf", pdf.content)
return {
success: true,
attachment_sys_id: attachmentSysId,
}
}
文档工作流(ES5)
文档审批
// Create document for approval (ES5 ONLY!)
function submitDocumentForApproval(documentSysId, approvers) {
var doc = new GlideRecord("dms_document")
if (!doc.get(documentSysId)) {
return { success: false, message: "Document not found" }
}
// Update state
doc.setValue("state", "pending_approval")
doc.update()
// Create approval records
for (var i = 0; i < approvers.length; i++) {
var approval = new GlideRecord("sysapproval_approver")
approval.initialize()
approval.setValue("sysapproval", documentSysId)
approval.setValue("approver", approvers[i])
approval.setValue("state", "requested")
approval.insert()
}
// Notify approvers
gs.eventQueue("document.approval.requested", doc, approvers.join(","), "")
return { success: true }
}
版本控制
// Create new document version (ES5 ONLY!)
function createDocumentVersion(documentSysId, newContent, changeNotes) {
var doc = new GlideRecord("dms_document")
if (!doc.get(documentSysId)) {
return null
}
// Get current version
var currentVersion = parseInt(doc.getValue("version"), 10) || 1
var newVersion = currentVersion + 1
// Archive current version
var archive = new GlideRecord("dms_document_version")
archive.initialize()
archive.setValue("document", documentSysId)
archive.setValue("version", currentVersion)
archive.setValue("content", doc.getValue("content"))
archive.setValue("created_by", gs.getUserID())
archive.insert()
// Update document with new version
doc.setValue("version", newVersion)
doc.setValue("content", newContent)
doc.work_notes = "Version " + newVersion + ": " + changeNotes
doc.update()
return {
document_sys_id: documentSysId,
version: newVersion,
}
}
// Get document version history
function getDocumentVersions(documentSysId) {
var versions = []
var version = new GlideRecord("dms_document_version")
version.addQuery("document", documentSysId)
version.orderByDesc("version")
version.query()
while (version.next()) {
versions.push({
version: version.getValue("version"),
created_on: version.getValue("sys_created_on"),
created_by: version.sys_created_by.getDisplayValue(),
})
}
return versions
}
附件安全(ES5)
检查附件访问权限
// Check if user can access attachment (ES5 ONLY!)
function canAccessAttachment(attachmentSysId) {
var attachment = new GlideRecord("sys_attachment")
if (!attachment.get(attachmentSysId)) {
return false
}
// Check access to parent record
var parentTable = attachment.getValue("table_name")
var parentSysId = attachment.getValue("table_sys_id")
var parent = new GlideRecord(parentTable)
return parent.get(parentSysId) // Returns false if no read access
}
文件移入和移出:附件工具
以上内容都属于脚本端。脚本可以将附件从一条记录复制到另一条记录,也可以读取已有附件,但无法完成大家通常需要的两件事:将 Agent 持有的文件放入记录,或将实例中的文件取出并交给 Agent。 后台脚本无法访问 Agent 的文件系统,也无法向其返回数 MB 的字节数据。以下 6 个工具改用 /api/now/attachment。
| 工具 | 方向 | 接收内容 |
|---|---|---|
snow_upload_attachment | 输入 | content 中的 base64 |
snow_file_upload | 输入 | file_data 中的 base64——相同端点,不同参数名称 |
snow_attach_file | 输入 | 本地路径,读取并以 multipart 方式提交 |
snow_get_attachments | 输出 | table_name + table_sys_id,仅返回元数据 |
snow_file_download | 输出 | attachment_sys_id,返回 base64 + 内容类型 |
snow_delete_attachment | — | attachment_sys_id,永久删除 |
base64 或路径
snow_upload_attachment 和 snow_file_upload 调用相同端点,语义也相同;区别仅在于参数名称(content/table_sys_id 与 file_data/record_sys_id)。
两者都不会读取文件——你需要提供已有的字节数据。
snow_attach_file 才会从磁盘读取文件。它会先检查文件大小,超过 max_size_mb 就会拒绝——该值默认为 25,这是客户端保护限制,而非实例限制。实例还有自己的限制 com.glide.attachment.max_size(单位为 MB,默认值为 1024,但通常会调低),而 glide.attachment.extensions 则限制允许的文件扩展名。文件未超过 25 MB、但超过实例限制时,API 会返回 400 错误,而非由保护限制拦截。
Base64 会使数据膨胀三分之一,因此 24 MB 的文件会形成 32 MB 的请求正文。如果要上传较大的文件,优先使用 snow_attach_file——multipart 会直接发送原始字节。
读取文件
snow_get_attachments 仅返回元数据:sys_id、file_name、size_bytes、content_type。要获取内容,始终需要调用两次:
const list = await snow_get_attachments({ table_name: "incident", table_sys_id: incSysId })
const shot = list.attachments.find((a) => a.content_type.indexOf("image/") === 0)
const file = await snow_file_download({ attachment_sys_id: shot.sys_id }) // base64 + content_type
没有“下载此记录上的所有附件”这一调用。你需要自行遍历列表,并在操作前检查 size_bytes——工具结果中会包含整个文件的 base64 数据。
表限制附件时
附件访问权限并不继承父记录的 ACL,而是通过 sys_attachment 判定,有三项相互独立的限制:
glide.attachment.role— 一个实例属性,指定了可添加附件所需的角色。- 表级附件控制。 可以将某个表设置为仅允许拥有指定角色的用户添加或读取附件;通常通过
sys_attachment上、以父表为条件的sys_security_acl记录来实现。 - 父记录自身的读取 ACL。 失去对事件的读取权限,也会失去对其附件的读取权限。
这种故障表现值得注意,因为它容易造成误判。sys_attachment 和其他表一样,因此无法读取该表的调用方会得到空列表,而不是错误——如果界面中能看到记录有附件,但 snow_get_attachments 返回 [],这说明是权限问题,而不是记录没有附件。读取单个附件失败时,通常会显示未找到,而不是禁止访问,因为平台不会确认你无权查看的 sys_id 是否存在。因此,在同一记录上成功列出附件后,紧接着却收到“找不到附件”,说明两次读取的权限不同,而不是文件已被删除。不要直接认定文件已不存在;请先使用 snow_acl_explain 检查 sys_attachment。
snow_delete_attachment 会硬删除 sys_attachment 记录及其 sys_attachment_doc 分块。没有回收站,也不会将其纳入更新集——这是数据,不是配置。
MCP 工具集成
可用工具
| 工具 | 用途 |
|---|---|
snow_query_table | 查询附件和模板 |
snow_execute_script | 测试文档脚本 |
snow_artifact_manage | 查找工件(action: "find",必须提供 type) |
snow_get_attachments | 列出记录上的附件 |
snow_file_download | 获取单个附件的字节内容 |
snow_attach_file | 附加本地文件(multipart) |
snow_upload_attachment | 附加 base64 内容 |
snow_file_upload | 附加 base64 内容(替代参数名称) |
snow_delete_attachment | 永久删除附件 |
示例工作流
// 1. Get record attachments
await snow_query_table({
table: "sys_attachment",
query: "table_name=incident^table_sys_id=inc_sys_id",
fields: "file_name,content_type,size_bytes,sys_created_on",
})
// 2. Generate document
await snow_execute_script({
script: `
var html = generateDocumentFromTemplate('Incident Summary', 'inc_sys_id');
gs.info('Generated: ' + (html ? 'success' : 'failed'));
`,
})
// 3. Find document templates
await snow_query_table({
table: "sys_report_template",
query: "table=incident",
fields: "name,description,table",
})
最佳实践
- 文件类型 — 限制允许的类型
- 大小限制 — 设置最大文件大小
- 病毒扫描 — 扫描上传文件
- 访问控制 — 继承父记录的权限
- 版本控制 — 跟踪更改
- 清理 — 移除孤立文件
- 模板 — 使用模板以保持一致性
- 仅限 ES5 — 不使用现代 JavaScript 语法