shopify:独立站商品上架流水线
⚠️ 第一步永远是自检:
cd scripts && python3 preflight.py。它会查 config.local.json 建了没、飞书/Shopify 授权就绪没、依赖的 Shopify 应用装了没——没绿之前别跑后面的脚本(脚本本身也有守卫,缺 config 会直接拦住并指回这里)。换设备/换人第一件事就是它。
核心模型:同事 = 提供者 + 最终确认;skill = 专家,把专业活全干。skill 只在 3 种时候找人:①缺必要输入→提醒补 ②遇决策→让拍板(含逐商品库存策略) ③全弄好→确认上线(唯一的闸,没有独立"审核")。
何时用
- 同事在 Shopify 建了草稿商品,要走完整上架(内容/SEO/GEO/多语言/集合/结构化/索引)。
- 批量补齐/优化已有商品。
- 想让「换 agent、换人」都能一致跑完上架。
前提(跑之前)
- config(必做):
cp config.example.json config.local.json,在config.local.json填你的 Feishu 表app_token/table_id(从表 URL 取;★这项不进公开仓库、只在本地;换设备各自建一份★)。运行时合并 example + local。 - 授权:Shopify 走
node /home/ubuntu/fe-www.funcinating.com-next/scripts/shopify/gql.mjs(应用凭证通道 D-011;mutation 加 --allow-mutations,绝不跑 shopify store auth);Feishu 走lark-cli——默认用你当前活动 profile(你 agent 早已登录自己的飞书,config.feishu.profile留空即可,不必再写); 只有要指定别的飞书身份时才填 profile。任何 token 都别写进文件。 前提:你的飞书账号得有那几张多维表格的读写权限。 - 依赖:
shopifyCLI、lark-cli、python3;多语言深挖见飞书《多语言适配指南(复用手册)》。
实体(entity)—— 一套脚本,三个资源
本 skill 用 --entity product|collection|article 覆盖 3 类 Shopify 资源,各自一张飞书表(config.entities),字段映射在 scripts/entities.py。新增实体只在 entities.py 加一段 + config 加表。
product(商品)·collection(集合,editorial/faq/IP卡)·article(文章,news/guides)·page(页面,about/faq/政策/信任页)—— 均 pull→audit→writeback E2E 闭环- 所有脚本都接
--entity:sync_pull.py --entity collection/audit.py --entity article/sync_writeback.py --entity collection。默认 product。
步骤 0 · preflight(初始化自检)· ✅ 已脚本化 —— 每次上手先跑
- ★先问用户要哪些能力模块(onboarding 策略)★:并非人人都动代码侧。上手时 agent 先问:「你只做 Shopify 后台内容(商品/集合/文章/页面/翻译/图片/巡检),还是也做主题 UI 多语言/前端代码侧?」——按答案决定验哪些、引导填哪些 config,别让只做后台的同事去配前端仓。
- core(默认,人人要):飞书表 + Shopify 鉴权。
- theme(可选,代码侧):额外需前端仓 checkout 路径 + THEME_TOKEN(config 的
theme块;不做就整块留空)。
- 跑法:纯后台
python3 preflight.py;要主题python3 preflight.py --modules core,theme。 - 查什么:①
config.local.json建好并填了 feishu 表标识 ②四实体表 table_id 全配(product/collection/article/page)③飞书授权(能否读表)④Shopify 授权(能否连店)⑤**(选了 theme 才查)** 主题.env.local含 THEME_TOKEN +locales_dir就绪 ⑥列出依赖的 Shopify 应用让你确认已装(如 Translate & Adapt;API 查不到、须人工确认,配config.required_apps)。 - 缺就提示怎么补(建 config / 跑 lark-cli auth / 配 shopify 凭证),全 ✅ 才开始 sync_pull。新设备/换人第一件事就跑它。
字段契约(以当前飞书表字段名为准,4 桶)
- ① 镜像(Shopify→表,只读回填):Product ID(幂等键)· handle · 商品URL · 状态 · Vendor · Product Type · Shopify分类 · 变体/SKU/价/库存 · 主图
- ② 内容(双向)(表内优化→确认后写回):商品名称 · 描述EN/中(必须保存完整 HTML,含全部
<img>/srcset/alt,不能只存文字段) · SEO Title/描述 EN/中 · Tags ·custom.*(material/height/box_size/hidden_odds/series/scenario_copy/faq)· 目标IP/品类/场景集合 - ③ 规划(表内,不同步):IP角色 · 商品类型 · 主/辅关键词 · 搜索意图 · 页面角色 · FAQ主题 · Schema优先级 · 资料来源 · 运营优先级 · 备注。新商品先初始化为「待评估」,optimize 必须拍板为 P0/P1 或 P2,否则 QA 阻断上线。
- ④ 流程状态(表内):内容审核状态(状态流)· 写回状态/时间/错误 · 索引/Schema/FAQ校验状态 · 负责人 · 上线验收 · 同步日期
状态流:待补素材 →(同事补硬缺)→ 待拍板 →(同事定 handle/集合/术语)→ 待确认上线 →(同事确认)→ 已上线
视图:01 待补/待拍板 · 03 待确认上线 · 04 SEO-GEO 验收(索引/schema 空)
步骤 1 · sync-pull(拉草稿进表)· ✅ 已脚本化
- 跑法:
cd scripts && python3 sync_pull.py [--all | --status draft | --product-id gid://shopify/Product/...] [--mirror-only] [--dry-run] [--limit N]。默认只拉 draft;--product-id精确回刷单品;--mirror-only只动 Shopify 镜像、不碰内容;--dry-run只显示真实字段 diff。已验证:按 Product ID 幂等 upsert、飞书记录完整翻页、镜像字段可清空旧值、内容仅填空、URL/多选/数字/日期类型感知、集合归属自动归类。 - 触发:同事建好草稿后 / 定期。纯读 Shopify,不回写。
- 逻辑:
- GraphQL 拉商品(默认
query:"status:draft",可指定/全量):id handle onlineStoreUrl status vendor productType tags category{fullName} title descriptionHtml seo variants(first:250){id/title/sku/price/inventoryQuantity/inventoryPolicy} featuredImage collections metafields。Product Type/Shopify分类属于镜像;Tags 只填表内空值,不覆盖运营选择。 - 按 Shopify Product ID upsert 进飞书表(
lark-cli base +record-upsert或api ...records/batch_update),有则更新①镜像、无则新建行。 - ②内容字段仅填空、不覆盖(不动运营已优化的)。
- 集合归属写入 目标IP/品类/场景集合(★老坑:手动拉常漏集合,务必拉★)。
- 有真实 diff 才盖
最近Shopify同步日期;新草稿内容审核状态=待补素材、运营优先级=待评估。 - 新建飞书行同时初始化
Shopify写回状态=未写回、FAQ JSON校验状态=待检查;Shopify写回时间/写回错误信息保持空。拉取 Shopify 不是写回成功事件,不得生成时间或成功状态。
- GraphQL 拉商品(默认
- 铁律:幂等键=Product ID(绝不重复行)· 内容字段绝不覆盖非空 · handle 只镜像不回写 · 第二次同参数计划必须 0 写入 · 逐商品失败记录不中断。
- 验收:每草稿一行,镜像齐、集合有、状态=待补素材,抽验与 Shopify 一致。
步骤 2 · product-audit(核查缺啥,只提醒「硬缺」)· ✅ 已脚本化
- 跑法:
python3 scripts/audit.py(核查「待补素材」行的硬缺,输出补充清单;软缺不烦同事)。 - 触发:状态=待补素材的行。
- 做:逐行核查必填。分 硬缺(只有同事能给:材质/尺寸/隐藏款概率[仅盲盒]/系列/IP/官方依据/主图)vs 软缺(skill 能生成:SEO/关键词/FAQ/scenario/集合)。硬缺→生成补充清单提醒同事;软缺不烦他。硬缺齐→可进 optimize。
- 铁律:只提醒「人才能提供的」;skill 能做的绝不甩给同事。
- 责任:skill 核查+提醒;同事补硬缺(在 01 视图)。
步骤 3 · product-optimize(生成 SEO/metafield/FAQ/集合/schema;拍板汇总问)
- 触发:硬缺已补的行。
- 做:① title(H1,含关键词+IP+品类)② SEO title(≤60,
X | Funcinating)/ SEO desc(≤160)③ handle 建议(英文关键词 slug)④ 主/辅关键词·搜索意图·页面角色·Schema优先级·FAQ主题·运营优先级(P0/P1 或 P2) ⑤ metafields:scenario_copy · faq(合法 json[{"question","answer"}])⑥ 集合归属建议 ⑦ 去 AI 味(调 humanizer/humanizer-zh)⑧ 遇决策(handle 用哪个 / 集合归哪 / 术语)→ 一次性汇总给同事拍板 → 状态=待确认上线。 - 铁律:DNT(config.dnt_names 不译)· handle 英文不译 · faq 合法 json · 非美元语言不出现
$(平价框架)· 去 AI 味 · 拍板项一次问、别碎问。 - 责任:skill 生成;同事只拍板。
步骤 3.5 · image-optimize(正文图尺寸优化)· ✅ 已脚本化
- 跑法:
python3 image_optimize.py --dry-run(预览)→--apply(写回)。--handle <h>限单品;--width 1600兜底封顶;--no-srcset/--no-lazy退化。 - 解决:商品描述 HTML 里的
<img>常是原图直出(几 MB/张),浏览器为显示750px 的图下载整张。改写正文45%,且懒加载让正文图不拖首屏 LCP。纯改 descriptionHtml(productUpdate),不动 media/不重传,去 width 参数即回退。<img>:src 加?width=封顶 + srcset 响应式 +loading=lazy——手机单页正文图 ↓ - 触发:上架前 / 批量优化老商品。图片复用:先查是否有跨商品字节相同的图(content-length+md5),有才复用 URL、无则不折腾(art-toy 站多为专属图,常 0 命中)。
- 铁律:只改 cdn.shopify.com 且未封顶的
<img>;已带width=的跳过(幂等);主图廊(media)归主题响应式、本步不碰。
步骤 4 · product-translate(多语言,承接《多语言适配指南》)· ✅ 已脚本化(两模式)
- 跑法(两套内容,机械 bookends,中间 agent 翻):
- 标准可翻译内容(title/描述/SEO):
translate.py --entity <e> --lang <l> --resource-id <gid> --export out.json只拉目标资源的 EN+digest → agent 逐条填target→--import out.json走translationsRegister回。商品body_html导入前同样执行图片门禁:禁止 Markdown 化 src/srcset,译文<img>数不得少于 EN 源。单商品上架必须传--resource-id,避免误导出或处理其他商品。 _<lang>metafield 变体(json/rich_text 不走标准翻译):translate.py --entity <e> --lang <l> --export-mf mf.json拉 scenario_copy/faq(商品)、editorial_body/faq/homepage_*(集合)的 EN 基线 → agent 保结构翻 →--import-mf mf.json走metafieldsSet写<key>_<lang>(后缀见 entities.LANG_SUFFIX:es/th/zh/zh_tw)。已 E2E 验证(ZZ 测试 key 写→查→删零残留)。
- 标准可翻译内容(title/描述/SEO):
- 上线前两道自检(踩过的坑,务必跑):
- ★主题 UI 完整性:
python3 locale_check.py --pull --lang <l>——--pull从 live 主题实拉 locale 再比(★必须用线上真值:本地 checkout 会落后于线上、产生几百条假缺口,别信★)。揪出缺失/还是英文的主题串(筛选条 All/HOT/NEW、角标、header/footer/blog、aria)。★新语言最爱漏这个,店面半英半外自己看不出★。--out gaps.json导缺口给 agent 翻,合并回<l>.json再theme publish。DNT 专名自动过滤;剩「疑似」含同形词(Material/SKU/Global)需复核。 - market 启用:
python3 translate.py --lang <l> --market-check——内容翻好了还得在 market 启用+发布该语言,否则前台根本不显示(翻完纳闷「怎么没变」的坑)。
- ★主题 UI 完整性:
- 触发:EN 基线定后(可与步骤 3 并行)。目标语言 = config.languages.alternates。
- 做:从 EN 翻 title/描述/SEO → 各语言;
_<lang>metafield 变体(scenario_copy/faq);主题 UI 串(locale_check 揪出的);术语拍板(如 charm→llavero)问一次。zh-TW/th/es 批量翻,zh-CN 可同事原创。 - 铁律:DNT/handle 不译 · 各语言共用英文 handle · 非美元语言无
$· 功能词本地化 · 主题 UI 译文必须进locales/<lang>.json文件(release 会冲掉 API override) · 上线后theme publish清缓存(push不清)。深挖见飞书手册。 - 责任:skill 翻;术语问同事。
步骤 5 · regional-pricing(飞书矩阵定价→同档商品复核)· ✅ 已脚本化
- 位置:翻译完成后、最终上线确认前单独执行。它不复用
sync_writeback.py,不改Product.status,不发布商品,也不增删 Catalog 成员;商品若尚未加入任一配置目录会直接阻断,先完成目录归属再重跑。 - 跑法:先只读预览,再明确执行:
python3 scripts/regional_pricing.py --product-id 'gid://shopify/Product/...' --tier P02 --dry-run- 检查 mutation plan 后:
python3 scripts/regional_pricing.py --product-id 'gid://shopify/Product/...' --tier P02 --apply - 默认从
config.pricing.reference_products找同档 Active 参考商品;该档未登记时必须传--reference-product-id 'gid://shopify/Product/...'。 - 变体标题/SKU 无法唯一说明包装数量时,显式重复传
--variant-multiplier 'VARIANT_ID=N';不能按变体顺序猜。 - 目标与参考商品端盒数量不同但价格档一致时,必须由同事明确确认“单盒价 × 数量”后,显式传
--allow-reference-packaging-mismatch。脚本仍分别按飞书单盒价校验两件商品的每个包装倍数;默认不带开关时继续阻断。
- 数值来源:飞书价格矩阵是唯一金额 SSOT;档位必须显式是
P01–P07。不能按美元价自动推断(P04/P05在现有目录金额相同),也不能让参考商品反向覆盖飞书。 - 标准配置:飞书金额定义为单盒/1 Pc;整端价=
单盒价×已确认盒数。US-USD写商品基础 USD 价,US 与 Global 都必须保持继承(RELATIVE);GB/EU/TH/SG/MY/VN/PH/ID/MX 写本币 FIXED。Catalog/PriceList 用配置中的稳定 GID 定位,标题和币种只做漂移校验;发现任何未纳入配置的新 ACTIVE Catalog 就阻断,先扩充飞书矩阵与配置。CN/BR 当前无专属 Catalog,只报SKIPPED_NO_CATALOG,不得映射 Global 或自动建目录。 - 同档复核:参考商品须为 Active;默认要求包装倍数完全一致。仅在同事明确确认“单盒价 × 数量”且显式带
--allow-reference-packaging-mismatch时,允许端盒数量不同,并分别按各自倍数对飞书矩阵做归一校验。参考商品的 amount/currency/originType/Compare-at 必须与飞书矩阵和标准配置一致;漂移报REFERENCE_DRIFT并阻断,不能拿错误参考修正目标。 - 安全门禁:只允许目标为 DRAFT;Compare-at 非空即阻断,绝不静默清除促销;写前重读飞书、目标、参考品与 Catalog/PriceList;只写 before/desired 差异;多市场中途失败自动恢复本轮已成功批次;写后全量回读,确认商品仍 DRAFT,再生成一次计划必须 0 mutation。
步骤 6 · confirm-publish(自检→同事确认→写回+上架+索引)· ✅ 写回已脚本化
- 跑法:先执行
python3 scripts/qa.py --entity product --status 待确认上线 --write-status,把 FAQ 结构校验结果幂等回填飞书;全绿后可先执行python3 scripts/sync_writeback.py --prepare-draft --inventory-policy 'PRODUCT_ID=CONTINUE|DENY',将标题/描述/SEO/metafields/集合写入并保持 DRAFT,完成翻译与验收后再去掉--prepare-draft做最终激活。批量上架时每个商品重复传一次--inventory-policy。第一次未传可以让脚本列出待确认商品并阻断;不存在默认策略,也不得代替用户推断。脚本同时执行config.sellability区域可售门禁,无需另跑手工运费检查。 - 库存策略人工闸(商品必做):
- 逐商品向用户展示当前各变体
inventoryPolicy,明确解释CONTINUE=库存为0仍可下单、DENY=库存为0停止销售。 - 让用户逐商品二选一;不得预选、不得根据商品类型/库存/标签自动决定、不得用“推荐值”当确认。
- 用户未明确回答的商品不写回、不转
ACTIVE;脚本缺少对应PRODUCT_ID=...时整批阻断。 - 获得确认后,脚本用
productVariantsBulkUpdate只修改与选择不一致的变体,再继续商品写回。多于 250 个变体时停止并转人工处理;写后必须回读确认每个变体都等于用户选择。
- 逐商品向用户展示当前各变体
- 区域履约与前台可售门禁(商品必做):
- 从
config.sellability.delivery_profile_id读取标准配送方案;确认上线后把尚未关联的目标变体用deliveryProfileUpdate(variantsToAssociate)关联进去,不按配送方案名称猜。--dry-run只报告计划,不修改。 - 回读确认商品全部变体属于该方案;
required_country_codes中每个国家都有启用运费方式 + ACTIVE 且 fulfillsOnlineOrders 的仓库。分页超限或任一国家缺覆盖即阻断。 - 回读每个变体的库存地点。
DENY必须在对应可履约仓有正available,且sellableOnlineQuantity>0;CONTINUE仍须存在可履约仓库存记录,不能用允许超卖掩盖错误配送关联。 - 商品激活后按
storefront_checks读取真实区域/products/<handle>.js,逐变体要求available=true;Admin mutation 成功、商品有 URL、后台库存为正都不能替代这一步。允许短时重试以等待 Shopify 传播,仍为 false 就把飞书写回状态记为失败,不得记“已上线/成功”。 - 若后台配送/库存全部通过但前台仍 false,禁止自动改成 CONTINUE,也禁止静默改库存。先向用户报告;仅在用户明确确认后,才可用带
changeFromQuantity和幂等键的inventorySetQuantities做 CAS 库存事件并恢复原值,随后重新验证后台与前台。
- 从
- 完整写回:写回「待确认上线」行:标准配送方案关联 + 变体库存策略 + productUpdate 标题/描述/SEO + metafieldsSet custom.* + 字段级 diff + handle 不写回 + DRAFT→ACTIVE 并发布到 Online Store + 集合归属 collectionAddProducts。任何 Shopify mutation 之前先做两类硬闸:①资料来源非空、运营优先级已拍板、FAQ 是非空
[{question,answer}]数组;②描述禁止src="[URL](URL)"/srcsetMarkdown 化,且飞书<img>数不得少于 Shopify 当前值。任一失败时整商品零 Shopify mutation,飞书记失败原因及真实 FAQ 校验状态。每个 mutation 的userErrors都是失败;全部写完必须重新读取 Shopify,核对 ACTIVE、Online Store 商品 URL、库存策略、内容、metafield、集合、区域履约和前台真实可售,并刷新飞书镜像与同步时间。只有全部回读一致才回填「已上线/成功/写回时间」并清空旧错误;任一处不一致只写「失败+错误」,绝不假绿。★运行本脚本=同事「确认上线」闸★。多语言写回=product-translate 步骤;GSC 索引提交仍手动/待补。 - 触发:状态=待确认上线。
- 做:① 自检:必填齐 · faq 合法 json · SEO 长度 · DNT · 无
$· 正式列非空(★FAQ 坑:内容别只在草稿列★) · handle 英文 · 运营优先级已拍板 ② 出「上线预检报告」并列出当前库存策略 ③ 同事逐商品确认 CONTINUE/DENY + 确认上线 ④ 先写用户确认的变体库存策略,再写回 Shopify:productUpdate(标题/描述/seo/tags/type)+metafieldsSet(custom.* +_<lang>)+translationsRegister(各语言)+ 集合归属;字段级 diff(只写与线上不同的)⑤ 商品转 Active ⑥ 重新读取 Shopify 验证所有目标值,并把商品URL/状态/库存/分类等镜像刷新回飞书 ⑦ 回读全绿后再回填写回状态/时间·状态=已上线;GSC 提交与索引状态仍按独立验收处理。 - 铁律:只写同事确认的 · handle 不写回(改动走手动 + 301)· 正式列空不写上线 · 失败逐行记
写回错误不中断 · 加商品不用 theme publish(动主题代码才要)。 - 责任:同事确认上线(拍板);skill 执行全部写回/上架/索引。
巡检 · health(店铺健康,只读)· ✅ 已脚本化
- 跑法:
python3 health.py(内容)·python3 health.py --i18n --pull(加多语言,实拉 live locale)。 - 用途:常态监控,不是补缺口——每周/每次批量改动后跑,一张报告聚合:商品 SEO/alt/集合归属/滞留草稿/正文图是否已优化 · 集合 SEO/导购正文 · 301 · 各语言 locale 完整性 + market 启用 · 飞书商品流程字段完整性。流程巡检会抓 ACTIVE/已上线但 URL 为空、没有经回读验证的成功状态、成功无时间、失败无错误、资料来源为空、FAQ 有内容但校验未通过等矛盾。全绿 exit 0,有待办 exit 1(可接 CI/定时)。 只读,不改任何东西。
- 揪出的每项都指向对应修复脚本(缺翻译→translate、未优化图→image_optimize、locale 残英文→locale_check)。
全局铁律(贴墙)
- token 绝不进文件——Shopify 走 CLI 鉴权,Feishu 走 keychain profile。
- handle 永远英文 slug,Shopify 建品时从标题自动生成并冻结;不手打、不翻译、改动补 301。
- 专名不译(DNT):见 config.dnt_names。
- 非美元语言不出现
$,用平价框架。 - 顺序:先配齐内容+翻译+集合+区域价格,最后才转 Active;区域定价未通过同档复核不得上线。
- json 字段(faq)必须合法,否则 schema/前台坏。
- 主题层改动才需
theme publish清 CDN 缓存(theme push不清);单纯加商品不用。 - 飞书表是 SSOT:阶段用视图不拆表;实体(商品/集合/文章)各一张表。
- 新语言别只翻商品:主题 UI 串(locale 文件)+ market 启用是两个独立必做项——跑
locale_check.py+--market-check自检,否则店面半英半外 / 前台压根不显示。 - 多配送区别复制商品:Shopify「1商品=1配送方案」,复制会变「款×2」数据乱(-TH 教训);用一方案+多仓库组。audit 会扫区域后缀警示。
- Judge.me 评价挂件:用官方挂件(简版会空),且 app embed 的
settings_data必须存进主题设置,否则 release 会剥掉挂件;改主题--only外科式推。评价按用户原文展示,不做语言适配。 - 库存策略绝不默认:每个待上架商品都必须由用户明确确认
CONTINUE或DENY;未确认即阻断,不得根据“普通商品/预售商品”等上下文替用户决定。 - 价格档位绝不猜:P01–P07 必须显式给出;飞书矩阵管金额、同档 Active 商品管二次校验,二者任一漂移都阻断上线。
- 上线后必须回刷镜像:成功标准不是 mutation 返回,而是 Shopify 回读与飞书镜像一致。历史补账用
sync_pull.py --entity product --all --mirror-only;它只读 Shopify,并且同参数第二次必须 0 写入。 - 描述图片不可倒退:
商品描述EN/中文存完整 HTML;历史对账时 EN 以 ShopifydescriptionHtml为恢复源,中文以目标 locale 前台/翻译资源为恢复源。写回前发现 Markdown 化图片地址或<img>数少于 Shopify 当前值,必须阻断,不能用纯文字版本覆盖详情图。 - 流程状态只记已证事实:
未写回=没有通过当前值回读证明的成功事件;成功=本次写回后 Shopify 全字段回读一致;失败=真实执行或回读失败且必须带错误。Shopify写回时间只在成功后写,写回错误信息只记录真实错误。商品ACTIVE、存在 URL 或updatedAt都不能反推历史写回成功/时间。历史欠账先用health.py发现,再按“Shopify 当前值对账→mutation plan→只回填有依据字段→精确回读→二次计划 0 写入”处理。 - 后台有库存不等于区域前台可售:上架成功必须同时证明标准配送方案关联、目标国家运费方式、线上履约仓库存、
sellableOnlineQuantity和区域商品 JSONavailable=true;CONTINUE不能当修复手段,真实库存刷新必须另获用户确认并使用 CAS 后恢复原值。
关联
- 深挖多语言:飞书《多语言适配指南(复用手册)》
- 总纲:飞书《Shopify 独立站运营 · Skill 地图与拆解》
- 去 AI 味:
humanizer/humanizer-zh - 集合表/文章表:照本 skill 的内容步骤复用;区域定价仅适用于商品实体