@steedos-labs/content-compliance 0.8.36 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/index.js +1 -1
  2. package/lib/actions/contentHubMaterialGroups.d.ts +19 -0
  3. package/lib/actions/contentHubMaterialGroups.js +70 -0
  4. package/lib/actions/contentHubMaterialGroups.js.map +1 -0
  5. package/lib/actions/contentHubSyncPreview.d.ts +21 -0
  6. package/lib/actions/contentHubSyncPreview.js +51 -0
  7. package/lib/actions/contentHubSyncPreview.js.map +1 -0
  8. package/lib/actions/contentHubTagValues.d.ts +12 -0
  9. package/lib/actions/contentHubTagValues.js +70 -0
  10. package/lib/actions/contentHubTagValues.js.map +1 -0
  11. package/lib/actions/index.d.ts +3 -0
  12. package/lib/actions/index.js +3 -0
  13. package/lib/actions/index.js.map +1 -1
  14. package/lib/actions/sendToContentHub.js +1 -1
  15. package/lib/actions/sendToContentHub.js.map +1 -1
  16. package/lib/actions/spotCheckMaterial.js +2 -1
  17. package/lib/actions/spotCheckMaterial.js.map +1 -1
  18. package/lib/index.js +173 -30
  19. package/lib/index.js.map +1 -1
  20. package/lib/methods/__tests__/contentHubMaterialGroup.test.d.ts +1 -0
  21. package/lib/methods/__tests__/contentHubMaterialGroup.test.js +97 -0
  22. package/lib/methods/__tests__/contentHubMaterialGroup.test.js.map +1 -0
  23. package/lib/methods/__tests__/contentHubMaterialGroupRule.test.d.ts +1 -0
  24. package/lib/methods/__tests__/contentHubMaterialGroupRule.test.js +168 -0
  25. package/lib/methods/__tests__/contentHubMaterialGroupRule.test.js.map +1 -0
  26. package/lib/methods/__tests__/contentHubPush.test.d.ts +1 -0
  27. package/lib/methods/__tests__/contentHubPush.test.js +568 -0
  28. package/lib/methods/__tests__/contentHubPush.test.js.map +1 -0
  29. package/lib/methods/__tests__/contentHubSyncRule.test.d.ts +1 -0
  30. package/lib/methods/__tests__/contentHubSyncRule.test.js +130 -0
  31. package/lib/methods/__tests__/contentHubSyncRule.test.js.map +1 -0
  32. package/lib/methods/__tests__/contentHubTagSync.test.d.ts +1 -0
  33. package/lib/methods/__tests__/contentHubTagSync.test.js +743 -0
  34. package/lib/methods/__tests__/contentHubTagSync.test.js.map +1 -0
  35. package/lib/methods/__tests__/tagLabel.test.d.ts +1 -0
  36. package/lib/methods/__tests__/tagLabel.test.js +136 -0
  37. package/lib/methods/__tests__/tagLabel.test.js.map +1 -0
  38. package/lib/methods/approval_engine.js +10 -7
  39. package/lib/methods/approval_engine.js.map +1 -1
  40. package/lib/methods/consts.d.ts +4 -0
  41. package/lib/methods/consts.js.map +1 -1
  42. package/lib/methods/content_hub.d.ts +8 -1
  43. package/lib/methods/content_hub.js +278 -27
  44. package/lib/methods/content_hub.js.map +1 -1
  45. package/lib/methods/content_hub_copyright.d.ts +15 -0
  46. package/lib/methods/content_hub_copyright.js +223 -0
  47. package/lib/methods/content_hub_copyright.js.map +1 -0
  48. package/lib/methods/content_hub_material_group.d.ts +29 -0
  49. package/lib/methods/content_hub_material_group.js +268 -0
  50. package/lib/methods/content_hub_material_group.js.map +1 -0
  51. package/lib/methods/content_hub_material_group_rule.d.ts +23 -0
  52. package/lib/methods/content_hub_material_group_rule.js +58 -0
  53. package/lib/methods/content_hub_material_group_rule.js.map +1 -0
  54. package/lib/methods/content_hub_sync_rule.d.ts +51 -0
  55. package/lib/methods/content_hub_sync_rule.js +122 -0
  56. package/lib/methods/content_hub_sync_rule.js.map +1 -0
  57. package/lib/methods/content_hub_tag.d.ts +57 -0
  58. package/lib/methods/content_hub_tag.js +830 -0
  59. package/lib/methods/content_hub_tag.js.map +1 -0
  60. package/lib/methods/pls.d.ts +4 -1
  61. package/lib/methods/pls.js +60 -51
  62. package/lib/methods/pls.js.map +1 -1
  63. package/lib/methods/tag_label.d.ts +19 -0
  64. package/lib/methods/tag_label.js +192 -0
  65. package/lib/methods/tag_label.js.map +1 -0
  66. package/main/default/applications/pepsico_content_setting.app.yml +8 -0
  67. package/main/default/objects/pepesio_ch__c/fields/media_type.field.yml +2 -0
  68. package/main/default/objects/pepesio_ch__c/fields/pepsico_material_approval.field.yml +6 -0
  69. package/main/default/objects/pepesio_ch__c/fields/push_status.field.yml +11 -0
  70. package/main/default/objects/pepesio_ch__c/fields/type.field.yml +48 -8
  71. package/main/default/objects/pepesio_ch__c/listviews/all.listview.yml +2 -0
  72. package/main/default/objects/pepsico_ch_material_group_rule/fields/active.field.yml +6 -0
  73. package/main/default/objects/pepsico_ch_material_group_rule/fields/classification.field.yml +73 -0
  74. package/main/default/objects/pepsico_ch_material_group_rule/fields/name.field.yml +9 -0
  75. package/main/default/objects/pepsico_ch_material_group_rule/fields/orgs.field.yml +10 -0
  76. package/main/default/objects/pepsico_ch_material_group_rule/fields/priority.field.yml +10 -0
  77. package/main/default/objects/pepsico_ch_material_group_rule/fields/query_mode.field.yml +12 -0
  78. package/main/default/objects/pepsico_ch_material_group_rule/fields/remark.field.yml +7 -0
  79. package/main/default/objects/pepsico_ch_material_group_rule/listviews/all.listview.yml +20 -0
  80. package/main/default/objects/pepsico_ch_material_group_rule/pepsico_ch_material_group_rule.object.yml +19 -0
  81. package/main/default/objects/pepsico_ch_material_group_rule/permissions/admin.permission.yml +15 -0
  82. package/main/default/objects/pepsico_ch_material_group_rule/permissions/user.permission.yml +14 -0
  83. package/main/default/objects/pepsico_ch_sync_rule/fields/active.field.yml +6 -0
  84. package/main/default/objects/pepsico_ch_sync_rule/fields/classification.field.yml +73 -0
  85. package/main/default/objects/pepsico_ch_sync_rule/fields/kol_classification.field.yml +67 -0
  86. package/main/default/objects/pepsico_ch_sync_rule/fields/name.field.yml +9 -0
  87. package/main/default/objects/pepsico_ch_sync_rule/fields/need_sync.field.yml +11 -0
  88. package/main/default/objects/pepsico_ch_sync_rule/fields/orgs.field.yml +10 -0
  89. package/main/default/objects/pepsico_ch_sync_rule/fields/priority.field.yml +10 -0
  90. package/main/default/objects/pepsico_ch_sync_rule/fields/product.field.yml +65 -0
  91. package/main/default/objects/pepsico_ch_sync_rule/fields/remark.field.yml +7 -0
  92. package/main/default/objects/pepsico_ch_sync_rule/fields/spot_check.field.yml +12 -0
  93. package/main/default/objects/pepsico_ch_sync_rule/listviews/all.listview.yml +24 -0
  94. package/main/default/objects/pepsico_ch_sync_rule/pepsico_ch_sync_rule.object.yml +19 -0
  95. package/main/default/objects/pepsico_ch_sync_rule/permissions/admin.permission.yml +15 -0
  96. package/main/default/objects/pepsico_ch_sync_rule/permissions/user.permission.yml +14 -0
  97. package/main/default/objects/pepsico_ch_tag_sync_log/fields/action.field.yml +6 -0
  98. package/main/default/objects/pepsico_ch_tag_sync_log/fields/changed_at.field.yml +6 -0
  99. package/main/default/objects/pepsico_ch_tag_sync_log/fields/duration.field.yml +6 -0
  100. package/main/default/objects/pepsico_ch_tag_sync_log/fields/error_code.field.yml +6 -0
  101. package/main/default/objects/pepsico_ch_tag_sync_log/fields/error_msg.field.yml +6 -0
  102. package/main/default/objects/pepsico_ch_tag_sync_log/fields/expired_date.field.yml +6 -0
  103. package/main/default/objects/pepsico_ch_tag_sync_log/fields/name.field.yml +7 -0
  104. package/main/default/objects/pepsico_ch_tag_sync_log/fields/parent_tag_val_id.field.yml +6 -0
  105. package/main/default/objects/pepsico_ch_tag_sync_log/fields/request_id.field.yml +7 -0
  106. package/main/default/objects/pepsico_ch_tag_sync_log/fields/result.field.yml +6 -0
  107. package/main/default/objects/pepsico_ch_tag_sync_log/fields/source_ip.field.yml +6 -0
  108. package/main/default/objects/pepsico_ch_tag_sync_log/fields/space.field.yml +6 -0
  109. package/main/default/objects/pepsico_ch_tag_sync_log/fields/tag_id.field.yml +6 -0
  110. package/main/default/objects/pepsico_ch_tag_sync_log/fields/tag_val_id.field.yml +7 -0
  111. package/main/default/objects/pepsico_ch_tag_sync_log/fields/tag_val_name.field.yml +6 -0
  112. package/main/default/objects/pepsico_ch_tag_sync_log/listviews/all.listview.yml +21 -0
  113. package/main/default/objects/pepsico_ch_tag_sync_log/pepsico_ch_tag_sync_log.object.yml +16 -0
  114. package/main/default/objects/pepsico_ch_tag_sync_log/permissions/admin.permission.yml +15 -0
  115. package/main/default/objects/pepsico_ch_tag_sync_log/permissions/user.permission.yml +14 -0
  116. package/main/default/objects/pepsico_ch_tag_sync_request/fields/changed_at.field.yml +6 -0
  117. package/main/default/objects/pepsico_ch_tag_sync_request/fields/duration.field.yml +6 -0
  118. package/main/default/objects/pepsico_ch_tag_sync_request/fields/expired_date.field.yml +6 -0
  119. package/main/default/objects/pepsico_ch_tag_sync_request/fields/item_count.field.yml +6 -0
  120. package/main/default/objects/pepsico_ch_tag_sync_request/fields/name.field.yml +7 -0
  121. package/main/default/objects/pepsico_ch_tag_sync_request/fields/request_id.field.yml +7 -0
  122. package/main/default/objects/pepsico_ch_tag_sync_request/fields/response.field.yml +9 -0
  123. package/main/default/objects/pepsico_ch_tag_sync_request/fields/result_code.field.yml +6 -0
  124. package/main/default/objects/pepsico_ch_tag_sync_request/fields/source_ip.field.yml +6 -0
  125. package/main/default/objects/pepsico_ch_tag_sync_request/fields/space.field.yml +6 -0
  126. package/main/default/objects/pepsico_ch_tag_sync_request/fields/tag_id.field.yml +6 -0
  127. package/main/default/objects/pepsico_ch_tag_sync_request/fields/tag_label.field.yml +6 -0
  128. package/main/default/objects/pepsico_ch_tag_sync_request/listviews/all.listview.yml +19 -0
  129. package/main/default/objects/pepsico_ch_tag_sync_request/pepsico_ch_tag_sync_request.object.yml +16 -0
  130. package/main/default/objects/pepsico_ch_tag_sync_request/permissions/admin.permission.yml +15 -0
  131. package/main/default/objects/pepsico_ch_tag_sync_request/permissions/user.permission.yml +14 -0
  132. package/main/default/objects/pepsico_material/fields/material_type_label.field.yml +10 -0
  133. package/main/default/objects/pepsico_material_approval/buttons/pepsico_confirm_online.button.yml +78 -1
  134. package/main/default/objects/pepsico_material_approval/buttons/pepsico_modify_pls.button.yml +1 -1
  135. package/main/default/objects/pepsico_material_approval/fields/available_channels.field.yml +2 -0
  136. package/main/default/objects/pepsico_material_approval/fields/available_channels_label.field.yml +10 -0
  137. package/main/default/objects/pepsico_material_approval/fields/ch_material_group.field.yml +56 -0
  138. package/main/default/objects/pepsico_material_approval/fields/ch_material_group_path.field.yml +14 -0
  139. package/main/default/objects/pepsico_material_approval/fields/ch_sync_enabled.field.yml +11 -0
  140. package/main/default/objects/pepsico_material_approval/fields/ch_sync_rule_check.field.yml +55 -0
  141. package/main/default/objects/pepsico_material_approval/fields/content_rule.field.yml +2 -0
  142. package/main/default/objects/pepsico_material_approval/fields/content_rule_label.field.yml +10 -0
  143. package/main/default/objects/pepsico_material_approval/fields/ec_check_food_name.field.yml +2 -2
  144. package/main/default/objects/pepsico_material_approval/fields/ec_check_ingredients.field.yml +2 -2
  145. package/main/default/objects/pepsico_material_approval/fields/ec_check_net_content.field.yml +2 -2
  146. package/main/default/objects/pepsico_material_approval/fields/ec_check_producer_address.field.yml +2 -2
  147. package/main/default/objects/pepsico_material_approval/fields/ec_check_producer_name.field.yml +2 -2
  148. package/main/default/objects/pepsico_material_approval/fields/ec_check_shelf_life.field.yml +2 -2
  149. package/main/default/objects/pepsico_material_approval/fields/ec_check_standard_code.field.yml +2 -2
  150. package/main/default/objects/pepsico_material_approval/fields/ec_check_storage_condition.field.yml +2 -2
  151. package/main/default/objects/pepsico_material_approval/fields/ec_has_detail_page.field.yml +1 -1
  152. package/main/default/objects/pepsico_material_approval/fields/ec_producers.field.yml +1 -1
  153. package/main/default/objects/pepsico_material_approval/fields/ec_qc_production_email.field.yml +2 -2
  154. package/main/default/objects/pepsico_material_approval/fields/ec_related_approval.field.yml +1 -1
  155. package/main/default/objects/pepsico_material_approval/fields/effective_date.field.yml +2 -0
  156. package/main/default/objects/pepsico_material_approval/fields/expiry_date.field.yml +2 -0
  157. package/main/default/objects/pepsico_material_approval/fields/form_version.field.yml +6 -2
  158. package/main/default/objects/pepsico_material_approval/fields/has_nutrition_table.field.yml +1 -1
  159. package/main/default/objects/pepsico_material_approval/fields/material_editable.field.yml +78 -0
  160. package/main/default/objects/pepsico_material_approval/fields/material_type.field.yml +2 -0
  161. package/main/default/objects/pepsico_material_approval/fields/material_type_label.field.yml +10 -0
  162. package/main/default/objects/pepsico_material_approval/fields/pls_file.field.yml +1 -1
  163. package/main/default/objects/pepsico_material_approval/fields/pls_km_files.field.yml +2 -5
  164. package/main/default/objects/pepsico_material_approval/fields/pls_upload_files.field.yml +1 -1
  165. package/main/default/objects/pepsico_material_approval/fields/product_label.field.yml +10 -0
  166. package/main/default/objects/pepsico_material_approval_data/fields/guding_pass.field.yml +9 -1
  167. package/main/default/objects/pepsico_material_data/fields/guding_pass.field.yml +9 -1
  168. package/main/default/objects/picklist_options/fields/ch_changed_at.field.yml +8 -0
  169. package/main/default/objects/picklist_options/picklist_options.object.yml +7 -0
  170. package/main/default/tabs/object_pepsico_ch_material_group_rule.tab.yml +8 -0
  171. package/main/default/tabs/object_pepsico_ch_sync_rule.tab.yml +8 -0
  172. package/main/default/triggers/pepsico_material_approval_tag_label.trigger.yml +80 -0
  173. package/main/default/triggers/pepsico_material_tag_label.trigger.yml +77 -0
  174. package/package.json +1 -1
  175. package/scripts/audit-legacy-tags.js +246 -0
  176. package/scripts/probe-content-hub-tags.js +134 -0
  177. package/main/default/objects/pepsico_material_approval/fields/has_detail_page.field.yml +0 -18
