fix(微信订阅): 用微信真实关键词修正歌曲排期模板(thing1/phrase2/time3/character_string4),并修掉字段认领顺序导致的校准失效

This commit is contained in:
2026-10-24 11:56:50 +08:00
parent dc2565da34
commit f5a43b37cc
7 changed files with 192 additions and 56 deletions
+55 -22
View File
@@ -153,6 +153,11 @@ export type ResolveTemplateFieldsResult = {
unresolvedVariables: string[]
/** 配置里有、但微信模板里根本不存在的关键词名(这些字段微信会直接 47003) */
invalidConfiguredCodes: string[]
/**
* 微信模板里有、但**没有任何变量去填**的关键词(形如 `thing1`)。
* 微信对缺失的关键词会回 `47003 data.thing1.value is empty` —— 这种「少填」比「填错」更常见。
*/
unclaimedCodes: string[]
}
/**
@@ -180,6 +185,8 @@ export function resolveFieldsFromKeywords(
reason,
unresolvedVariables: [] as string[],
invalidConfiguredCodes,
// 配置里没覆盖到的关键词 = 微信会报 value is empty 的那些
unclaimedCodes: keywords.map((k) => k.code).filter((code) => !(code in config)),
...extra
})
@@ -201,37 +208,44 @@ export function resolveFieldsFromKeywords(
const resolved: Record<string, string> = {}
const unresolved: string[] = []
// 一轮:类型 + 标签双重命中,优先认领把握最大的
/*
* 评分规则:首选类型 +2、次选递减;中文标签命中再 +3(目的就是压过纯类型匹配)。
*
* ⚠️ **必须全局按分数从高到低认领**,不能「按变量顺序逐个挑」—— 早先就是这么写的,
* 结果 playDate(首选 date,但模板里没有 date 字段)先把 character_string4 抢走了,
* 因为它的次选类型里也有 character_string;轮到 sequence 时已经没有可用关键词,
* 整张模板退回旧配置,发出去就是 47003。按分数排序后 sequence↔character_string4(分高)先成对。
*/
const pairs: Array<{ variable: string; code: string; score: number }> = []
for (const variable of variables) {
const allowed = VARIABLE_TYPES[variable]
let best: { code: string; score: number } | null = null
for (const kw of keywords) {
if (used.has(kw.code)) continue
const typeRank = allowed.indexOf(kw.type)
if (typeRank < 0) continue
// 首选类型 +2,备选类型递减;中文标签命中 +3(目的就是压过纯类型匹配)
const score = (allowed.length - typeRank) * 2 + (labelHits(kw.label, variable) ? 3 : 0)
if (!best || score > best.score) best = { code: kw.code, score }
pairs.push({ variable, code: kw.code, score })
}
if (!best) {
unresolved.push(variable)
continue
}
used.add(best.code)
resolved[best.code] = variable
}
// 同分时优先「标签命中」的(score 里已经含了,这里只是让顺序稳定)
pairs.sort((a, b) => b.score - a.score)
for (const pair of pairs) {
if (resolved[pair.code] !== undefined) continue
if (Object.values(resolved).includes(pair.variable)) continue
if (used.has(pair.code)) continue
used.add(pair.code)
resolved[pair.code] = pair.variable
}
if (unresolved.length) {
return configResult(
`有变量在微信模板里找不到对应的关键词(${unresolved.join('、')}),沿用配置`,
{ unresolvedVariables: unresolved }
)
for (const variable of variables) {
if (!Object.values(resolved).includes(variable)) unresolved.push(variable)
}
const unclaimed = keywords.map((k) => k.code).filter((code) => !(code in resolved))
/*
* 安全检查:同一类型出现多个关键词时,必须靠中文标签区分开。
* 例如驳回模板有两个 thing,如果不看标签只按类型分配,songTitle 和 reason 会互换 ——
* 能把消息发出去,但内容张冠李戴,比发不出去更糟。
* 安全检查(先做,因为它是"不可采纳"的硬条件):同一类型出现多个关键词时,
* 必须靠中文标签区分开。例如驳回模板有两个 thing,如果不看标签只按类型分配,
* songTitle 和 reason 会互换 —— 能把消息发出去,但内容张冠李戴,比发不出去更糟。
*/
const byType = new Map<KeywordType, TemplateKeyword[]>()
for (const kw of keywords) {
@@ -260,6 +274,23 @@ export function resolveFieldsFromKeywords(
}
}
/*
* 到这里已经通过了同类型区分的安全检查,剩下的唯一问题是「能不能把模板填满」:
* - 模板里每个关键词都有变量认领(unclaimed 为空)→ 采用微信侧映射。
* 此时即使有变量在模板里找不到位置(比如配置里写了模板没有的 date),也**直接忽略**它 ——
* 发一个微信不认识的字段没有任何好处,只会让整条消息 47003。
* - 还有关键词没人填 → 配置可能本来就指望它(例如配置里写了正确的 phrase5),
* 这时候不该自作聪明覆盖配置,沿用配置更安全。
*/
if (unclaimed.length) {
return configResult(
`微信模板里还有关键词没被任何字段填上(${unclaimed.join('、')}),` +
(unresolved.length ? `同时有变量在模板里找不到位置(${unresolved.join('、')}):` : '') +
'为避免把模板改坏,沿用配置',
{ unresolvedVariables: unresolved }
)
}
// 配置里的关键词名只要有一个在微信模板里不存在,就说明配置已经过期,改用校准结果
const changed =
configuredCodes.some((code) => !(code in resolved)) ||
@@ -271,9 +302,11 @@ export function resolveFieldsFromKeywords(
reason: changed
? `按微信真实模板校准了字段名:${Object.entries(resolved)
.map(([code, variable]) => `${code}=${variable}`)
.join(',')}`
.join(',')}` +
(unresolved.length ? `(模板里没有对应关键词的变量已忽略:${unresolved.join('、')})` : '')
: '配置与微信真实模板一致',
unresolvedVariables: [],
invalidConfiguredCodes
unresolvedVariables: unresolved,
invalidConfiguredCodes,
unclaimedCodes: unclaimed
}
}