mcpField
D
document-management

document-management

github.com/serac-labs文档处理77105 次安装9 周前更新
代码仓库

关于 document-management

document-management 是面向 ServiceNow 文档处理的技能,涵盖附件、模板、文档生成和审批等操作。它提供脚本示例,并说明如何通过工具上传、下载、列出和删除附件。

主要功能

  1. 01管理记录附件
  2. 02创建模板并生成文档
  3. 03从 HTML 生成 PDF
  4. 04处理文档审批与版本
  5. 05检查附件权限及限制

适用场景

  1. 01为 ServiceNow 记录上传或下载文件
  2. 02按模板生成文档或 PDF
  3. 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",
})

最佳实践

  1. 文件类型 — 限制允许的类型
  2. 大小限制 — 设置最大文件大小
  3. 病毒扫描 — 扫描上传文件
  4. 访问控制 — 继承父记录的权限
  5. 版本控制 — 跟踪更改
  6. 清理 — 移除孤立文件
  7. 模板 — 使用模板以保持一致性
  8. 仅限 ES5 — 不使用现代 JavaScript 语法

评论

支持 Markdown