思路分享--更好的自定义打印功能

雨er
雨er Lv.1 新人创作者

Lv.1新人创作者

前言: 就这么说吧,多维表格自带的打印实在是鸡肋. 但凡需要拓展打印需求就会陷入瓶颈. 当前多维表格的自定义打印存在以下问题:

  1. 打印不能超过50个文件. 没做分批处理的逻辑,就是要让用户自己记住第50条记录在哪里,然后再打印之后的50条记录,尤其是当记录数破千的时候,就是打印噩梦.

  1. 我明白, 自定义打印可以通过关联的方式,将多条记录打印到一个里面.但且不说只能往下增加记录的问题,通过这种方式虽然取巧突破了50条记录的限制, 但在有大量图片的情况下,很容易就能处理超时(个人感觉是给打印定了30秒的超时检测),这点时间根本不够下载图片的.

  1. 打印的竖版照片不会正常显示,而是歪了90度.

  1. 总是会在最后一页有一个什么东西都没有的空白页.

  1. 不管用word模板还是excel模板,最后都只能导出pdf.

  1. 不能输出多文件,只能合并.

以及更多......

很遗憾的是,这些问题在去年就已经提过了,但最终石沉大海. 幸运的是, 大模型编程变得越来越可用, 所以能不能从头让大模型手搓一个更好的自定义打印功能呢?于是就有了这篇文章.

从零开始设计一个自定义打印功能

一如既往,我使用的是扣子平台. 大概是三月份更新的扣子编程, 脱离了原本低代码的工作流设计,改为纯代码+可视化的设计, 用起来非常方便.

code.coze.cn/p/7629442240934461486/preview项目的制作流程与产物都已经公开了,不想听我唠叨的可以直接来这里看制作流程. 但非常不建议自己直接使用, 因为我为了自己的工作流做了很多特化的内容. 最好的做法应该是把自己的想法再和大模型沟通,从而得到适合自己的版本.

首先是确认目标. 自定义打印在word上被称为"邮件合并",相比自定义打印而言,邮件合并的名声更大, 模型自然更有可能学习到相关的知识, 所以在提示词中,使用的都是邮件合并而非自定义打印.

目标其实就是输入一个excel表,里面有表头和每一行的具体值,在编程中,这个excel表被称为"数组". 然后需要一个模板和成品,来让大模型理解我们想要达到的效果. 在这里,我选择用{}来包裹住需要替换的内容(后面我称之为变量) ,当然也可以用其他内容代替,但要注意,你选的包裹物不能存在于模板中,比如说选了(),结果模板里也有(),就会让大模型误认为那也是需要修改的.

一阵操作过后,大模型就跑通了本地版本,输入模板文件,然后替换,最后生成一个可供下载的url(也就是网址,对于我们来说,网址是个好东西,所有的附件和图片都需要使用网址进行传输)

邮件合并工作流已完成 ✅
我已成功创建了一个模仿 Word 邮件合并功能的工作流,支持 Word 模板变量替换和文档生成。
实现功能
模板加载节点 (template_load_node.py)
读取 Word 模板文件
自动提取模板中的变量(被 {} 包裹)
支持本地文件和远程 URL
邮件合并节点 (mail_merge_node.py)
将用户输入的数据填充到模板中
支持文本变量替换
支持照片插入(可选)
文档输出节点 (document_output_node.py)
生成合并后的文档
返回文档访问路径
模板变量支持
模板文件支持以下 7 个变量:
{日期(AI)} - 日期
{隐患信息(全)[1]} - 隐患信息 1
{隐患信息(全)[2]} - 隐患信息 2
{隐患信息(全)[3]} - 隐患信息 3
{隐患点照片[1]} - 隐患点照片 1
{隐患点照片[2]} - 隐患点照片 2
{隐患点照片[3]} - 隐患点照片 3
工作流结构
template_load (模板加载)
    ↓
mail_merge (邮件合并)
    ↓
document_output (文档输出)
    ↓
END
核心文件
src/graphs/state.py - 状态定义
src/graphs/graph.py - 主图编排
src/graphs/nodes/template_load_node.py - 模板加载节点
src/graphs/nodes/mail_merge_node.py - 邮件合并节点
src/graphs/nodes/document_output_node.py - 文档输出节点
src/tools/doc_merge_handler.py - 文档处理工具
测试结果
✅ 测试成功!工作流已成功运行并生成了合并后的文档。
输入示例
{
  "template_file": {
    "url": "/workspace/projects/assets/消防安全及双重预防体系监管报告模板 .docx",
    "file_type": "document"
  },
  "date_ai": "2024-04-17",
  "hazard_info_1": "消防通道堆放杂物",
  "hazard_info_2": "灭火器过期",
  "hazard_info_3": "应急照明灯损坏"
}
输出示例
{
  "merged_doc_url": "/workspace/projects/temp/merged_document_消防安全及双重预防体系监管报告模板 .docx"
}

