sdd-flow-kit 1.0.16 → 1.0.22
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/README.md +22 -3
- package/dist/cli.js +24 -4
- package/dist/core/inferProject.js +152 -0
- package/dist/core/opsxDelivery.js +248 -0
- package/dist/postinstall.js +57 -39
- package/dist/steps/doctor.js +24 -1
- package/dist/steps/setupProject.js +24 -68
- package/dist/steps/startFlow.js +15 -6
- package/package.json +1 -1
- package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +24 -10
- package/src/templates/artifacts/06-agent-skill.template.md +30 -8
- package/src/templates/skills/confluence-doc.py.template +248 -66
- package/src/templates/skills/doc-skill.SKILL.md.template +13 -4
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from urllib.parse import urljoin, urlparse, unquote
|
|
8
|
+
from urllib.parse import quote, urljoin, urlparse, unquote
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from playwright.sync_api import sync_playwright
|
|
@@ -24,6 +24,11 @@ def env(name: str, default: str | None = None) -> str:
|
|
|
24
24
|
return str(v).strip()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def env_bool(name: str, default: bool = False) -> bool:
|
|
28
|
+
v = env(name, "1" if default else "0").lower()
|
|
29
|
+
return v in ("1", "true", "yes", "on")
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
def load_env_file(file_path: Path) -> None:
|
|
28
33
|
"""轻量加载 .env 文件(不依赖 python-dotenv)。"""
|
|
29
34
|
if not file_path.exists() or not file_path.is_file():
|
|
@@ -57,11 +62,13 @@ def version_slug(prefix: str, version: str) -> str:
|
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
def search_keywords(prefix: str, version: str) -> list[str]:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
"""固定顺序(与用户 Confluence 全局搜索习惯一致)。无论用户输入 ADI 2.3.2 还是 ADI_V2.3.2 都走这一套。"""
|
|
66
|
+
return [
|
|
67
|
+
f"{prefix}_V{version}",
|
|
68
|
+
f"{prefix}-V{version}",
|
|
69
|
+
f"{prefix}_{version}",
|
|
70
|
+
f"{prefix}-{version}",
|
|
71
|
+
]
|
|
65
72
|
|
|
66
73
|
|
|
67
74
|
def sanitize_filename(name: str) -> str:
|
|
@@ -116,23 +123,54 @@ def href_path_matches_keyword(href: str, keyword: str) -> bool:
|
|
|
116
123
|
return kw in path or compact in path_compact
|
|
117
124
|
|
|
118
125
|
|
|
119
|
-
def
|
|
120
|
-
|
|
126
|
+
def score_link_match(text: str, href: str, prefix: str, version: str, keyword: str) -> int:
|
|
127
|
+
"""对搜索结果链接打分;>=2 视为命中。"""
|
|
128
|
+
t = (text or "").lower()
|
|
129
|
+
path = unquote(urlparse(href).path).lower()
|
|
130
|
+
kw = keyword.lower()
|
|
131
|
+
p = prefix.lower()
|
|
132
|
+
v_compact = version.replace(".", "")
|
|
133
|
+
t_compact = re.sub(r"\s+", "", t)
|
|
134
|
+
path_compact = path.replace(".", "").replace("-", "").replace("_", "")
|
|
135
|
+
|
|
136
|
+
score = 0
|
|
137
|
+
if kw in t:
|
|
138
|
+
score += 4
|
|
139
|
+
if kw.replace(".", "") in t_compact:
|
|
140
|
+
score += 3
|
|
141
|
+
if p in t and version in t:
|
|
142
|
+
score += 4
|
|
143
|
+
if p in t and v_compact in t_compact:
|
|
144
|
+
score += 3
|
|
145
|
+
if href_path_matches_keyword(href, keyword):
|
|
146
|
+
score += 3
|
|
147
|
+
slug = f"{p}v{v_compact}"
|
|
148
|
+
if slug in path_compact or f"{p}-v{v_compact}" in path or f"{p}_v{v_compact}" in path:
|
|
149
|
+
score += 4
|
|
150
|
+
return score
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def collect_search_candidates(
|
|
154
|
+
page, base_url: str, prefix: str, version: str, keyword: str
|
|
155
|
+
) -> list[tuple[str, str, int]]:
|
|
156
|
+
"""返回 [(url, title, score), ...]"""
|
|
121
157
|
seen: set[str] = set()
|
|
158
|
+
scored: list[tuple[int, str, str]] = []
|
|
159
|
+
|
|
122
160
|
selectors = [
|
|
123
161
|
".search-result .title a",
|
|
124
162
|
".search-result-title a",
|
|
125
163
|
"#search-results .gs-title a",
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
"
|
|
164
|
+
".search-results-panel a",
|
|
165
|
+
"a[data-linked-resource-id]",
|
|
166
|
+
"a[href*='/pages/']",
|
|
167
|
+
"a[href*='/display/']",
|
|
130
168
|
]
|
|
131
|
-
scored: list[tuple[int, str]] = []
|
|
132
169
|
|
|
133
170
|
for selector in selectors:
|
|
134
171
|
links = page.locator(selector)
|
|
135
|
-
|
|
172
|
+
count = min(links.count(), 200)
|
|
173
|
+
for i in range(count):
|
|
136
174
|
href = links.nth(i).get_attribute("href") or ""
|
|
137
175
|
raw_text = (links.nth(i).inner_text() or "").strip()
|
|
138
176
|
title_text = raw_text.split("\n")[0].strip()
|
|
@@ -145,23 +183,66 @@ def find_search_result(page, base_url: str, keyword: str):
|
|
|
145
183
|
continue
|
|
146
184
|
seen.add(full_url)
|
|
147
185
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
continue
|
|
152
|
-
|
|
153
|
-
score = 2 + (1 if path_hit else 0)
|
|
154
|
-
scored.append((score, full_url))
|
|
186
|
+
s = score_link_match(text, href, prefix, version, keyword)
|
|
187
|
+
if s >= 2:
|
|
188
|
+
scored.append((s, full_url, text[:120]))
|
|
155
189
|
|
|
156
|
-
if not scored:
|
|
157
|
-
return None
|
|
158
190
|
scored.sort(key=lambda x: x[0], reverse=True)
|
|
159
|
-
|
|
160
|
-
print(f" 找到文档链接: {best[:100]}...")
|
|
161
|
-
return best
|
|
191
|
+
return [(u, t, s) for s, u, t in scored]
|
|
162
192
|
|
|
163
193
|
|
|
164
|
-
def
|
|
194
|
+
def read_breadcrumb_text(page) -> str:
|
|
195
|
+
parts: list[str] = []
|
|
196
|
+
for selector in [
|
|
197
|
+
"#breadcrumbs li",
|
|
198
|
+
"#breadcrumbs a",
|
|
199
|
+
"ol.aui-nav-breadcrumbs li",
|
|
200
|
+
".breadcrumbs li",
|
|
201
|
+
"nav[aria-label='Breadcrumbs'] li",
|
|
202
|
+
]:
|
|
203
|
+
loc = page.locator(selector)
|
|
204
|
+
for i in range(min(loc.count(), 20)):
|
|
205
|
+
txt = (loc.nth(i).inner_text() or "").strip()
|
|
206
|
+
if txt and txt not in parts:
|
|
207
|
+
parts.append(txt)
|
|
208
|
+
return " > ".join(parts)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def is_prd_page(page, page_url: str) -> bool:
|
|
212
|
+
"""检查页面面包屑/父级是否属于 PRD(需求文档)。"""
|
|
213
|
+
page.goto(page_url, wait_until="networkidle", timeout=60000)
|
|
214
|
+
page.wait_for_timeout(1200)
|
|
215
|
+
|
|
216
|
+
crumbs = read_breadcrumb_text(page)
|
|
217
|
+
crumbs_upper = crumbs.upper()
|
|
218
|
+
if re.search(r"\bPRD\b", crumbs_upper) or "需求" in crumbs or "PRD" in crumbs_upper:
|
|
219
|
+
print(f" ✓ PRD 父级命中: {crumbs[:120]}")
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
title = (page.locator("#title-text").inner_text() or "").strip()
|
|
224
|
+
except Exception:
|
|
225
|
+
title = ""
|
|
226
|
+
title_upper = title.upper()
|
|
227
|
+
if re.search(r"\bPRD\b", title_upper) or "需求文档" in title:
|
|
228
|
+
print(f" ✓ 标题含 PRD/需求: {title[:80]}")
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
print(f" ✗ 非 PRD 父级: {crumbs[:120] or title[:80]}")
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def run_search_dosearch(page, base_url: str, prefix: str, version: str, keyword: str) -> list[tuple[str, str, int]]:
|
|
236
|
+
url = f"{base_url.rstrip('/')}/dosearchsite.action?queryString={quote(keyword)}"
|
|
237
|
+
print(f" → 全站搜索: {url}")
|
|
238
|
+
page.goto(url, wait_until="networkidle", timeout=60000)
|
|
239
|
+
page.wait_for_timeout(2500)
|
|
240
|
+
if is_login_page(page):
|
|
241
|
+
return []
|
|
242
|
+
return collect_search_candidates(page, base_url, prefix, version, keyword)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_search_quick(page, base_url: str, prefix: str, version: str, keyword: str) -> list[tuple[str, str, int]]:
|
|
165
246
|
search_selector = None
|
|
166
247
|
for selector in [
|
|
167
248
|
"#quick-search-query",
|
|
@@ -176,14 +257,92 @@ def run_search(page, base_url: str, keyword: str) -> str | None:
|
|
|
176
257
|
search_selector = selector
|
|
177
258
|
break
|
|
178
259
|
if not search_selector:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
search_selector = "input[type='text']"
|
|
260
|
+
return []
|
|
261
|
+
print(f" → 顶栏搜索框: {search_selector} 关键词: {keyword}")
|
|
182
262
|
page.fill(search_selector, keyword, timeout=8000)
|
|
183
263
|
page.keyboard.press("Enter")
|
|
184
264
|
page.wait_for_load_state("networkidle", timeout=30000)
|
|
185
|
-
page.wait_for_timeout(
|
|
186
|
-
return
|
|
265
|
+
page.wait_for_timeout(2500)
|
|
266
|
+
return collect_search_candidates(page, base_url, prefix, version, keyword)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def resolve_prd_target(
|
|
270
|
+
page, base_url: str, prefix: str, version: str, keywords: list[str]
|
|
271
|
+
) -> tuple[str | None, str | None, str]:
|
|
272
|
+
"""依次用每个关键词搜索,直到找到父级为 PRD 的页面。"""
|
|
273
|
+
seen_urls: set[str] = set()
|
|
274
|
+
fallback: tuple[str, str, str] | None = None # url, title, keyword
|
|
275
|
+
|
|
276
|
+
for kw in keywords:
|
|
277
|
+
print(f"\n🔍 搜索关键词: {kw}")
|
|
278
|
+
batches = [
|
|
279
|
+
("dosearchsite", run_search_dosearch(page, base_url, prefix, version, kw)),
|
|
280
|
+
("quick-search", run_search_quick(page, base_url, prefix, version, kw)),
|
|
281
|
+
]
|
|
282
|
+
for method_name, candidates in batches:
|
|
283
|
+
if not candidates:
|
|
284
|
+
print(f" ({method_name}) 无结果")
|
|
285
|
+
continue
|
|
286
|
+
print(f" ({method_name}) {len(candidates)} 条候选,检查 PRD 父级...")
|
|
287
|
+
for url, title, _score in candidates:
|
|
288
|
+
if url in seen_urls:
|
|
289
|
+
continue
|
|
290
|
+
seen_urls.add(url)
|
|
291
|
+
print(f" · 候选: {title[:70]}")
|
|
292
|
+
if is_prd_page(page, url):
|
|
293
|
+
return url, kw, title
|
|
294
|
+
if fallback is None:
|
|
295
|
+
fallback = (url, kw, title)
|
|
296
|
+
|
|
297
|
+
if fallback:
|
|
298
|
+
url, kw, title = fallback
|
|
299
|
+
print(f" ⚠ 未找到 PRD 父级,回退使用: {title[:70]}")
|
|
300
|
+
return url, kw, title
|
|
301
|
+
return None, None, ""
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def is_login_page(page) -> bool:
|
|
305
|
+
url = page.url.lower()
|
|
306
|
+
if "login.action" in url or "/login" in url:
|
|
307
|
+
return True
|
|
308
|
+
if page.locator("#login-form, form#login, #username-field").count() > 0:
|
|
309
|
+
if page.locator("#login-button, #login").count() > 0:
|
|
310
|
+
return True
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def ensure_logged_in(page, base_url: str, username: str, password: str) -> None:
|
|
315
|
+
login_url = f"{base_url.rstrip('/')}/login.action"
|
|
316
|
+
page.goto(login_url, wait_until="networkidle", timeout=60000)
|
|
317
|
+
if not is_login_page(page):
|
|
318
|
+
print("✅ 已处于登录状态")
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
if not username or not password:
|
|
322
|
+
raise RuntimeError("需要登录但未提供 CONFLUENCE_USERNAME / CONFLUENCE_PASSWORD")
|
|
323
|
+
|
|
324
|
+
print("🔐 正在登录 Confluence...")
|
|
325
|
+
page.wait_for_selector("#username-field, input[name='os_username']", timeout=15000)
|
|
326
|
+
user_sel = "#username-field" if page.locator("#username-field").count() else "input[name='os_username']"
|
|
327
|
+
pass_sel = "#password-field" if page.locator("#password-field").count() else "input[name='os_password']"
|
|
328
|
+
btn_sel = "#login-button" if page.locator("#login-button").count() else "#login"
|
|
329
|
+
page.fill(user_sel, username, timeout=8000)
|
|
330
|
+
page.fill(pass_sel, password, timeout=8000)
|
|
331
|
+
page.click(btn_sel)
|
|
332
|
+
page.wait_for_load_state("networkidle", timeout=45000)
|
|
333
|
+
page.wait_for_timeout(1500)
|
|
334
|
+
|
|
335
|
+
if is_login_page(page):
|
|
336
|
+
err = ""
|
|
337
|
+
for sel in [".aui-message.error", ".aui-message.aui-message-error", "#login-error"]:
|
|
338
|
+
loc = page.locator(sel)
|
|
339
|
+
if loc.count():
|
|
340
|
+
err = (loc.first.inner_text() or "").strip()
|
|
341
|
+
break
|
|
342
|
+
raise RuntimeError(
|
|
343
|
+
f"Confluence 登录失败(仍在登录页)。请检查账号密码或 .env 中的 CONFLUENCE_* 配置。{err}"
|
|
344
|
+
)
|
|
345
|
+
print("✅ 登录成功")
|
|
187
346
|
|
|
188
347
|
|
|
189
348
|
def main():
|
|
@@ -192,10 +351,15 @@ def main():
|
|
|
192
351
|
print(" 用法: python3 confluence-doc.py <版本号>")
|
|
193
352
|
sys.exit(1)
|
|
194
353
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
354
|
+
# 从脚本目录向上查找 .env(兼容 adInsight-web/frontend 子包)
|
|
355
|
+
script_dir = Path(__file__).resolve().parent
|
|
356
|
+
cur = script_dir
|
|
357
|
+
for _ in range(8):
|
|
358
|
+
for env_name in [".env.pre", ".env", ".env.local", ".env.development", ".env.test"]:
|
|
359
|
+
load_env_file(cur / env_name)
|
|
360
|
+
if cur.parent == cur:
|
|
361
|
+
break
|
|
362
|
+
cur = cur.parent
|
|
199
363
|
|
|
200
364
|
base_url = env("CONFLUENCE_BASE_URL", "https://confluence.huan.tv") or "https://confluence.huan.tv"
|
|
201
365
|
search_url = env("CONFLUENCE_SEARCH_URL") or f"{base_url.rstrip('/')}/#all-updates"
|
|
@@ -229,8 +393,21 @@ def main():
|
|
|
229
393
|
print(f"📁 输出目录: {docs_root}")
|
|
230
394
|
print(f"🖼️ 图片目录: {version_dir}")
|
|
231
395
|
|
|
396
|
+
headed = env_bool("CONFLUENCE_HEADED") or env("CONFLUENCE_HEADLESS", "1").lower() in (
|
|
397
|
+
"0",
|
|
398
|
+
"false",
|
|
399
|
+
"no",
|
|
400
|
+
)
|
|
401
|
+
headless = not headed
|
|
402
|
+
slow_mo = int(env("CONFLUENCE_SLOW_MO", "400" if headed else "0") or "0")
|
|
403
|
+
|
|
404
|
+
print(f"🖥️ 浏览器: headless={headless}, slow_mo={slow_mo}ms(有头模式: CONFLUENCE_HEADED=1)")
|
|
405
|
+
|
|
232
406
|
with sync_playwright() as p:
|
|
233
|
-
|
|
407
|
+
launch_opts: dict = {"headless": headless}
|
|
408
|
+
if slow_mo > 0:
|
|
409
|
+
launch_opts["slow_mo"] = slow_mo
|
|
410
|
+
browser = p.chromium.launch(**launch_opts)
|
|
234
411
|
context = browser.new_context(
|
|
235
412
|
user_agent=(
|
|
236
413
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
@@ -239,44 +416,44 @@ def main():
|
|
|
239
416
|
)
|
|
240
417
|
page = context.new_page()
|
|
241
418
|
try:
|
|
242
|
-
print("\n📄
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
print("✅ 登录完成")
|
|
256
|
-
|
|
257
|
-
target_url = None
|
|
258
|
-
matched_keyword = None
|
|
259
|
-
for kw in keywords:
|
|
260
|
-
print(f"\n🔍 搜索关键词: {kw}")
|
|
261
|
-
found = run_search(page, base_url, kw)
|
|
262
|
-
if found:
|
|
263
|
-
target_url = found
|
|
264
|
-
matched_keyword = kw
|
|
265
|
-
break
|
|
266
|
-
print(f" 未找到 {kw},尝试下一个关键词...")
|
|
419
|
+
print("\n📄 打开 Confluence 并登录...")
|
|
420
|
+
ensure_logged_in(page, base_url, username, password)
|
|
421
|
+
print(f"📄 当前页: {page.url[:120]}")
|
|
422
|
+
|
|
423
|
+
direct_url = env("CONFLUENCE_PAGE_URL")
|
|
424
|
+
target_url = direct_url if direct_url else None
|
|
425
|
+
matched_keyword = "CONFLUENCE_PAGE_URL" if direct_url else None
|
|
426
|
+
|
|
427
|
+
matched_title = ""
|
|
428
|
+
if not target_url:
|
|
429
|
+
target_url, matched_keyword, matched_title = resolve_prd_target(
|
|
430
|
+
page, base_url, prefix, version, keywords
|
|
431
|
+
)
|
|
267
432
|
|
|
268
433
|
if not target_url or not matched_keyword:
|
|
269
|
-
|
|
434
|
+
debug_path = docs_root / "confluence-search-debug.png"
|
|
435
|
+
try:
|
|
436
|
+
page.screenshot(path=str(debug_path), full_page=True)
|
|
437
|
+
print(f" 已保存调试截图: {debug_path}")
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
print(f"❌ 未找到 {prefix} {version} 的需求文档。")
|
|
441
|
+
print(
|
|
442
|
+
" 排查:1) 是否存在 PRD 父级页面 2) 依次尝试 ADI_V2.3.2 / ADI-V2.3.2 / ADI_2.3.2 / ADI-2.3.2"
|
|
443
|
+
)
|
|
444
|
+
print(" 3) 或设置 CONFLUENCE_PAGE_URL=<页面完整URL> 直接下载")
|
|
270
445
|
sys.exit(1)
|
|
271
446
|
|
|
272
|
-
print(f"✅
|
|
273
|
-
|
|
447
|
+
print(f"✅ 命中关键词: {matched_keyword}")
|
|
448
|
+
if matched_title:
|
|
449
|
+
print(f" 页面: {matched_title[:100]}")
|
|
450
|
+
print(f" URL: {target_url[:120]}...")
|
|
274
451
|
page.goto(target_url, wait_until="networkidle", timeout=60000)
|
|
275
452
|
|
|
276
453
|
try:
|
|
277
454
|
title = page.locator("#title-text").text_content().strip()
|
|
278
455
|
except Exception:
|
|
279
|
-
title = f"{matched_keyword} 需求文档"
|
|
456
|
+
title = matched_title or f"{matched_keyword} 需求文档"
|
|
280
457
|
|
|
281
458
|
content_elem = page.locator("#main-content")
|
|
282
459
|
if not content_elem.count():
|
|
@@ -331,6 +508,11 @@ def main():
|
|
|
331
508
|
print(f"\n❌ 获取失败: {e}")
|
|
332
509
|
raise
|
|
333
510
|
finally:
|
|
511
|
+
if env_bool("CONFLUENCE_DEBUG_PAUSE", default=not headless):
|
|
512
|
+
try:
|
|
513
|
+
input("\n⏸ 调试暂停:按 Enter 关闭浏览器...")
|
|
514
|
+
except (EOFError, KeyboardInterrupt):
|
|
515
|
+
pass
|
|
334
516
|
browser.close()
|
|
335
517
|
|
|
336
518
|
|
|
@@ -27,6 +27,12 @@ python3 confluence-doc.py <版本号>
|
|
|
27
27
|
|
|
28
28
|
说明:脚本内置默认账号密码(`lixinxin` / `Xin@147258`),可直接执行;如需覆盖,再设置 `CONFLUENCE_USERNAME` / `CONFLUENCE_PASSWORD`。
|
|
29
29
|
|
|
30
|
+
有头调试(可观察登录与搜索过程):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500 CONFLUENCE_DEBUG_PAUSE=1 python3 confluence-doc.py ADI_V2.3.2
|
|
34
|
+
```
|
|
35
|
+
|
|
30
36
|
版本号支持多种写法:
|
|
31
37
|
|
|
32
38
|
```bash
|
|
@@ -36,11 +42,14 @@ python3 confluence-doc.py {{PRODUCT_PREFIX}}-V1.2.3
|
|
|
36
42
|
python3 confluence-doc.py {{PRODUCT_PREFIX}}_V1.2.3
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
脚本搜索顺序(**固定**,即使用户说「{{PRODUCT_PREFIX}} 1.2.3」也按此顺序):
|
|
46
|
+
|
|
47
|
+
1. `{{PRODUCT_PREFIX}}_V<版本>`(如 ADI_V2.3.2)
|
|
48
|
+
2. `{{PRODUCT_PREFIX}}-V<版本>`(如 ADI-V2.3.2)
|
|
49
|
+
3. `{{PRODUCT_PREFIX}}_<版本>`(如 ADI_2.3.2)
|
|
50
|
+
4. `{{PRODUCT_PREFIX}}-<版本>`(如 ADI-2.3.2)
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
2. `{{PRODUCT_PREFIX}}_V<版本>`
|
|
43
|
-
3. `V<版本>`(兜底)
|
|
52
|
+
每个关键词都会走全站搜索 + 顶栏搜索;在候选结果中**优先选面包屑父级含 PRD 的页面**。
|
|
44
53
|
|
|
45
54
|
## 输出结构
|
|
46
55
|
|