直客通榜单海报
用途
把榜单模版 + 排行榜列表 + 小程序码变成一张可直接分发的榜单海报 jpg(750×1629)。
判据:用户提供的是排行榜列表(酒店名+名次),不是单一销售额或倒计时天数。
硬规则
- 模版从
references/templates.md取远程直链下载,技能不打包图片。底图前缀必须是bangdan-。 - 替换位只有各行的酒店名称(按名次逐行对位),名次序号(皇冠/圆标)是画面固定元素。
- 行数不匹配时:用户行数 < 模版行数删多余行;> 模版行数新增行(样式一致、行距一致、不超边框)。
- 排版校正出图后必做(模型必然画错):前3名用皇冠+加粗,第4名起用圆牌+常规字重;名称水平居中。用
fix_bangdan_layout.py --names一次搞定(铺底重绘,不留残影)。仅适用于 01/02/04 号——03 号的序号是文字「TOP n」而非皇冠/圆牌,且面板是浅底深字,走下面的快速通道,不要用 fix_bangdan_layout 的序号替换逻辑。 - 二维码用真实素材合成,绝不让模型画。来源优先级:用户自备码 > 第0b步接口取的真实码 > 码位留白(不卡住出图)。没给码时先出留白版。
- 用户已提供二维码 → 就用他的码,不下载、不用接口码覆盖。第0b步仍要跑,但只作身份验证(--verify-only,不落盘任何码图)。
- 只有用户没给码时,才走完整取码并把接口码当码位素材。
- 自备码也要收酒店 ID:--hotel-id是脚本必填参数,且酒店校验先于员工校验,ID 无效只会回11102、走不到员工判断,所以验证身份必须给真实 hotelId(此处仅用于验证,不用于取码)。 - 一张成品 = 1次生成式调用。其余全是本地脚本,零积分。想重生成前先问:能不能用脚本修?
- 先问身份并验证手机号,非员工直接终止:第0步 AskUserQuestion 里一并问是否为直客通员工。
- 非直客通员工 → 立即停止,不执行本技能任何功能:不出图、不选模版、不取码、不上报。只回一句「本技能仅面向直客通员工使用,无法为你生成海报」并结束,不要再走第1步及之后。用户改口说自己是员工时,再走员工流程。
- 是直客通员工 → 必须再要「员工本人手机号(11 位)」+「酒店 ID」,跑fetch_external_qr.py验证(见第0b步)。自备码时酒店 ID 只用于身份验证(脚本参数必填、且酒店校验先于员工校验),不会用来取码。验证通过才继续出图;11101非直客通员工 → 请用户换号重试一次,换号仍失败即等同非员工,终止。员工→交付后自动跑上报脚本(不再询问)。 - 手机号只能由用户提供,且拿到之前不得推进流程:
- 索要手机号时不要给任何替代选项(不提「用样例号」「用之前验证过的号」「我先用一个号」之类),只用自由文本请用户填本人 11 位手机号。
- 用户没给出 11 位真实手机号 → 停在第0步,不执行第1步及之后的任何步骤(不下载底图、不选模版、不生成图、不取码、不上报)。仅回复一句索要手机号的话并等待。
- 严禁用样例号、历史会话里出现过的号、或任何非用户本人提供的号码去调接口或出图。技能中不保存任何示例手机号,唯一合法的号码来源就是用户本人在当前会话中填写的那一个。 - 二维码来源说明(仅接口取码时说):若码位素材来自第0b步接口取的真实码,生图结束后必须向用户说明该二维码来源:店铺装修 → 首页设置 → 页面二维码。用户自己提供二维码(自备码)时不需要说明来源,不要说。码位留白交付则两边都不涉及。
工作流
快速通道(01/02/03/04 全支持,优先走这条)
四款模版都走本地合成,跳过第 3a 步的生成式调用,一条命令出终稿。
前置:第0步身份确认 + 第0b步验证取码必须先过——非员工不进这条通道,员工没验证通过也不出图。
python <skill_dir>/scripts/make_bangdan_local.py --names "第1名,第2名,…" \
--template bangdan-03-red-gold-luxe.jpg \
[--qr <真实码.jpg>] [--hotel-logo 酒店logo.png] --out 终稿.jpg
四款的几何已经内置在脚本的 PRESETS 里(行心列表/对齐中心/字色/墨迹高/字距/擦除带/分隔线偏移/
码位圆角),按 --template 的文件名自动选择,直接传模版名即可,不要手抄另一张模版的参数。
数值来源与测量方法见 references/templates.md(01/04 为 2026-10-08 实测补齐)。
只有 01/04 的序号是皇冠/圆牌图形、行数 10 行;02/03 是 8 行(02 序号图形、03 是「TOP n」文字)。
--qr 的取值顺序:用户自备码(最高优先,不可被接口码替换)> 第0b步 --out 存下来的真实码 > 不给 --qr 出留白版。码图是原图就直接用,不要再缩放或重绘;留白版后补时单独跑一次本命令或 compose_qr.py。
理由:fix_bangdan_layout.py 最终也是用系统字体重绘每一行名称,生成式那步保不住模版原生字体,
却会把奖杯/皇冠/标题等装饰整幅重绘一遍导致画质损失。本地合成时装饰区与原模版逐像素一致。
黑版酒店 logo 落在深色 logo 区会自动转白版;全链路只在最后出一次 JPEG。
跑完仍需过第 4 步验收。
别再走的老路:make_bangdan02.py 是 02 号专用版(几何硬编码),仅作兼容保留;
新增模版或改几何请改 make_bangdan_local.py 的 PRESETS。
第0步 · 员工身份确认
出图前必须询问用户是否为直客通员工(可并入第1步的 AskUserQuestion 一次问完),身份决定后续两条路:
回答「非直客通员工」→ 在此终止。 明确告知「本技能仅面向直客通员工使用,无法为你生成海报」,不再执行后续任何步骤(不选模版、不出图、不取码、不上报),也不要替用户改用别的方式出图。用户后续改口称是员工时,再按下面的员工流程走。
回答「是直客通员工」:同一轮或紧接着再问一次,用自由文本收「员工本人手机号(11 位,用于验证)」+「酒店 ID(用于验证/取码)」。两项都要,自备码也不例外(原因见硬规则 5)。
- 手机号不要用 AskUserQuestion 的选项形式问,也不要列「用样例号 / 用之前验证过的号码」这类替代项——只问一句「请提供你的直客通员工手机号(11 位),用于接口验证」。
- 两者都是第0b步的必填项,缺一就问,不要猜、不要用任何非用户提供的号码顶替。
- 没拿到手机号 → 到此为止,不往下走第1步(见硬规则 8)。不要把「先出图、号码后补」当成可行路径。
- 同一会话里用户已给过就不再重复问;只给了酒店 ID 没给手机号,仍然按「未拿到」处理。
- 用户已自备二维码 → 仍要手机号 + 酒店 ID 做验证,但不下载接口码(见第0b步「自备码分支」)。
- 员工身份确认后,交付时自动跑第6步匿名上报(回答「是」即视为同意,不再询问)。
第0b步 · 员工手机号验证 + 取真实二维码(仅员工)
先判断码的来源,再决定怎么跑:
| 码的来源 | 本步做什么 | --qr 喂什么 |
|---|---|---|
| 用户自备码 | 跑 --verify-only 验证身份(仍需 hotel-id),不下载接口码 |
用户给的那张码 |
| 用户没给码 | 跑完整取码(不带 --verify-only),下载真实码 |
接口下载的码 |
| 接口技术类失败 | 不卡交付 | 码位留白(后补) |
自备码分支(重要):用户已经提供二维码时,禁止再用接口码替换他的码——即便接口成功返回 qrUrl,也只把它当作「身份验证通过」的信号,码位素材仍用用户自备的那张。酒店 ID 照样要传(--hotel-id 必填,且 11102 会先于员工校验返回),但它只服务于验证,不产出码图。
前置条件:--mobile 必须是第0步中用户本人提供的 11 位手机号。没拿到就别跑这一步,更不要拿样例号或其它非用户提供的号码填进去(这属于绕过验证,禁止)。
手机号是验证凭据,不是存档信息:脚本内置 AES 加密后再发送,日志只回显掩码(前 3 位 + **** + 后 4 位),不外发明文、不落盘。
脚本是纯标准库实现,不依赖 numpy/PIL:/usr/bin/python3 与托管 python 都能跑(与榜单其它脚本不同,它不强依赖 numpy,所以不必切到 ~/.workbuddy/binaries/python/envs/default/bin/python)。
# 只验证身份(用户自备码时走这条,不下载任何码图)
/usr/bin/python3 <skill_dir>/scripts/fetch_external_qr.py --hotel-id <酒店ID> --mobile <手机号> --verify-only
# 验证 + 下载真实二维码(下载的码图直接喂给快速通道或第3e步的 --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 探测确认路由已部署,返回 HTTP 200 + 业务码)。不提供预发等其它环境,也不支持切换。鉴权头/超时在同目录qr_external.config.json里改,不用改代码。 - 加密方式:AES/ECB/PKCS5Padding,密钥
xvvdRQgjnK86LPbI,密文大写十六进制,sysChannelId固定10324。脚本内置 AES-128,无需第三方加密库。
结果判定(脚本 stdout 一行 JSON,exit 0 成功 / 1 失败):
| 返回 | 判定 | 下一步 |
|---|---|---|
code=0 + qrUrl |
验证通过,是直客通员工 | 用户没给码 → 用 --out 存的图当 --qr;用户已自备码 → 忽略 qrUrl,改用自备码(不下载、不覆盖);继续出图 |
11101 非直客通员工 |
验证失败 | 换一个已开通的员工手机号重试一次;仍失败 → 等同非员工,终止技能,不出图不上报 |
11102 无自营品牌 |
酒店 ID 无效(说明接口链路是通的) | 核对 hotelId 是否为有效门店;此时还没走到员工判断,不能据此认定对方不是员工 |
11103 调用次数超限 |
触发限频 | 等周期结束;可先用用户自备码出图,不阻塞交付 |
11100 / 400 / 网络错误 |
见脚本 hint 字段 |
按提示排查;接口挂了不卡出图——码位留白交付,后补 |
两类失败要分开处理:
- 身份类(11101 非直客通员工)→ 终止技能,换号重试一次仍失败就不再出图、不上报(见硬规则 7)。
- 技术类(限频 11103、接口报错 11100/400、网络不通、网关 401/504)→ 不卡住交付,说明原因后码位留白或用用户自备码,继续走第1步。员工身份由用户自述 + 后续补验,上报照常在第6步跑。
第1步 · 收集参数
用 AskUserQuestion 问:排行榜列表(按名次排好序的酒店名)+ 身份。二维码可选,用户没提不要问。
(身份与手机号已在第0步/第0b步处理完;用户已给过的不要重复问。)
第2步 · 选模版(合集图必须先到,再提问)
硬要求:合集图与「选模版」提问必须分两轮,先图后问。禁止把 present_files 和 AskUserQuestion 放在同一条消息里并行调用——提问卡是覆盖层,会盖在消息流上方,用户在弹窗里作答时图还没渲染出来或被遮挡,表现就是「问编号的时候没给图参考」。技能包不打包图片,合集图必须现下。
2a. 下载并校验合集图(先 mkdir -p <工作目录>/refs)
curl -sS -o <工作目录>/refs/collection-bangdan-v1.jpg \
https://zhiketong-xibao-tpl-62503.app.workbuddy.host/collection-bangdan-v1.jpg
- 校验四条:HTTP 200、
content-type: image/*、体积 > 100KB、能被 PIL 打开。任何一条不过 → 走 2d 兜底,不要直接跳到提问。 - 首选域名失败换备用域名
https://zhiketong-xibao-templates.app.workbuddy.host/重试一次。
2b. 单独一轮 present_files 展示图(本回合不要再问任何问题)
present_files必须是该回合的最后一个工具调用。展示后写一句「上方是榜单 4 款模版合集,请告诉我编号」,停下等用户回应,不要在同一条回复里继续追问。
2c. 下一轮再用 AskUserQuestion 收编号
- 选项 label 要能对着图认位置:
模版 01 <风格名>(合集图第 1 张)……,description 补一句特征。 - ⚠️
AskUserQuestion单题最多 4 个选项,4 款模版正好一题问完;若后续扩到 5 款以上要拆题。 - 用户直接报编号时不必再弹选项。
2d. 兜底(合集图拿不到时):如实告知「合集图未能加载(网络/CDN 原因)」,并把 4 款模版的「编号 + 风格名 + 一句话特征」以文字清单列出让用户选,不得静默跳过。
选定后:按 references/templates.md 直链下载底图(文件名前缀 bangdan-),Read 确认画幅是 750×1629。
第3步 · 改文案 + 校正 + 贴码
3a. 生成式改文案(唯一烧积分)
调用 ImageGen(image1=模版底图,size=模版画布,input_fidelity=high)。提示词要点:只改各行酒店名称,其余保持原样;底部白色码位保持空白;逐行列出替换映射(中文引号""标注)。
3b. 清洗生成图
python <skill_dir>/scripts/postprocess_gen.py \
--in <生成图> --out <底稿.jpg> --size 750x1629 \
[--drop-box x0,y0,x1,y1 ...]
3c. 榜单排版校正(核心步骤)
python <skill_dir>/scripts/fix_bangdan_layout.py \
--poster <底稿.jpg> --template <模版底图> \
--out <校正后.png> --header-y0 524 --header-y1 556 \
--names "第1名酒店,第2名酒店,…"
04号模版需加 --header-center 395(自动检测会报错)。
02号是「浅字深底」(白字压深色面板),必须加 --light-ink,否则行检测只能检出 1 行整块面板;同时配 --text-color 255,255,255。
03号正好相反,是「深字浅底」(蜜桃色面板 + 深红/深棕字):不要加 --light-ink、不要配白字色,
否则重绘出来是白字压在浅底上(几乎看不见)。而且 03 的序号是「TOP n」文字而非皇冠/圆牌,
replace_badges 对它无意义——03 请直接走快速通道。几何见 references/templates.md。
3d. 合成 Logo
python <skill_dir>/scripts/add_logo.py --poster <校正后.png> --out <加logo后.jpg> \
[--hotel-logo <酒店logo.png>]
3e. 二维码
# 有码(用户自备码优先;没自备码才用第0b步取到的真实二维码)
python <skill_dir>/scripts/compose_qr.py --poster <加logo后.jpg> --qr <真实码.jpg> --out <终稿.jpg>
# 没码:跳过,3d产物即为终稿(留白版)
<真实码.jpg> 就是第0b步 fetch_external_qr.py --out 落盘的那张(建议后缀写 .jpg,生产返回的就是 jpg;写成其它后缀也能存下来,合成脚本按内容识别,不受影响)。后补给码时只对留白版重跑本命令即可,不必重出整张海报。
第4步 · 验收
python <skill_dir>/scripts/verify_poster.py --poster <终稿.jpg> \
[--ghost-box 190,540,300,580]
退出码 0 = 全 PASS。有 FAIL 时先走脚本修,不要重新生成。
第5步 · 交付
把 jpg 复制到用户工作目录,present_files 发终稿路径。说明用的是哪路底图链接。
码来源告知:本图码位若用的是第0b步接口取的真实码,交付文案里附一句——「海报上的二维码取自:店铺装修 → 首页设置 → 页面二维码」。自备码或留白版不加这句。
第6步 · 匿名上报(仅直客通员工)
python3 <skill_dir>/scripts/report_invocation.py
用户在第0步已确认是员工,直接执行不再询问(即便第0b步因技术故障没取到码,上报照跑)。非员工已在第0步终止,不会走到这一步。上报失败不影响交付。
提示词要点
- 只改各行酒店名称,其余保持原样,风格一致
- 逐行列出替换映射,新名称用中文引号""标注
- 底部白色码位保持空白
- 行数适配:行数不足时删多余行(含序号与虚线),行数超出时末尾新增行
- 固定文案(标题/副标题/扫码了解活动详情)逐字保留
脚本清单
| 脚本 | 用途 |
|---|---|
make_bangdan_local.py |
四款模版通用快速通道(首选):一步到位本地合成(改名称+删多余行+双logo+贴码,零生成式积分);01/02/03/04 几何内置在 PRESETS,按 --template 自动选 |
make_bangdan02.py |
02 号专用旧版(几何硬编码),兼容保留;新需求改用上面那个 |
fix_bangdan_layout.py |
核心:名次图形校正+字重校正+名称居中重绘 |
postprocess_gen.py |
生成图清洗+归一750×1629+水印抹除 |
add_logo.py |
合成直客通logo(+可选酒店logo) |
compose_qr.py |
贴真实二维码(码位固定150×150,中心374.5,1469.5) |
verify_poster.py |
终稿验收(尺寸/水印/残影) |
fetch_external_qr.py |
外部二维码接口:员工手机号验证 + 取真实二维码(纯标准库,内置 AES)|第0b步(员工必跑) |
qr_external.config.json |
接口 url / 鉴权头 / 超时配置(固定 prod 生产,无环境切换) |
report_invocation.py |
匿名调用上报(仅直客通员工) |
环境:除 fetch_external_qr.py 是纯标准库(任意 python3 可跑)外,其余脚本需要 Pillow + numpy,
用 ~/.workbuddy/binaries/python/envs/default/bin/python。
取码接口实测要点(与喜报技能同源,别再重复踩坑)
- 唯一环境 = 生产:
POST https://ebgate.zhiketong.com/cms-api/qr/external/make。不存在预发,脚本也不提供--env;生产返回 404 说明路由被撤,通知接口方即可,不要另找地址。 504 ZuulException: timeout是服务端上游超时,别按参数/鉴权问题排查:短时间连着多次 504 且无业务码就是它。1–2 分钟内重试 2–3 次,仍全 504 就按技术类失败处理、码位留白交付,别改本地代码也别填 headers。- 401 是网关侧白名单问题,由对方放行;确需凭证时填
qr_external.config.json的headers。 - 能返回
11102说明密文解密成功(解不开才回11100),所以加密是对的,别怀疑密钥。 11102≠ 员工身份结论:酒店校验在员工校验之前,给假 hotelId 只会拿到11102,走不到11101。要验「是不是直客通员工」必须用真实有效的 hotelId。- 码型跟酒店走:同一接口换个 hotelId 就换码型 ——
189802→ 小程序码 430×430(/wxa-qr/,永久有效,zxingcpp 解不出属正常);191984→ 标准 QR 440×440(可解码);232997→ 小程序码。想知道是哪种,用托管 python 跑zxingcpp.read_barcodes()试一下即可。每家酒店单独看,别照搬上一家的结论,也别统一提示「临时码 30 天过期」。 - 别拿
qrUrl域名反推环境:picssl与release-picssl是同一套 CDN 的两个入口,不代表任何环境含义。 - 出图只能用用户本人提供的手机号:历史上出现过「用户选了填本人手机号但没传过来,就用样例号顶替」的违规操作。号码拿不到就停在原地继续要,不要换一条能走通的路。