直客通业务喜报
把「海报模版 + 酒店名称 + 小程序码」变成喜报 jpg(750×1629)。
硬规则
- 先定类型:倒计时海报 / 战绩喜报海报。用户已说清就不再问。
- 类型锁定模版,两套不得串用:
| 类型 | 集合图 | 底图前缀 |
|---|---|---|
| 战绩喜报 | collection-zhanji-v3.jpg |
zhanji- |
| 倒计时 | collection-countdown-v3.jpg |
countdown- |
发集合图前自检:类型=<类型>,集合图=<文件名>,底图前缀=<前缀>,三者同属一套。类型未确定不发任何图。
- 只改两处:酒店名称/标题、数字。其余画面逐字保留。
- 二维码用真实素材合成,不让生成模型画。来源优先级:用户自备码 > 第 1b 步接口取的真实码 > 码位留白(不卡住出图)。
- 用户已提供二维码 → 就用他的码,不下载、不用接口码覆盖。第 1b 步仍要跑,但只作身份验证(--verify-only,不落盘任何码图)。
- 只有用户没给码时,才走完整取码并把接口码当码位素材。
- 自备码也要收酒店 ID:--hotel-id是脚本必填参数,且酒店校验先于员工校验,ID 无效只会回11102、走不到员工判断,所以验证身份必须给真实 hotelId(此处仅用于验证,不用于取码)。 - 提示词固定格式:写「只改标题/酒店名称和数字,其余保持原样」,要改的文字用中文引号""标注,底部码位保持留白。
- 先问身份并验证手机号,非员工直接终止:第 1 步 AskUserQuestion 里一并问是否为直客通员工。
- 非直客通员工 → 立即停止,不执行本技能任何功能:不出图、不选模版、不取码、不上报。只回一句「本技能仅面向直客通员工使用,无法为你生成海报」并结束,不要再走第 2 步及之后。用户改口说自己是员工时,再走员工流程。
- 是直客通员工 → 必须再要「员工本人手机号(11 位)」+「酒店 ID」,跑fetch_external_qr.py验证(见第 1b 步)。自备码时酒店 ID 只用于身份验证(脚本参数必填、且酒店校验先于员工校验),不会用来取码。验证通过才继续出图;11101非直客通员工 → 请用户换号重试,换号仍失败即等同非员工,终止。员工→交付后自动跑上报脚本(不再询问)。 - 手机号只能由用户提供,且拿到之前不得推进流程:
- 索要手机号时不要给任何替代选项(不提「用文档样例号」「用之前验证过的号」「我先用一个号」之类),只用自由文本请用户填本人 11 位手机号。
- 用户没给出 11 位真实手机号 → 停在第 1 步,不执行第 2 步及之后的任何步骤(不下载底图、不选模版、不生成图、不取码、不上报)。仅回复一句索要手机号的话并等待。
- 严禁用文档样例号、历史会话里出现过的号、或任何非用户本人提供的号码去调接口或出图。技能中不保存任何示例手机号,唯一合法的号码来源就是用户本人在当前会话中填写的那一个。 - 二维码来源说明(仅接口取码时说):若码位素材来自第 1b 步接口取的真实码,生图结束后必须向用户说明该二维码来源:店铺装修 → 首页设置 → 页面二维码。用户自己提供二维码(自备码)时不需要说明来源,不要说。码位留白交付则两边都不涉及。
工作流
第 1 步 · 确认类型与参数
用 AskUserQuestion 一次问完:海报类型 + 是否直客通员工 + 酒店名称 + 数字(倒计时天数/销售额)。用户已说明的不再问。二维码是可选项,没提就不问。
若回答「非直客通员工」→ 在此终止。 明确告知「本技能仅面向直客通员工使用,无法为你生成海报」,不再执行后续任何步骤(不选模版、不出图、不取码、不上报),也不要替用户改用别的方式出图。用户后续改口称是员工时,再按下面的员工流程走。
若回答「是直客通员工」:同一轮或紧接着再问一次,用自由文本收「员工本人手机号(11 位,用于验证)」+「酒店 ID(用于验证/取码)」。两项都要,自备码也不例外(原因见硬规则 4)。
- 手机号不要用 AskUserQuestion 的选项形式问,也不要列「用文档样例号 / 用之前验证过的号码」这类替代项——只问一句「请提供你的直客通员工手机号(11 位),用于接口验证」。
- 缺手机号就问,不要猜、不要用任何非用户提供的号码顶替;没拿到手机号 → 到此为止,不往下走第 2 步(见硬规则 7)。不要把「先出图、号码后补」当成可行路径。
- 同一会话里用户已给过就不再重复问。
- 用户已自备二维码 → 仍要手机号 + 酒店 ID 做验证,但不下载接口码(见下面「自备码分支」)。
第 1b 步 · 员工手机号验证 + 取真实二维码(仅员工)
先判断码的来源,再决定怎么跑:
| 码的来源 | 本步做什么 | 第 3b 步 --qr 喂什么 |
|---|---|---|
| 用户自备码 | 跑 --verify-only 验证身份(仍需 hotel-id),不下载接口码 |
用户给的那张码 |
| 用户没给码 | 跑完整取码(不带 --verify-only),下载真实码 |
接口下载的码 |
| 接口技术类失败 | 不卡交付 | 码位留白(后补) |
自备码分支(重要):用户已经提供二维码时,禁止再用接口码替换他的码——即便接口成功返回 qrUrl,也只把它当作「身份验证通过」的信号,码位素材仍用用户自备的那张。酒店 ID 照样要传(--hotel-id 必填,且 11102 会先于员工校验返回),但它只服务于验证,不产出码图。
前置条件:--mobile 必须是第 1 步中用户本人提供的 11 位手机号。没拿到就别跑这一步,更不要拿文档样例号或其它非用户提供的号码填进去(这属于绕过验证,禁止)。
手机号是验证凭据,不是存档信息:脚本内置 AES 加密后再发送,日志只回显掩码(前 3 位 + **** + 后 4 位),不外发明文、不落盘。
# 只验证身份(用户自备码时走这条,不下载任何码图)
/usr/bin/python3 <skill_dir>/scripts/fetch_external_qr.py --hotel-id <酒店ID> --mobile <手机号> --verify-only
# 验证 + 下载真实二维码(下载的码图直接喂给第 3b 步的 --qr)
/usr/bin/python3 <skill_dir>/scripts/fetch_external_qr.py --hotel-id <酒店ID> --mobile <手机号> --out <qr.jpg>
- 只有一个接口地址(生产):
POST https://ebgate.zhiketong.com/cms-api/qr/external/make(2026-10-08 20:1x 探测确认路由已部署,返回 HTTP 200 + 业务码,不再是原来的 404)。不提供预发等其它环境,也不支持切换。鉴权头/超时在同目录qr_external.config.json里改,不用改代码。 - 加密方式:AES/ECB/PKCS5Padding,密钥
xvvdRQgjnK86LPbI,密文大写十六进制,sysChannelId固定10324。脚本是纯标准库实现,加密结果已与接口文档样例核对通过。
结果判定(脚本 stdout 一行 JSON,exit 0 成功 / 1 失败):
| 返回 | 判定 | 下一步 |
|---|---|---|
code=0 + qrUrl |
验证通过,是直客通员工 | 用户没给码 → 用 --out 存的 png 当第 3b 步的 --qr;用户已自备码 → 忽略 qrUrl,改用自备码(不下载、不覆盖);继续出图 |
11101 非直客通员工 |
验证失败 | 换一个已开通的员工手机号重试一次;仍失败 → 等同非员工,终止技能,不出图不上报 |
11102 无自营品牌 |
酒店 ID 无效(说明接口链路是通的) | 核对 hotelId 是否为有效门店;此时还没走到员工判断,不能据此认定对方不是员工 |
11103 调用次数超限 |
触发限频 | 等周期结束;可先用用户自备码出图,不阻塞交付 |
11100 / 400 / 网络错误 |
见脚本 hint 字段 |
按提示排查;接口挂了不卡出图——码位留白交付,后补 |
两类失败要分开处理:
- 身份类(11101 非直客通员工)→ 终止技能,换号重试一次仍失败就不再出图、不上报(见硬规则 6)。
- 技术类(限频 11103、接口报错 11100/400、网络不通、网关 401)→ 不卡住交付,说明原因后码位留白或用用户自备码,继续走第 2 步。员工身份由用户自述 + 后续补验,上报照常在第 5 步跑。
第 2 步 · 选模版(合集图必须先到,再提问)
硬要求:合集图与「选模版」提问必须分两轮,先图后问。禁止把 present_files 和 AskUserQuestion 放在同一条消息里并行调用——提问卡是覆盖层,会盖在消息流上方,用户在弹窗里作答时图还没渲染出来或被遮挡,用户看到的就是「问编号的时候没给图参考」。技能包不打包图片,合集图必须现下,所以下面 2a 的下载校验不能省。
2a. 下载并校验合集图(先 mkdir -p <工作目录>/refs)
curl -sS -o <工作目录>/refs/collection-zhanji-v3.jpg \
https://zhiketong-xibao-tpl-62503.app.workbuddy.host/collection-zhanji-v3.jpg
- 校验四条:HTTP 200、
content-type: image/*、体积 > 100KB、能被 PIL 打开。任何一条不过 → 走 2d 兜底,不要直接跳到提问。 - 首选域名失败就换备用域名
https://zhiketong-xibao-templates.app.workbuddy.host/再试一次。 - 集合图是 3662×4341、约 1.7MB 的 8 联拼版(不是 750×1629),尺寸大、体积大属正常,别据此判失败。
2b. 单独一轮 present_files 展示图(本回合不要再问任何问题)
present_files必须是该回合的最后一个工具调用,路径用 2a 下载好的绝对路径。- 展示后写一句话:「上方是{{战绩喜报/倒计时}} 8 款模版合集,请告诉我编号」,然后停下等用户回应,不要在同一条回复里继续追问编号。
2c. 下一轮再用 AskUserQuestion 收编号
- ⚠️
AskUserQuestion单题最多 4 个选项,而模版有 8 款,必须拆成两题(01–04 / 05–08),或干脆只问自由文本让用户填编号。 - 选项 label 要能对着图认位置:
模版 01 红金大气风(合集图第 1 张)、模版 02 暖红高级风(第 2 张)……,description 再补一句特征(如「深底金字横幅」)。 - 用户直接报编号(如「07」)时不必再弹选项,按
references/templates.md取对应直链即可。
2d. 兜底(合集图拿不到时):明确告诉用户「合集图未能加载(网络/CDN 原因)」,并把该类型 8 款模版的「编号 + 风格名 + 一句话特征」用文字清单列出来让用户选,不要静默跳过、也不要只回一句「请选择模版」。用户凭编号选定后照常出图。
选定编号后:按 references/templates.md 直链下载对应底图,Read 确认是正确的图(750×1629,不是 736×1616 等近似尺寸)。
第 3 步 · 生成 + 一键后处理
3a. 生成改文案的图
调用图像编辑工具(miora_edit_image 或本环境 ImageGen),必须显式传 size: "750x1629"——不传会返回 1024×1024 方图,归一后纵向拉伸变形。实测传了会回 736×1616,比例基本一致。提示词让底部码位保持留白,不要传二维码素材。
3b. 一键后处理(默认路径,1 次调用走完全部)
/usr/bin/python3 <skill_dir>/scripts/make_poster.py --gen <生成图.png> --out <终稿.jpg> \
[--qr <小程序码.png>] [--hotel-logo <酒店logo.png>] \
[--drop-box x0,y0,x1,y1 ...] [--move-box x0,y0,x1,y1,dy ...] \
[--wm-threshold 110] [--keep]
内部依次跑:抹水印 → 归一 750×1629 → 加 logo → 贴码。不给 --qr 就出码位留白版(后补给码时再跑一次 compose_qr.py 即可)。底图均为无 logo 版,本步必跑。
--qr 的取值顺序:用户自备码(最高优先,不可被接口码替换)> 第 1b 步 --out 存下来的真实码 png > 不给 --qr 出留白版。码图是原图就直接用,不要再缩放或重绘。
模版有「示范酒店」示意行时(战绩 8 张都有;倒计时 01–08 都有),用 --drop-box 抹掉、--move-box 把酒店名居中,坐标在生成图原始像素上现量。例外:倒计时 01 的示意行 北京直客通示范酒店 是「替换」而非「删除」——它上方是 限时臻礼 如约将至、下方是固定文案 双11官微促销活动,直接让模型把这一行改成真实酒店名即可,不要 --drop-box。
跑完目检右下角:水印漏检就调低 --wm-threshold(默认 110,可试 90)再跑一次,不要手工量坐标。
3c. 文字居中校正(默认不做)
名称明显偏移时优先改提示词让模型一次到位,确需校正才跑:
/usr/bin/python3 <skill_dir>/scripts/recenter_text.py --in <底稿.jpg> --out <校正后.jpg> \
--box <现量的文字包围盒> --center-x <底图设计中心x> [--center-y <底图设计中心y>] \
[--donor <干净背景行y0,y1>]
第 4 步 · 核对与交付
Read 终稿核对:酒店名称正确且居中、数字正确、固定文案未变、Logo 正确、码位干净(有码则码完整、无码则纯白)、画布 750×1629。发现问题不自动重做,说清楚由用户决定。
交付:present_files(画布路径 + copied_assets),说明用的哪路底图链接。
码来源告知:本图码位若用的是第 1b 步接口取的真实码,交付文案里附一句——「海报上的二维码取自:店铺装修 → 首页设置 → 页面二维码」。自备码或留白版不加这句。
第 5 步 · 上报(仅直客通员工)
/usr/bin/python3 <skill_dir>/scripts/report_invocation.py
用户在第 1 步已确认是员工,直接执行不再询问。非员工已在第 1 步终止,不会走到这一步。上报失败不影响交付。
提示词模板
> 这是一张竖版{{倒计时/战绩喜报}}海报。只改标题/酒店名称和数字,其余保持原样,风格一致。
>
> 1)酒店名称「{{原文字}}」改为"{{新文字}}",字体、字号、字重、颜色、字间距、居中位置与原来完全一致。
> 2){{数字替换句}}。
> 3)海报底部的纯白色圆角方形区域保持完全空白,不要在其中绘制任何图案或二维码。
>
> 以下内容必须原样保留、逐字不变:{{按模版列出的固定文案}}。
>
> 输出竖版海报,尺寸与宽高比与原图完全一致({{宽}}x{{高}}),不要裁切或扩展画面。
模版有「示范酒店」示意行时加一条:删除第二行「示范酒店」,酒店名称只保留一行,放到原两行整体的竖直居中位置。
数字替换句示例:
- 倒计时天数:画面中央的巨型金色立体数字「3」改为"2",保持原有立体缎带质感、渐变、高光与位置。
- 销售额:海报中央的巨型数字「318.6」改为"188.6",保持原有立体质感、颜色渐变、小数点位置;右侧「万」与左侧竖排「突破」保持原样。
数字格式
优先短格式(188.6 万、1280 万)。长数字串错率高,拆两次生成(先改名称再改数字)。千分位逗号建议去掉,能压缩成「万/亿」的一律压缩。
实测硬规则(踩过的坑,别再踩)
- 取码接口现状(唯一环境 = 生产,2026-10-08 定稿):
POST https://ebgate.zhiketong.com/cms-api/qr/external/make
| 时间 | 生产 ebgate |
|---|---|
| 2026-10-08 18:19 / 18:31 | 网关 401 unauthorized(网关侧白名单问题,后对方放行) |
| 18:49–18:56 | 网关放行,但应用返回 404 path=/qr/external/make(当时未部署) |
| 19:20–19:23 | 上游超时:连续 5 次 HTTP 504 ZuulException: timeout |
| 20:1x | 路由已部署:POST 任意 body 均返回 HTTP 200 + {"code":"11100","message":"生成失败,请联系管理员"}(无效密文探测,11100 说明路由存在、只是解密失败) |
| 20:20(最新) | 完整取码跑通:hotelId=189802 + 员工本人号 → code=0、qrId=321038936、qrUrl=https://picssl.zhiketong.com/wxa-qr/….jpg |
504 ZuulException: timeout是服务端上游超时,别当参数或鉴权问题排查:不是本机网络、不是客户端timeout(配置 30s,504 是网关自己返的 body)。表现特征——短时间连续多次都 504、无业务码。处理方式:- 1–2 分钟内重试 2–3 次;仍全 504 就按技术类失败处理,码位留白交付(见结果判定表
11100/网络错误一行),不要卡住出图; - 别改本地代码、别填 headers——这不是鉴权问题;
- 隔十几分钟复测,恢复后再单独跑
compose_qr.py给已交付的留白版补码,不必重出图。
- 1–2 分钟内重试 2–3 次;仍全 504 就按技术类失败处理,码位留白交付(见结果判定表
只有生产这一个环境,不存在环境切换(2026-10-08 起:预发接口已从技能中删除,脚本不再提供 --env,配置文件 env 固定 prod)。生产出 404 说明路由又被撤,通知接口方即可,不要去找别的地址。
- 之前的 401 是网关侧鉴权/白名单问题,对方放行后自动恢复,无需本地改代码或填凭证;若再遇 401,按脚本 hint 处理(填
qr_external.config.json的headers,或让接口方加 IP 白名单)。 - 生产路由已部署:20:1x 探测返回 200 +
11100,不再是之前的 404(之前是cms-api在生产上没有/qr/external/make这条路由,现已发布)。 - 排查期已排除的干扰项(别再重复试):渠道号放进
sysChannelId/channelId/X-Channel-Id/Authorization头均无效;不存在的/cms-api/*路径当时也 401,网关根路径 200「欢迎访问EB网关」。 - 加密是对的:能返回
11102(而非11100)说明手机号密文解密成功——密文解不开才会返回11100。 11102≠ 员工身份结论:酒店校验在员工校验之前,hotelId为假值(如 100、1)只会得到11102,走不到11101员工判断。要验证「是不是直客通员工」,必须给真实有效的hotelId。-
完整取码已实测通过(2026-10-08 20:20):
hotelId=189802+ 员工本人手机号 →code=0、verified=true、qrId=321038936、qrUrl=https://picssl.zhiketong.com/wxa-qr/d40eeb7d-….jpg,图片 430×430。结论:
1. 码类型由该酒店的首页配置决定,不同 hotelId 可能拿到不同类型的码(同一员工、同一时间、同一接口只差 hotelId,就得到两种码):hotelId 尺寸/位深 码类型 zxingcpp 189802 430×430 RGB 小程序码(放射状圆点 + 中间圆形酒店图 + 右下角绿色小程序图标) 解不出(专有格式,属正常,别据此判坏) 191984 440×440 灰度 标准 QR( QR Code),内容为https://open.weixin.qq.com/sns/getexpappinfo?appid=…&path=pages/ticket/custom-poster/custom-poster.html?customId=…&fs=<qrId>&iswxtpa=1#wechat-redirect可解码 想知道拿到的是哪种码,用托管 python 跑
zxingcpp.read_barcodes()试一下即可——解得出是 QR 跳转码,解不出多半是小程序码;两种都能正常贴海报,最终成败以微信实测扫码为准。
2. 别拿qrUrl域名当依据:picssl与release-picssl是同一套 CDN 的两个入口,同一张图的 uuid 换成任一域名都能下到同样字节的文件(实测d40eeb7d-….jpg两边都返 200 / 124503 字节),release-前缀不代表任何环境含义。判定只看脚本回显的env(固定prod)与请求地址是否为ebgate。
3. 生产返回的码 430×430,贴码清晰度足够,无需担心放大失真。
4.--out建议写.jpg:生产返回的源文件是 jpg,脚本按原始字节写盘、不转码,用.png后缀也能存下来且 PIL 能正常打开(按内容识别),只是扩展名与实际格式不符,容易看错。贴码时以脚本回显的saved路径为准。
- ⚠️ 出图必须用用户本人提供的手机号(硬规则 7):2026-10-08 19:xx 那次用文档样例号出图是被判为违规的——用户在选项里选了「填写本人手机号」但号码没传过来,就默认用了样例号顶替。正确做法是停在原地继续要号码,而不是换一条能走通的路。
- 码类型/时效跟酒店走:189802 → 小程序码(/wxa-qr/,永久);191984 → 标准 QR 跳转码。二者同源于「酒店首页配置」,调用方不能指定。每家酒店的码都要单独试,别照搬上一家的结论,也别统一提醒「临时码 30 天过期」——只有确认拿到的是公众号临时码时才需要提示时效。万一某张海报扫码不灵,先换用户自备的高清码,不要先去改贴码脚本(compose_qr.py用 LANCZOS 放大,实测扫码正常)。
- 解释器固定/usr/bin/python3:系统 python3 有 numpy 2.0.2 + PIL 11.3;托管 python 3.13 没装 numpy,会直接ModuleNotFoundError白跑一次。
-make_poster.py不会自建输出目录:它会先把中间产物写成<out 同目录>/_clean.png,目录不存在就FileNotFoundError挂在第 1 步。跑之前先mkdir -p <out 目录>(只给--out文件名时就是 mkdir -p 当前目录)。
- 水印阈值:postprocess_gen.py内置min(RGB) > 200必然漏检(「AI生成 / WORKBUDDY」实测最亮只有 110~130 灰阶)。make_poster.py已改成 110 并用「左侧干净背景横向延展」抹除——左右插值补底的右锚点会落在水印自己身上,抹不干净。
- 量文字包围盒用亮度不用颜色:金色阈值(R>140 & R-B>60)只圈到字芯,实际范围上下各多约 20px。用lum > 60才准;量错会导致补底只覆盖一半、原字留残影。
-recenter_text.py只认深字浅底:ink = lum < th。浅字深底模版(倒计时 02/05/06)要先ImageOps.invert()反相 → 跑校正 → 再反相回来。
- 深底淡字模版别动名称纵向位置:酒店名背后压着巨大的半透明COUNTDOWN字样,往上腾空的那条带里全是淡字笔画,无论捐赠行补底还是等高空带下移都会留重影/断笔。横向偏差 10px 内可直接交付,纵向偏高属可接受偏差。
- 浅底深字模版可放心校正纵向(倒计时 08 已验证):模型同样会把单行停在原第一行位置,实测上偏 24px。08 删掉示意行后腾空的 y≈496–530 是干净背景,正好当捐赠源。实测配方:--box 266,448,490,481 --dx -3 --dy 24 --donor 496,530,名称从中心 (378.0, 464.5) 归位到设计两行块中心 (375.0, 488.5),补底无重影、无断层。据此可推:捐赠行就取被删掉的那一行所占区间,无需另找干净带。
-recenter_text.py的墨迹判据是lum < 170,浅底深字直接可用;只有深底淡字模版才需要先ImageOps.invert()反相。
- 提示词补强:模型有「保留原文左边界向右扩字」和「单行停在第一行位置」的固定偏差。写死「共 N 个字 / 水平居中、与下方文案同一竖线 / 字号与原名称完全一致不要放大 / 竖直位置取原两行整体中心」,比泛泛说「保持一致」有效。
脚本清单
| 脚本 | 作用 | 何时单独跑 |
|---|---|---|
make_poster.py |
主编排器:抹水印→归一→加 logo→贴码,一次调用出终稿 | 每张海报必跑(第 3b 步) |
compose_qr.py |
真实码合成,自动量圆形/圆角方码位并加形状蒙版 | 留白版后补给码时 |
postprocess_gen.py |
生成图清洗 + 归一(被 make_poster 调用) | 需单独调试尺寸/抹除时 |
add_logo.py |
顶部 logo,按背景深浅自动选彩色/白色(被 make_poster 调用) | 需调 logo 位置/间距时 |
recenter_text.py |
文字居中校正(可选,默认不做) | 名称偏移且提示词改不动时 |
fetch_external_qr.py |
外部二维码接口:员工手机号验证 + 取真实二维码(纯标准库,内置 AES) | 第 1b 步(员工必跑) |
qr_external.config.json |
接口环境 / url / 鉴权头 / 超时配置(默认 prod 生产) | 需换环境或填网关 token 时 |
report_invocation.py |
匿名调用上报(仅直客通员工) | 第 5 步 |