@@ -0,0 +1,246 @@
1
+ /**
2
+ * 存量标签数据体检 —— issue #448 上线前必跑。
3
+ *
4
+ * 用法(直接对着历史库跑, 只读, 不写任何数据):
5
+ * mongosh "mongodb://127.0.0.1:27017/pepsico-9-17" steedos-packages/content-review/scripts/audit-legacy-tags.js
6
+ *
7
+ * 查七件事:
8
+ * 1. C2 存量 tagValId 是否只含字母数字下划线、≤64 —— 违规的将来被 40003 挡掉, 永远同步不了
9
+ * 2. C5 树深是否 ≤3 —— 超了的话给其下加子节点会被拒
10
+ * 3. C1 同一类目内 value 是否唯一 —— 重复会让定位键失效
11
+ * 4. 父级引用是否悬空 —— 悬空的在下拉里会被当成一级节点, 层级静默错位
12
+ * 5. 水位线 多少条缺 ch_changed_at —— 缺的没有乱序保护, 会被旧事件覆盖
13
+ * 6. 历史单据 引用了不可见/不存在选项的单据数 —— 方案 §10 要求历史单据不得显示为空
14
+ * 7. 硬编码路径 三个元数据文件里写死的 7 条素材类型路径是否仍然成立(见 P1)
15
+ *
16
+ * 判"可见"的口径照抄真实数据源, 不是想当然:
17
+ * 四个字段的 source 都是 picklist_options(filters:[["enable","=",true]]),
18
+ * adaptor 再按 parent.value 自底向上拼树。于是
19
+ * - enable 不等于 true(含字段缺失)的选项, 下拉里根本没有;
20
+ * - 父级被过滤掉时, 其下整棵子树都拼不进树, 即使子节点自己 enable=true。
21
+ * 只数"自己被停用"会显著少报受影响面。
22
+ */
23
+ const WHITELIST = {
24
+ '223321001114213': '品牌/产品',
25
+ '223321001114222': '素材类型',
26
+ '223321001114944': '可使用渠道',
27
+ '223321022150011': '素材授权规则'
28
+ };
29
+ // 审批单上引用这四类标签的字段(值为 '-' 连接的完整路径, 可能是字符串或数组)
30
+ const APPROVAL_FIELDS = {
31
+ '223321001114213': ['product'],
32
+ '223321001114222': ['material_type'],
33
+ '223321001114944': ['available_channels'],
34
+ '223321022150011': ['content_rule']
35
+ };
36
+ // 写死在元数据里、驱动"上线必填失效日期+授权规则+渠道"这条规则的 7 条素材类型路径。
37
+ // 出处: fields/expiry_date.field.yml、fields/content_rule.field.yml、
38
+ // buttons/pepsico_confirm_online.button.yml(3 处)
39
+ const HARDCODED_PATHS = [
40
+ '223321001114363-223321001114372',
41
+ '223321001114363-223321001114381',
42
+ '223321001114363-223321001114395',
43
+ '223321001114363-2233305175173759',
44
+ '223321001114468-22334121112868126',
45
+ '2233305104650610-2233305104650621',
46
+ '2233305104650610-2233305104650637'
47
+ ];
48
+ const VALID = /^[A-Za-z0-9_]+$/;
49
+ const MAX_LEN = 64, MAX_DEPTH = 3, SEP = '-';
50
+
51
+ const line = (s) => print(s);
52
+ const head = (s) => { print(''); print('═══ ' + s + ' ═══'); };
53
+
54
+ let totalProblems = 0;
55
+ const flag = (n) => { totalProblems += n; return n; };
56
+
57
+ head('0. 托管类目');
58
+ const picklists = {};
59
+ Object.keys(WHITELIST).forEach((code) => {
60
+ const pl = db.picklists.findOne({ code: code });
61
+ if (!pl) { line(' ✗ ' + code + ' ' + WHITELIST[code] + ' —— 类目不存在!接口会返回 50000'); flag(1); return; }
62
+ picklists[code] = pl;
63
+ line(' ✓ ' + code + ' ' + WHITELIST[code] + ' (_id=' + pl._id + ')');
64
+ });
65
+
66
+ // 各类目的索引, 后面硬编码路径那节要跨类目找, 所以存下来
67
+ const indexes = {};
68
+
69
+ Object.keys(picklists).forEach((code) => {
70
+ const pl = picklists[code];
71
+ head(WHITELIST[code] + ' ' + code);
72
+
73
+ const opts = db.picklist_options.find({ picklist: pl._id }).toArray();
74
+ const byId = {}, byValue = {};
75
+ opts.forEach((o) => { byId[o._id] = o; (byValue[o.value] = byValue[o.value] || []).push(o); });
76
+
77
+ // parent 存的是 _id, 但历史数据里也见过直接存 value 的, 两种都认
78
+ const parentOf = (o) => (o.parent ? (byId[o.parent] || (byValue[o.parent] && byValue[o.parent][0]) || null) : null);
79
+ const hasParentRef = (o) => o.parent !== null && o.parent !== undefined && o.parent !== '';
80
+
81
+ // value 允许含 '-'(恰恰是 C2 要查的违规), 所以解析末级不能 split('-').pop(),
82
+ // 那样 'LEGACY-OLD-01' 会被切成 '01'。改成在已知 value 里做最长后缀匹配。
83
+ const valuesByLen = Object.keys(byValue).sort((a, b) => b.length - a.length);
84
+ const leafCache = {};
85
+ const resolveLeaf = (path) => {
86
+ const p = String(path);
87
+ if (leafCache[p] !== undefined) return leafCache[p];
88
+ let hit = null;
89
+ for (let i = 0; i < valuesByLen.length; i++) {
90
+ const v = valuesByLen[i];
91
+ if (p === v || (p.length > v.length && p.slice(-(v.length + SEP.length)) === SEP + v)) { hit = byValue[v][0]; break; }
92
+ }
93
+ leafCache[p] = hit;
94
+ return hit;
95
+ };
96
+
97
+ // 可见性: 照抄数据源口径 —— 自己 enable=true, 且所有祖先都 enable=true
98
+ const visibilityOf = (o) => {
99
+ let c = o, guard = 0;
100
+ while (c && guard++ < 20) {
101
+ if (c.enable !== true) return c === o ? 'self_disabled' : 'ancestor_disabled';
102
+ if (!hasParentRef(c)) return 'visible';
103
+ const p = parentOf(c);
104
+ if (!p) return 'root_promoted'; // 父级悬空: 下拉里会被当一级节点, 层级错位但仍可见
105
+ c = p;
106
+ }
107
+ return 'cycle';
108
+ };
109
+
110
+ indexes[code] = { pl: pl, opts: opts, byId: byId, byValue: byValue, parentOf: parentOf, resolveLeaf: resolveLeaf, visibilityOf: visibilityOf };
111
+
112
+ const enabled = opts.filter((o) => o.enable === true);
113
+ line(' 选项总数 ' + opts.length + ' (enable=true ' + enabled.length +
114
+ ' / 其余 ' + (opts.length - enabled.length) + ', 其余含 enable 字段缺失, 下拉里同样不出现)');
115
+ const invisible = opts.filter((o) => { const v = visibilityOf(o); return v === 'self_disabled' || v === 'ancestor_disabled'; });
116
+ const byAncestor = opts.filter((o) => visibilityOf(o) === 'ancestor_disabled');
117
+ line(' 下拉里不可见 ' + invisible.length + ' 条, 其中 ' + byAncestor.length + ' 条自己是启用的, 只因父级被停用而整棵子树消失');
118
+
119
+ // --- C2 字符集与长度 ---
120
+ const badChar = opts.filter((o) => !VALID.test(String(o.value)));
121
+ const tooLong = opts.filter((o) => String(o.value).length > MAX_LEN);
122
+ if (badChar.length) {
123
+ line(' ✗ C2 字符集违规 ' + flag(badChar.length) + ' 条 —— 这些标签将来无法同步(40003):');
124
+ badChar.slice(0, 10).forEach((o) => line(' ' + o.value + ' ' + o.name));
125
+ if (badChar.length > 10) line(' … 共 ' + badChar.length + ' 条');
126
+ } else { line(' ✓ C2 字符集'); }
127
+ if (tooLong.length) { line(' ✗ C2 超长 ' + flag(tooLong.length) + ' 条'); } else { line(' ✓ C2 长度'); }
128
+
129
+ // --- C1 value 唯一 ---
130
+ const dups = Object.keys(byValue).filter((v) => byValue[v].length > 1);
131
+ if (dups.length) {
132
+ line(' ✗ C1 value 重复 ' + flag(dups.length) + ' 个 —— 定位键失效:');
133
+ dups.slice(0, 10).forEach((v) => line(' ' + v + ' ×' + byValue[v].length +
134
+ ' (' + byValue[v].map((o) => o.name).join(' / ') + ')'));
135
+ } else { line(' ✓ C1 value 唯一'); }
136
+
137
+ // --- 父级悬空 ---
138
+ const dangling = opts.filter((o) => hasParentRef(o) && !parentOf(o));
139
+ if (dangling.length) {
140
+ line(' ✗ 父级悬空 ' + flag(dangling.length) + ' 条 —— parent 指向的选项在本类目里找不到,');
141
+ line(' adaptor 会把它们当一级节点渲染, 层级静默错位:');
142
+ dangling.slice(0, 10).forEach((o) => line(' ' + o.value + ' ' + o.name + ' parent=' + o.parent));
143
+ } else { line(' ✓ 无父级悬空'); }
144
+
145
+ // --- C5 层级 ---
146
+ const depthOf = (o) => { let d = 1, c = o, guard = 0; while (c && parentOf(c) && guard++ < 20) { c = parentOf(c); d++; } return d; };
147
+ const deep = opts.filter((o) => depthOf(o) > MAX_DEPTH);
148
+ const maxDepth = opts.reduce((m, o) => Math.max(m, depthOf(o)), 0);
149
+ line(' 实际最大层级 ' + maxDepth);
150
+ if (deep.length) {
151
+ line(' ✗ C5 超过 ' + MAX_DEPTH + ' 层 ' + flag(deep.length) + ' 条:');
152
+ deep.slice(0, 10).forEach((o) => line(' ' + o.value + ' ' + o.name + ' 第' + depthOf(o) + '层'));
153
+ } else { line(' ✓ C5 层级'); }
154
+
155
+ // --- 水位线 ---
156
+ const noWm = opts.filter((o) => !o.ch_changed_at).length;
157
+ if (noWm) {
158
+ line(' ⚠ 缺 ch_changed_at ' + noWm + ' 条 —— 没有乱序保护, 首个到达的事件无论多旧都会被应用。');
159
+ line(' (四个类目的补基线命令统一打印在最后)');
160
+ } else { line(' ✓ 水位线齐全'); }
161
+
162
+ // --- 历史单据引用 ---
163
+ const fields = APPROVAL_FIELDS[code] || [];
164
+ fields.forEach((f) => {
165
+ const q = {}; q[f] = { $exists: true, $ne: null };
166
+ const proj = { _id: 1, name: 1 }; proj[f] = 1;
167
+ const rows = db.pepsico_material_approval.find(q, proj).toArray();
168
+ const invisibleHit = [], orphanHit = [];
169
+ let refCount = 0;
170
+ rows.forEach((r) => {
171
+ const vals = Array.isArray(r[f]) ? r[f] : [r[f]];
172
+ vals.forEach((path) => {
173
+ if (!path) return;
174
+ refCount++;
175
+ const hit = resolveLeaf(path);
176
+ if (!hit) { orphanHit.push(r._id + ' → ' + path); return; }
177
+ const vis = visibilityOf(hit);
178
+ if (vis === 'self_disabled' || vis === 'ancestor_disabled') {
179
+ invisibleHit.push(r._id + ' → ' + path + ' (' + hit.name + ', ' +
180
+ (vis === 'self_disabled' ? '本身停用' : '父级停用') + ')');
181
+ }
182
+ });
183
+ });
184
+ line(' 历史单据字段 ' + f + ': ' + rows.length + ' 张单据, ' + refCount + ' 处引用');
185
+ if (orphanHit.length) {
186
+ line(' ✗ 孤儿引用 ' + flag(orphanHit.length) + ' 处 —— 选项表里找不到, 表单已无法回显:');
187
+ orphanHit.slice(0, 5).forEach((s) => line(' ' + s));
188
+ if (orphanHit.length > 5) line(' … 共 ' + orphanHit.length + ' 处');
189
+ } else { line(' ✓ 无孤儿引用'); }
190
+ if (invisibleHit.length) {
191
+ line(' ⚠ 引用下拉里已不可见的选项 ' + invisibleHit.length + ' 处 —— 数据源过滤 enable=true,');
192
+ line(' 方案 §10 要求历史单据不得因停用而显示为空, 这些就是受影响面:');
193
+ invisibleHit.slice(0, 5).forEach((s) => line(' ' + s));
194
+ if (invisibleHit.length > 5) line(' … 共 ' + invisibleHit.length + ' 处');
195
+ } else { line(' ✓ 无引用不可见选项'); }
196
+ });
197
+ });
198
+
199
+ // --- 硬编码路径(P1) ---
200
+ head('硬编码标签路径 7 条');
201
+ line(' 出处: fields/expiry_date.field.yml、fields/content_rule.field.yml、buttons/pepsico_confirm_online.button.yml');
202
+ line(' 这 7 条驱动"上线必填失效日期 + 授权规则 + 渠道", 形如 material_type.includes(\'…\')。');
203
+ line(' 匹配不上时必填校验静默失效, 没有任何报错 —— 所以这节要盯住"路径是否仍然成立"。');
204
+ const codes = Object.keys(indexes);
205
+ HARDCODED_PATHS.forEach((path) => {
206
+ let found = null, foundCode = null;
207
+ for (let i = 0; i < codes.length; i++) {
208
+ const hit = indexes[codes[i]].resolveLeaf(path);
209
+ if (hit) { found = hit; foundCode = codes[i]; break; }
210
+ }
211
+ if (!found) { line(' ✗ ' + path + ' 末级在四个托管类目里都找不到 —— 规则已失效'); flag(1); return; }
212
+ const ix = indexes[foundCode];
213
+ // 从末级往上拼回完整路径, 与写死的那条逐字比对 —— 这同时验了父子关系(C4)
214
+ const chain = []; let c = found, guard = 0;
215
+ while (c && guard++ < 20) { chain.unshift(c); c = ix.parentOf(c); }
216
+ const actual = chain.map((o) => o.value).join(SEP);
217
+ const names = chain.map((o) => o.name).join(' > ');
218
+ const vis = ix.visibilityOf(found);
219
+ const ok = actual === path;
220
+ line(' ' + (ok && vis === 'visible' ? '✓' : '✗') + ' ' + path);
221
+ line(' 名称 ' + names + ' (类目: ' + WHITELIST[foundCode] + ')');
222
+ if (!ok) { line(' ✗ 实际路径已变成 ' + actual + ' —— 父级被移动过, 公式匹配不上了'); flag(1); }
223
+ if (vis !== 'visible') { line(' ✗ 当前不可见(' + vis + ') —— 新单据选不到, 但公式仍在引用'); flag(1); }
224
+ const f = (APPROVAL_FIELDS[foundCode] || [])[0];
225
+ if (f) {
226
+ const q = {}; q[f] = path;
227
+ line(' 历史单据精确引用此路径 ' + db.pepsico_material_approval.countDocuments(q) + ' 张');
228
+ }
229
+ });
230
+
231
+ // --- P0-b 补基线 ---
232
+ head('上线补基线(P0-b, 存量记录缺水位线则乱序保护整体失效)');
233
+ const ids = Object.keys(picklists).map((c) => '"' + picklists[c]._id + '"').join(', ');
234
+ line(' 初始导入完成、接口上线那一刻执行(把时间换成真实上线时刻):');
235
+ line('');
236
+ line(' db.picklist_options.updateMany(');
237
+ line(' { picklist: { $in: [' + ids + '] }, ch_changed_at: { $exists: false } },');
238
+ line(' { $set: { ch_changed_at: new Date("<接口上线时刻>") } })');
239
+ line('');
240
+ line(' 不补的话: 代码判据是 if (existing && existing.ch_changed_at), 存量记录整个跳过水位线比较,');
241
+ line(' 半年前的改名事件推进来也会被直接应用, 正确名称被覆盖。');
242
+
243
+ head('结论');
244
+ line(totalProblems === 0
245
+ ? ' 未发现阻塞性问题(⚠ 项仍需按上面的提示处理)。'
246
+ : ' 发现 ' + totalProblems + ' 处需要处理的问题, 见上。');
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 拉取 Content Hub 真实标签树, 校验 issue #448 的 C1 / C2 / C5 约束。
4
+ *
5
+ * 用途: 回答 issue #448 待确认 #1「tagValId 是否满足 C1/C2」——
6
+ * 该项标注「不满足则方案重做」, 联调前必须先验。
7
+ *
8
+ * 凭据只从环境变量读, 不写进仓库:
9
+ * CH_BASE_URL=https://cback.getmogic.com \
10
+ * CH_APP_ID=... CH_APP_SECRET=... CH_TENANT_ID=... CH_WORKSPACE_ID=... \
11
+ * node scripts/probe-content-hub-tags.js
12
+ */
13
+ const https = require('https');
14
+ const { URL } = require('url');
15
+
16
+ const BASE = (process.env.CH_BASE_URL || '').replace(/\/+$/, '');
17
+ const APP_ID = process.env.CH_APP_ID;
18
+ const APP_SECRET = process.env.CH_APP_SECRET;
19
+ const TENANT_ID = process.env.CH_TENANT_ID;
20
+ const WORKSPACE_ID = process.env.CH_WORKSPACE_ID;
21
+
22
+ if (!BASE || !APP_ID || !APP_SECRET || !TENANT_ID || !WORKSPACE_ID) {
23
+ console.error('缺少环境变量: CH_BASE_URL / CH_APP_ID / CH_APP_SECRET / CH_TENANT_ID / CH_WORKSPACE_ID');
24
+ process.exit(2);
25
+ }
26
+
27
+ // 与服务端实现保持一致(见 src/methods/content_hub_tag.ts)
28
+ const TAG_VAL_ID_PATTERN = /^[A-Za-z0-9_]+$/;
29
+ const MAX_TAG_VAL_ID_LENGTH = 64;
30
+ const MAX_TAG_DEPTH = 3;
31
+
32
+ const post = (path, body, headers) => new Promise((resolve, reject) => {
33
+ const payload = JSON.stringify(body);
34
+ const url = new URL(BASE + path);
35
+ const req = https.request({
36
+ hostname: url.hostname,
37
+ port: url.port || 443,
38
+ path: url.pathname + url.search,
39
+ method: 'POST',
40
+ headers: Object.assign({
41
+ 'Content-Type': 'application/json;charset=UTF-8',
42
+ 'Content-Length': Buffer.byteLength(payload)
43
+ }, headers || {})
44
+ }, (res) => {
45
+ let raw = '';
46
+ res.on('data', (c) => { raw += c; });
47
+ res.on('end', () => {
48
+ try {
49
+ resolve({ status: res.statusCode, body: JSON.parse(raw) });
50
+ } catch (e) {
51
+ resolve({ status: res.statusCode, body: raw.slice(0, 400) });
52
+ }
53
+ });
54
+ });
55
+ req.on('error', reject);
56
+ req.write(payload);
57
+ req.end();
58
+ });
59
+
60
+ const TAGS = [
61
+ { key: 'brand-product', label: '品牌/产品', tagId: '223321001114213' },
62
+ { key: 'channel', label: '可使用渠道', tagId: '223321001114944' },
63
+ { key: 'material-type', label: '素材类型', tagId: '223321001114222' }
64
+ // 注意: 对方 API v2 文档里没有「素材授权规则」(223321022150011) 的查询接口,
65
+ // 但它在 issue #448 的托管白名单内 —— 需要向 Content Hub 确认。
66
+ ];
67
+
68
+ (async () => {
69
+ console.log('=== T0 换取 tenant-access-token ===');
70
+ const auth = await post('/creativeSaas/auth/v1/tenantAccessToken/internal', { appId: APP_ID, appSecret: APP_SECRET });
71
+ const token = auth.body && auth.body.data && auth.body.data.tenantAccessToken;
72
+ if (!token) {
73
+ console.error('取 token 失败:', JSON.stringify(auth.body).slice(0, 300));
74
+ process.exit(1);
75
+ }
76
+ console.log(' ok, expire =', auth.body.data.expire);
77
+
78
+ const problems = [];
79
+ for (const tag of TAGS) {
80
+ console.log(`\n=== ${tag.label} (${tag.tagId}) ===`);
81
+ const res = await post(`/creativeSaas/pepsico/compliance/v1/tags/${tag.key}`,
82
+ { tenantId: TENANT_ID, workspaceId: WORKSPACE_ID, keyword: null },
83
+ { 'tenant-access-token': token });
84
+ const list = res.body && res.body.data && (res.body.data.list || res.body.data);
85
+ if (!Array.isArray(list)) {
86
+ console.error(' 返回结构不是数组:', JSON.stringify(res.body).slice(0, 300));
87
+ problems.push(`${tag.label}: 返回结构异常`);
88
+ continue;
89
+ }
90
+
91
+ const seen = new Map();
92
+ let total = 0, maxDepth = 0, disabled = 0;
93
+ const badCharset = [], tooLong = [], tooDeep = [], duplicated = [];
94
+
95
+ const walk = (nodes, depth) => {
96
+ for (const node of nodes || []) {
97
+ total += 1;
98
+ maxDepth = Math.max(maxDepth, depth);
99
+ const id = String(node.id);
100
+ if (!TAG_VAL_ID_PATTERN.test(id)) badCharset.push(`${id} (${node.value})`);
101
+ if (id.length > MAX_TAG_VAL_ID_LENGTH) tooLong.push(id);
102
+ if (depth > MAX_TAG_DEPTH) tooDeep.push(`${id} 第 ${depth} 层 (${node.value})`);
103
+ if (seen.has(id)) duplicated.push(`${id}: ${seen.get(id)} / ${node.value}`);
104
+ else seen.set(id, node.value);
105
+ if (node.disable === true || node.status === 0) disabled += 1;
106
+ walk(node.childTagList, depth + 1);
107
+ }
108
+ };
109
+ walk(list, 1);
110
+
111
+ console.log(` 节点总数 ${total}, 最大层级 ${maxDepth}, 停用节点 ${disabled}`);
112
+ const report = (name, arr, constraint) => {
113
+ if (arr.length) {
114
+ console.log(` ✗ ${constraint} 违反 ${arr.length} 处: ${arr.slice(0, 5).join('; ')}${arr.length > 5 ? ' …' : ''}`);
115
+ problems.push(`${tag.label} ${constraint}: ${arr.length} 处`);
116
+ } else {
117
+ console.log(` ✓ ${constraint}`);
118
+ }
119
+ };
120
+ report('charset', badCharset, 'C2 只含字母数字下划线');
121
+ report('length', tooLong, `C2 长度 ≤ ${MAX_TAG_VAL_ID_LENGTH}`);
122
+ report('depth', tooDeep, `C5 层级 ≤ ${MAX_TAG_DEPTH}`);
123
+ report('dup', duplicated, 'C1 ID 唯一');
124
+ }
125
+
126
+ console.log('\n================ 结论 ================');
127
+ if (problems.length) {
128
+ console.log('发现约束冲突, 联调前必须先解决:');
129
+ problems.forEach((p) => console.log(' - ' + p));
130
+ process.exit(1);
131
+ }
132
+ console.log('三个标签类目均满足 C1 / C2 / C5, 可以按现方案联调。');
133
+ console.log('另需向 Content Hub 确认: 素材授权规则 223321022150011 的查询接口(v2 文档未提供)。');
134
+ })().catch((e) => { console.error('探测失败:', e.message); process.exit(1); });
@@ -1,18 +0,0 @@
1
- name: has_detail_page
2
- label: 素材是否包含商品详情页
3
- type: select
4
- data_type: text
5
- group: 基本信息
6
- inlineHelpText: 涉及到商品详情页的素材,请关联pls文件
7
- sort_no: 805
8
- is_wide: true
9
- required: true
10
- amis:
11
- type: button-group-select
12
- multiple: false
13
- options:
14
- - label: 是
15
- value: 'yes'
16
- - label: 否
17
- value: 'no'
18
- visible_on: (classification == 'MD-PO1-PO15' || classification == 'SLS-PO2-PO25') && form_version == 'v1'