@lightcone-ai/daemon 0.14.16 → 0.14.18
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.
- package/mcp-servers/official/keyword-research/index.js +95 -56
- package/mcp-servers/official/keyword-research/keyword-fixtures.json +58 -0
- package/mcp-servers/official/page-understanding/index.js +19 -0
- package/mcp-servers/official/page-understanding/manifest.json +8 -0
- package/mcp-servers/official/platform-policy-db/index.js +117 -163
- package/mcp-servers/official/platform-policy-db/policy-fixtures.json +170 -0
- package/mcp-servers/official/video-narration-planner/core.js +154 -116
- package/mcp-servers/official/video-narration-planner/index.js +29 -0
- package/mcp-servers/official/video-narration-planner/manifest.json +14 -0
- package/mcp-servers/official/video-narration-planner/planner-config.json +112 -0
- package/mcp-servers/official-common/tool-access-policy.js +90 -0
- package/package.json +1 -1
- package/src/_vendor/video/recorder/index.js +1 -41
- package/src/_vendor/video/recorder/plan-executor.js +2 -10
- package/src/agent-manager.js +205 -22
- package/src/chat-bridge.js +55 -12
- package/src/index.js +2 -1
- package/src/lifecycle-protocol.js +51 -0
|
@@ -1,66 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { startFixtureServer } from '../../official-common/server.js';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
6
|
+
const KEYWORD_FIXTURES = loadKeywordFixtures();
|
|
7
|
+
const FIXTURE_META = KEYWORD_FIXTURES.fixtureMeta;
|
|
8
|
+
const PLATFORM_ALIAS = KEYWORD_FIXTURES.platformAliases;
|
|
9
|
+
const PLATFORM_HINTS = KEYWORD_FIXTURES.platformHints;
|
|
10
|
+
const INTENT_BRANCHES = KEYWORD_FIXTURES.intentBranches;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
douyin: 'douyin',
|
|
16
|
-
tiktok_cn: 'douyin',
|
|
17
|
-
wechat: 'wechat-mp',
|
|
18
|
-
wechat_mp: 'wechat-mp',
|
|
19
|
-
wechatmp: 'wechat-mp',
|
|
20
|
-
gzh: 'wechat-mp',
|
|
21
|
-
'\u516c\u4f17\u53f7': 'wechat-mp',
|
|
22
|
-
all: 'all',
|
|
23
|
-
});
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
function deepFreeze(value) {
|
|
17
|
+
if (!value || typeof value !== 'object') return value;
|
|
18
|
+
if (Object.isFrozen(value)) return value;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
value.forEach(item => deepFreeze(item));
|
|
21
|
+
return Object.freeze(value);
|
|
22
|
+
}
|
|
23
|
+
for (const key of Object.keys(value)) {
|
|
24
|
+
deepFreeze(value[key]);
|
|
25
|
+
}
|
|
26
|
+
return Object.freeze(value);
|
|
27
|
+
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
29
|
+
function toToken(value) {
|
|
30
|
+
return String(value ?? '').trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeAliasMap(rawAliases) {
|
|
34
|
+
if (!isPlainObject(rawAliases)) return { all: 'all' };
|
|
35
|
+
const aliases = {};
|
|
36
|
+
for (const [key, value] of Object.entries(rawAliases)) {
|
|
37
|
+
const alias = toToken(key).toLowerCase();
|
|
38
|
+
const target = toToken(value).toLowerCase();
|
|
39
|
+
if (!alias || !target) continue;
|
|
40
|
+
aliases[alias] = target;
|
|
41
|
+
}
|
|
42
|
+
if (!Object.hasOwn(aliases, 'all')) aliases.all = 'all';
|
|
43
|
+
return aliases;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizePlatformHints(rawHints) {
|
|
47
|
+
if (!isPlainObject(rawHints)) return { all: [] };
|
|
48
|
+
const hints = {};
|
|
49
|
+
for (const [key, value] of Object.entries(rawHints)) {
|
|
50
|
+
const platform = toToken(key).toLowerCase();
|
|
51
|
+
if (!platform || !Array.isArray(value)) continue;
|
|
52
|
+
hints[platform] = value.map(item => toToken(item)).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(hints.all)) hints.all = [];
|
|
55
|
+
return hints;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeIntentBranches(rawBranches) {
|
|
59
|
+
if (!Array.isArray(rawBranches)) return [];
|
|
60
|
+
return rawBranches
|
|
61
|
+
.map((branch) => {
|
|
62
|
+
if (!isPlainObject(branch)) return null;
|
|
63
|
+
const id = toToken(branch.id);
|
|
64
|
+
const suffix = toToken(branch.suffix);
|
|
65
|
+
const weight = Number(branch.weight);
|
|
66
|
+
const childTemplates = Array.isArray(branch.child_templates)
|
|
67
|
+
? branch.child_templates.map(item => toToken(item)).filter(Boolean)
|
|
68
|
+
: [];
|
|
69
|
+
if (!id || !suffix || !Number.isFinite(weight) || childTemplates.length === 0) return null;
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
suffix,
|
|
73
|
+
weight,
|
|
74
|
+
child_templates: childTemplates,
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadKeywordFixtures() {
|
|
81
|
+
let parsed = null;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(
|
|
84
|
+
readFileSync(new URL('./keyword-fixtures.json', import.meta.url), 'utf8')
|
|
85
|
+
);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`keyword_fixture_load_failed:${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
if (!isPlainObject(parsed)) {
|
|
90
|
+
throw new Error('keyword_fixture_invalid:root_object_required');
|
|
91
|
+
}
|
|
92
|
+
const intentBranches = normalizeIntentBranches(parsed.intent_branches);
|
|
93
|
+
if (intentBranches.length === 0) {
|
|
94
|
+
throw new Error('keyword_fixture_invalid:intent_branches_required');
|
|
95
|
+
}
|
|
96
|
+
return deepFreeze({
|
|
97
|
+
fixtureMeta: isPlainObject(parsed.fixture_meta) ? parsed.fixture_meta : {},
|
|
98
|
+
platformAliases: normalizeAliasMap(parsed.platform_aliases),
|
|
99
|
+
platformHints: normalizePlatformHints(parsed.platform_hints),
|
|
100
|
+
intentBranches,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
64
103
|
|
|
65
104
|
function normalizePlatform(value) {
|
|
66
105
|
const raw = String(value ?? '').trim().toLowerCase();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fixture_meta": {
|
|
3
|
+
"mode": "fixture",
|
|
4
|
+
"as_of": "2026-04-29",
|
|
5
|
+
"capability": "keyword-research",
|
|
6
|
+
"disclaimer": "Keyword stats are deterministic fixtures for planning rehearsal, not live platform telemetry."
|
|
7
|
+
},
|
|
8
|
+
"platform_aliases": {
|
|
9
|
+
"xiaohongshu": "xhs",
|
|
10
|
+
"redbook": "xhs",
|
|
11
|
+
"douyin": "douyin",
|
|
12
|
+
"tiktok_cn": "douyin",
|
|
13
|
+
"wechat": "wechat-mp",
|
|
14
|
+
"wechat_mp": "wechat-mp",
|
|
15
|
+
"wechatmp": "wechat-mp",
|
|
16
|
+
"gzh": "wechat-mp",
|
|
17
|
+
"公众号": "wechat-mp",
|
|
18
|
+
"all": "all"
|
|
19
|
+
},
|
|
20
|
+
"platform_hints": {
|
|
21
|
+
"all": ["趋势", "案例", "模板"],
|
|
22
|
+
"xhs": ["小红书笔记", "标题写法", "封面策略"],
|
|
23
|
+
"douyin": ["短视频脚本", "完播率", "首屏开场"],
|
|
24
|
+
"wechat-mp": ["公众号长文", "转化漏斗", "私域承接"]
|
|
25
|
+
},
|
|
26
|
+
"intent_branches": [
|
|
27
|
+
{
|
|
28
|
+
"id": "pain_point",
|
|
29
|
+
"suffix": "痛点",
|
|
30
|
+
"weight": 1,
|
|
31
|
+
"child_templates": ["{seed} 常见问题", "{seed} 为什么没效果", "{seed} 误区"]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "tutorial",
|
|
35
|
+
"suffix": "教程",
|
|
36
|
+
"weight": 0.94,
|
|
37
|
+
"child_templates": ["{seed} 入门", "{seed} 操作步骤", "{seed} 执行清单"]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "template",
|
|
41
|
+
"suffix": "模板",
|
|
42
|
+
"weight": 0.9,
|
|
43
|
+
"child_templates": ["{seed} 文案模板", "{seed} 选题模板", "{seed} 表格模板"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "case_study",
|
|
47
|
+
"suffix": "案例",
|
|
48
|
+
"weight": 0.88,
|
|
49
|
+
"child_templates": ["{seed} 成功案例", "{seed} 失败复盘", "{seed} A/B 测试"]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "comparison",
|
|
53
|
+
"suffix": "对比",
|
|
54
|
+
"weight": 0.84,
|
|
55
|
+
"child_templates": ["{seed} 工具对比", "{seed} 方法对比", "{seed} 哪个更好"]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -8,6 +8,21 @@ import {
|
|
|
8
8
|
analyzePageFromHtmlFixture,
|
|
9
9
|
validatePageUnderstanding,
|
|
10
10
|
} from '../../../../src/video/understanding/index.js';
|
|
11
|
+
import {
|
|
12
|
+
findToolBlockRule,
|
|
13
|
+
loadToolBlockRulesFromManifest,
|
|
14
|
+
} from '../../official-common/tool-access-policy.js';
|
|
15
|
+
|
|
16
|
+
const TOOL_BLOCK_RULES = loadToolBlockRulesFromManifest(new URL('./manifest.json', import.meta.url));
|
|
17
|
+
|
|
18
|
+
function resolveAnalyzePageBlockMessage(env = process.env) {
|
|
19
|
+
const matchedRule = findToolBlockRule(TOOL_BLOCK_RULES, {
|
|
20
|
+
toolName: 'analyze_page',
|
|
21
|
+
workspaceId: env?.WORKSPACE_ID,
|
|
22
|
+
agentId: env?.AGENT_ID,
|
|
23
|
+
});
|
|
24
|
+
return matchedRule?.message ?? null;
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
function toText(payload) {
|
|
13
28
|
return {
|
|
@@ -49,6 +64,10 @@ server.tool(
|
|
|
49
64
|
},
|
|
50
65
|
async ({ url, persona = '', options = {} }) => {
|
|
51
66
|
try {
|
|
67
|
+
const blockedMessage = resolveAnalyzePageBlockMessage();
|
|
68
|
+
if (blockedMessage) {
|
|
69
|
+
return toError(blockedMessage);
|
|
70
|
+
}
|
|
52
71
|
let payload;
|
|
53
72
|
if (options?.fixture_mode) {
|
|
54
73
|
payload = await analyzePageFromHtmlFixture({
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
"tool_declarations": [
|
|
8
8
|
{ "name": "analyze_page", "classification": "cacheable" }
|
|
9
9
|
],
|
|
10
|
+
"tool_block_rules": [
|
|
11
|
+
{
|
|
12
|
+
"workspace_id": "ae63cc9e-feff-4d7e-a62e-a7a7c5fd69d9",
|
|
13
|
+
"agent_id": "91a45fd7-ce5f-4da6-9b27-e34bf7b7c0e2",
|
|
14
|
+
"tools": ["analyze_page"],
|
|
15
|
+
"message": "analyze_page blocked for editor_in_chief in CvMax. In this workspace, @short_video_scripter owns video production intake and page analysis."
|
|
16
|
+
}
|
|
17
|
+
],
|
|
10
18
|
"smoke_test": {
|
|
11
19
|
"tool": "analyze_page",
|
|
12
20
|
"arguments": {
|
|
@@ -1,175 +1,129 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { startFixtureServer } from '../../official-common/server.js';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
disclaimer: 'Policy fixtures are static snapshots; always verify against latest platform notices.',
|
|
10
|
-
});
|
|
6
|
+
const POLICY_FIXTURES = loadPolicyFixtures();
|
|
7
|
+
const FIXTURE_META = POLICY_FIXTURES.fixtureMeta;
|
|
8
|
+
const PLATFORM_ALIASES = POLICY_FIXTURES.platformAliases;
|
|
9
|
+
const PLATFORM_POLICY_DATA = POLICY_FIXTURES.platformPolicyData;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
id
|
|
56
|
-
term
|
|
57
|
-
severity
|
|
58
|
-
reason:
|
|
59
|
-
action:
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
ai: '建议标注 AI 生成来源',
|
|
124
|
-
},
|
|
125
|
-
sensitive_terms: Object.freeze([
|
|
126
|
-
{
|
|
127
|
-
id: 'wechatmp-clickbait-share',
|
|
128
|
-
term: '不转不是中国人',
|
|
129
|
-
severity: 'blocker',
|
|
130
|
-
reason: '诱导分享属于明确违规。',
|
|
131
|
-
action: 'remove_manipulative_phrase',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: 'wechatmp-political-rumor',
|
|
135
|
-
term: '内部通知',
|
|
136
|
-
severity: 'warn',
|
|
137
|
-
reason: '疑似无来源权威口径,需补证据。',
|
|
138
|
-
action: 'add_citation_or_remove',
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: 'wechatmp-medical-guarantee',
|
|
142
|
-
term: '根治',
|
|
143
|
-
severity: 'warn',
|
|
144
|
-
reason: '医疗强承诺需免责声明并谨慎表达。',
|
|
145
|
-
action: 'add_disclaimer',
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
id: 'wechatmp-finance-guarantee',
|
|
149
|
-
term: '稳赚',
|
|
150
|
-
severity: 'blocker',
|
|
151
|
-
reason: '金融收益承诺违规。',
|
|
152
|
-
action: 'remove_claim',
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: 'wechatmp-fake-scarcity',
|
|
156
|
-
term: '最后一天',
|
|
157
|
-
severity: 'warn',
|
|
158
|
-
reason: '虚假稀缺文案易触发风控。',
|
|
159
|
-
action: 'verify_or_tone_down',
|
|
160
|
-
},
|
|
161
|
-
]),
|
|
162
|
-
}),
|
|
163
|
-
});
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function deepFreeze(value) {
|
|
16
|
+
if (!value || typeof value !== 'object') return value;
|
|
17
|
+
if (Object.isFrozen(value)) return value;
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
value.forEach(item => deepFreeze(item));
|
|
20
|
+
return Object.freeze(value);
|
|
21
|
+
}
|
|
22
|
+
for (const key of Object.keys(value)) {
|
|
23
|
+
deepFreeze(value[key]);
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toToken(value) {
|
|
29
|
+
return String(value ?? '').trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeAliasMap(rawAliases) {
|
|
33
|
+
if (!isPlainObject(rawAliases)) return {};
|
|
34
|
+
const aliases = {};
|
|
35
|
+
for (const [key, value] of Object.entries(rawAliases)) {
|
|
36
|
+
const alias = toToken(key).toLowerCase();
|
|
37
|
+
const target = toToken(value).toLowerCase();
|
|
38
|
+
if (!alias || !target) continue;
|
|
39
|
+
aliases[alias] = target;
|
|
40
|
+
}
|
|
41
|
+
return aliases;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeSensitiveTerms(rawTerms) {
|
|
45
|
+
if (!Array.isArray(rawTerms)) return [];
|
|
46
|
+
return rawTerms
|
|
47
|
+
.map((item) => {
|
|
48
|
+
if (!isPlainObject(item)) return null;
|
|
49
|
+
const id = toToken(item.id);
|
|
50
|
+
const term = toToken(item.term);
|
|
51
|
+
const severity = toToken(item.severity).toLowerCase();
|
|
52
|
+
if (!id || !term || !severity) return null;
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
term,
|
|
56
|
+
severity,
|
|
57
|
+
reason: toToken(item.reason),
|
|
58
|
+
action: toToken(item.action),
|
|
59
|
+
};
|
|
60
|
+
})
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeRequiredLabels(raw) {
|
|
65
|
+
if (!isPlainObject(raw)) return {};
|
|
66
|
+
const output = {};
|
|
67
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
68
|
+
const normalizedKey = toToken(key);
|
|
69
|
+
const normalizedValue = toToken(value);
|
|
70
|
+
if (!normalizedKey || !normalizedValue) continue;
|
|
71
|
+
output[normalizedKey] = normalizedValue;
|
|
72
|
+
}
|
|
73
|
+
return output;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizePlatformPolicyData(rawData) {
|
|
77
|
+
if (!isPlainObject(rawData)) return {};
|
|
78
|
+
const normalized = {};
|
|
79
|
+
for (const [platformKey, rawEntry] of Object.entries(rawData)) {
|
|
80
|
+
if (!isPlainObject(rawEntry)) continue;
|
|
81
|
+
const platform = toToken(rawEntry.platform || platformKey).toLowerCase();
|
|
82
|
+
if (!platform) continue;
|
|
83
|
+
normalized[platform] = {
|
|
84
|
+
platform,
|
|
85
|
+
display_name: toToken(rawEntry.display_name),
|
|
86
|
+
policy_version: toToken(rawEntry.policy_version),
|
|
87
|
+
updated_at: toToken(rawEntry.updated_at),
|
|
88
|
+
ai_label_required: rawEntry.ai_label_required === true,
|
|
89
|
+
ad_disclosure_required: rawEntry.ad_disclosure_required === true,
|
|
90
|
+
policy_window_days: Number.isFinite(Number(rawEntry.policy_window_days))
|
|
91
|
+
? Math.max(1, Math.trunc(Number(rawEntry.policy_window_days)))
|
|
92
|
+
: 7,
|
|
93
|
+
required_labels: normalizeRequiredLabels(rawEntry.required_labels),
|
|
94
|
+
sensitive_terms: normalizeSensitiveTerms(rawEntry.sensitive_terms),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function loadPolicyFixtures() {
|
|
101
|
+
let parsed = null;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(
|
|
104
|
+
readFileSync(new URL('./policy-fixtures.json', import.meta.url), 'utf8')
|
|
105
|
+
);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`platform_policy_fixture_load_failed:${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
if (!isPlainObject(parsed)) {
|
|
110
|
+
throw new Error('platform_policy_fixture_invalid:root_object_required');
|
|
111
|
+
}
|
|
112
|
+
const platformPolicyData = normalizePlatformPolicyData(parsed.platform_policy_data);
|
|
113
|
+
if (Object.keys(platformPolicyData).length === 0) {
|
|
114
|
+
throw new Error('platform_policy_fixture_invalid:platform_policy_data_required');
|
|
115
|
+
}
|
|
116
|
+
return deepFreeze({
|
|
117
|
+
fixtureMeta: isPlainObject(parsed.fixture_meta) ? parsed.fixture_meta : {},
|
|
118
|
+
platformAliases: normalizeAliasMap(parsed.platform_aliases),
|
|
119
|
+
platformPolicyData,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
164
122
|
|
|
165
123
|
function normalizePlatform(value) {
|
|
166
124
|
const raw = String(value ?? '').trim().toLowerCase();
|
|
167
125
|
if (!raw) return '';
|
|
168
|
-
if (
|
|
169
|
-
if (raw === 'wechat' || raw === 'wechatmp' || raw === 'wechat_mp' || raw === 'gzh' || raw === '公众号') {
|
|
170
|
-
return 'wechat-mp';
|
|
171
|
-
}
|
|
172
|
-
if (raw === 'tiktok_cn') return 'douyin';
|
|
126
|
+
if (Object.hasOwn(PLATFORM_ALIASES, raw)) return PLATFORM_ALIASES[raw];
|
|
173
127
|
return raw;
|
|
174
128
|
}
|
|
175
129
|
|