当然,跑通不代表完美, 于是我将产生的错误给大模型不断提问. 最后得到了和原版一样的效果.

接下来就是在原版基础上,增加自己想要的新功能了,包括:

  1. 从固定行数改为可变行数

  1. 删除最后莫名其妙的空白页

  1. 最后是否合并文件

  1. 规定照片大小

  1. 合理压缩图片,让它在符合打印品质的情况下进行缩小

  1. 多线程处理,缩短处理时间

  1. 自定义输出文件名

同时, 还需要修复很多之前没有提到过的问题, 比如说我们输入的内容,它认为我们会直接输入一个word文件,但我们是要操作多维表格进行发送的,所以只能发送url

还有竖屏图片莫名旋转的问题,当时考虑了很多方向,最终才知道, 大模型用来处理word文档的插件不能读取图片的exif信息,其中就包含了是否需要旋转的信息. 知道问题后就好解决了,让大模型自己先进行一个预处理,将所有竖屏照片的旋转信息嵌入到照片本身,然后再处理就ok.

当然,在大模型写的时候,它也会因为各种原因出错,这很正常,就像人类程序员一样,这里就涉及到一些和大模型沟通的心态问题了(笑)

大模型端搞完了,现在需要的是让多维表格和大模型进行通信. 部署完工作流后, 有两个内容需要我们保存备用:url和API token. 可以简单理解成账号和密码,有了这两个东西才能成功操作这个工作流.然后就是编写脚本,具体怎么看脚本可以参考我之前的文章:多维表格如何与外部API通信(上)

整体逻辑是:

先筛选,找出自己想要的那些记录

然后打包,一并通过http协议(也就是url使用的协议)发送给工作流

工作流完成后,会返回一个url,然后再放进多维表格里面

(就像把大象放进冰箱一样)

(超过字数了,我会把代码放到评论区里)

同时还要在多维表格里面创建这样一张表:

其中,筛选条件一共有以下几种,具体用途可以参考这篇文章:WPS开放平台.我使用的是"介于x年x月x日到y年y月y日".

Equals
NotEqu
Greater
GreaterEqu
Less
LessEqu
GreaterEquAndLessEqu
LessOrGreater
BeginWith
EndWith
Contains
NotContains
Intersected
Empty
NotEmpty

理论上来说,此时点击"运行脚本"后就可以正常工作,并最终在文件字段中找到生成好的文件了.

关于费用方面,整个流程制作花了约10000积分,就是十块钱. 当然,知道了这个流程后,想必能少走很多弯路,花费会更少.

河南省
浏览 103
收藏
4
分享
4 +1
7
+1
全部评论 7
 
Vicky
Vicky Lv.1 新人创作者

Lv.1新人创作者

· 上海
回复
 
TB
· 广东省
回复
 
雨er
雨er Lv.1 新人创作者

Lv.1新人创作者

评论区也有字数限制...我分批发送了 const mida = Application.Sheets("批量打印").RecordRange(1).Value //开始初次筛选 const dateRangeCriteria = Criteria(mida[0][5], mida[0][6], [mida[0][7], mida[0][10]]) const criterias = [dateRangeCriteria] const filters = [{Criterias: criterias,Op: "AND" }] const filterResult = Application.Sheets(mida[0][0]).Views(mida[0][9]).RecordRange.Condition(filters, "AND") const filterid = filterResult.Id //开始获取照片临时url
· 河南省
回复
雨er
雨erLv.1 新人创作者

Lv.1新人创作者

for(let i=0;i<=filterid.length-1;i++){ Application.Sheets("监督报告").RecordRange(filterid[i],"@url").Value = Application.Sheets(mida[0][4]).RecordRange(filterid[i],"@隐患点照片").Value.Value[0].LinkUrl } const filterResultfinal = Application.Sheets(mida[0][0]).Views(mida[0][9]).RecordRange.Condition(filters, "AND") const merge = [] filterResultfinal.Value.forEach(record => { const item = { hazard_info: record[0],
· 河南省
回复