@shulex/cxclawctl 0.1.2 → 0.1.4

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 (208) hide show
  1. package/README.md +25 -22
  2. package/capabilities.lock.json +535 -131
  3. package/dist/cli.js +2203 -127
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +20 -6
  6. package/requirements-cli.txt +0 -2
  7. package/skills/__lib__/__init__.py +0 -0
  8. package/skills/__lib__/skill-runtime/bootstrap.py +0 -134
  9. package/skills/__lib__/solvea-api/__init__.py +0 -0
  10. package/skills/__lib__/solvea-api/client.py +0 -1944
  11. package/skills/__lib__/solvea-api/dashboard.py +0 -100
  12. package/skills/__lib__/solvea-api/solvea_api.py +0 -1341
  13. package/skills/agent-analytics/SKILL.md +0 -74
  14. package/skills/agent-analytics/references/chat-report-fields.md +0 -36
  15. package/skills/agent-analytics/references/report-fields.md +0 -130
  16. package/skills/agent-analytics/references/transcript.md +0 -110
  17. package/skills/agent-builder/SKILL.md +0 -42
  18. package/skills/agent-builder/interfaces/fallback-chatflow.md +0 -47
  19. package/skills/agent-builder/references/chatflow/graph-spec.md +0 -259
  20. package/skills/agent-builder/references/chatflow/node-selection.md +0 -89
  21. package/skills/agent-builder/references/chatflow/nodes/agent.md +0 -350
  22. package/skills/agent-builder/references/chatflow/nodes/business-track.md +0 -43
  23. package/skills/agent-builder/references/chatflow/nodes/code.md +0 -157
  24. package/skills/agent-builder/references/chatflow/nodes/collect-order.md +0 -47
  25. package/skills/agent-builder/references/chatflow/nodes/finish.md +0 -34
  26. package/skills/agent-builder/references/chatflow/nodes/human-review.md +0 -60
  27. package/skills/agent-builder/references/chatflow/nodes/if-else.md +0 -100
  28. package/skills/agent-builder/references/chatflow/nodes/intent.md +0 -79
  29. package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +0 -43
  30. package/skills/agent-builder/references/chatflow/nodes/llm.md +0 -117
  31. package/skills/agent-builder/references/chatflow/nodes/note.md +0 -38
  32. package/skills/agent-builder/references/chatflow/nodes/question.md +0 -97
  33. package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +0 -68
  34. package/skills/agent-builder/references/chatflow/nodes/reply.md +0 -97
  35. package/skills/agent-builder/references/chatflow/nodes/start.md +0 -35
  36. package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +0 -48
  37. package/skills/agent-builder/references/chatflow/nodes/tool.md +0 -138
  38. package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +0 -63
  39. package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +0 -52
  40. package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +0 -50
  41. package/skills/agent-builder/references/chatflow/nodes/variable-process.md +0 -70
  42. package/skills/agent-builder/references/chatflow/overview.md +0 -53
  43. package/skills/agent-builder/references/chatflow/visual-translation.md +0 -64
  44. package/skills/agent-builder/references/chatflow/workflow.md +0 -134
  45. package/skills/agent-builder/references/multi-agent/create-update.md +0 -185
  46. package/skills/agent-builder/references/multi-agent/graph-spec.md +0 -246
  47. package/skills/agent-builder/references/multi-agent/graph-structure.md +0 -12
  48. package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +0 -13
  49. package/skills/agent-builder/references/multi-agent/migration/large.md +0 -554
  50. package/skills/agent-builder/references/multi-agent/migration/principles.md +0 -446
  51. package/skills/agent-builder/references/multi-agent/migration/small.md +0 -225
  52. package/skills/agent-builder/references/multi-agent/orchestration.md +0 -11
  53. package/skills/agent-builder/references/multi-agent/overview.md +0 -48
  54. package/skills/agent-builder/references/multi-agent/update.md +0 -128
  55. package/skills/agent-builder/references/multi-agent/workflow.md +0 -87
  56. package/skills/agent-builder/references/single-agent/overview.md +0 -48
  57. package/skills/agent-builder/references/single-agent/prompt.md +0 -16
  58. package/skills/agent-builder/references/single-agent/tools.md +0 -36
  59. package/skills/agent-builder/references/single-agent/voice.md +0 -46
  60. package/skills/agent-builder/references/tools/http-tool.md +0 -315
  61. package/skills/agent-builder/references/tools/mock-tool.md +0 -279
  62. package/skills/agent-builder/references/tools/overview.md +0 -46
  63. package/skills/agent-builder/references/tools/tool-config.md +0 -43
  64. package/skills/agent-builder/references/tools/x-token.md +0 -34
  65. package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +0 -3458
  66. package/skills/agent-builder/scripts/chatflow/validate_graph.py +0 -1697
  67. package/skills/agent-builder/scripts/copy_marketplace_agent.py +0 -61
  68. package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +0 -5974
  69. package/skills/agent-builder/scripts/multi-agent/replay_compare.py +0 -679
  70. package/skills/agent-builder/scripts/patch_agent_tools.py +0 -135
  71. package/skills/agent-builder/scripts/tools/validate_tool_config.py +0 -384
  72. package/skills/agent-tester/SKILL.md +0 -35
  73. package/skills/agent-tester/batch-test/overview.md +0 -86
  74. package/skills/agent-tester/scripts/batch_test_sheet.py +0 -213
  75. package/skills/agent-tester/single-test/badcase-retest.md +0 -85
  76. package/skills/agent-tester/single-test/direct-test.md +0 -24
  77. package/skills/agent-tester/single-test/overview.md +0 -142
  78. package/skills/delivery/SKILL.md +0 -112
  79. package/skills/delivery/interfaces/classify.md +0 -38
  80. package/skills/delivery/interfaces/create-agents.md +0 -89
  81. package/skills/delivery/interfaces/delivery-test.md +0 -67
  82. package/skills/delivery/references/flow.md +0 -40
  83. package/skills/delivery/references/validation.md +0 -15
  84. package/skills/delivery/scripts/batch_test.py +0 -704
  85. package/skills/delivery/scripts/classify.py +0 -22
  86. package/skills/delivery/scripts/create_agents.py +0 -695
  87. package/skills/delivery/scripts/gen_delivery_report.py +0 -335
  88. package/skills/delivery/scripts/gen_excel.py +0 -234
  89. package/skills/delivery/scripts/gen_report.py +0 -208
  90. package/skills/delivery/scripts/plan.py +0 -163
  91. package/skills/delivery/scripts/render.py +0 -713
  92. package/skills/delivery/scripts/render_summary.py +0 -38
  93. package/skills/delivery/scripts/sample_history_cases.py +0 -109
  94. package/skills/kb-builder/SKILL.md +0 -80
  95. package/skills/kb-builder/interfaces/qa-upload.md +0 -55
  96. package/skills/kb-builder/interfaces/web-kb.md +0 -84
  97. package/skills/kb-builder/references/directory.md +0 -373
  98. package/skills/kb-builder/references/export.md +0 -282
  99. package/skills/kb-builder/references/isolation-keywords.md +0 -113
  100. package/skills/kb-builder/references/knowledge.md +0 -153
  101. package/skills/kb-builder/references/product.md +0 -52
  102. package/skills/kb-builder/references/qa-from-help-center.md +0 -58
  103. package/skills/kb-builder/references/qa-from-pdf.md +0 -94
  104. package/skills/kb-builder/references/qa-from-tickets.md +0 -168
  105. package/skills/kb-builder/references/qa-from-web.md +0 -98
  106. package/skills/kb-builder/references/qa-quality.md +0 -73
  107. package/skills/kb-builder/references/reply-template.md +0 -237
  108. package/skills/kb-builder/references/tag-isolation-migration.md +0 -61
  109. package/skills/kb-builder/references/tags.md +0 -45
  110. package/skills/kb-builder/references/upload.md +0 -130
  111. package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +0 -333
  112. package/skills/kb-builder/scripts/build_directory_plan.py +0 -144
  113. package/skills/kb-builder/scripts/crawl_site.py +0 -433
  114. package/skills/kb-builder/scripts/create_dirs.py +0 -146
  115. package/skills/kb-builder/scripts/export_knowledge.py +0 -454
  116. package/skills/kb-builder/scripts/extract_ticket_qa.py +0 -441
  117. package/skills/kb-builder/scripts/extract_web_qa.py +0 -118
  118. package/skills/kb-builder/scripts/import_qa.py +0 -219
  119. package/skills/kb-builder/scripts/import_source_documents.py +0 -143
  120. package/skills/kb-builder/scripts/import_templates.py +0 -409
  121. package/skills/poc/SKILL.md +0 -125
  122. package/skills/poc/assets/default_manual.css +0 -279
  123. package/skills/poc/assets/notice_escalate.png +0 -0
  124. package/skills/poc/assets/notice_escalate2.png +0 -0
  125. package/skills/poc/assets/notice_new_chat.png +0 -0
  126. package/skills/poc/assets/poc_test_template.html +0 -645
  127. package/skills/poc/assets/poc_test_template.md +0 -104
  128. package/skills/poc/assets/step3_preview.png +0 -0
  129. package/skills/poc/assets/step4_order_note.png +0 -0
  130. package/skills/poc/interfaces/classify.md +0 -38
  131. package/skills/poc/interfaces/create-agents.md +0 -108
  132. package/skills/poc/interfaces/poc-test.md +0 -94
  133. package/skills/poc/references/flow.md +0 -47
  134. package/skills/poc/references/kb-directory.md +0 -22
  135. package/skills/poc/references/validation.md +0 -17
  136. package/skills/poc/scripts/classify.py +0 -22
  137. package/skills/poc/scripts/create_agents.py +0 -818
  138. package/skills/poc/scripts/gen_excel.py +0 -234
  139. package/skills/poc/scripts/gen_poc_manual.py +0 -249
  140. package/skills/poc/scripts/gen_report.py +0 -208
  141. package/skills/poc/scripts/inspect_chatflow.py +0 -209
  142. package/skills/poc/scripts/plan.py +0 -178
  143. package/skills/poc/scripts/poc_test.py +0 -495
  144. package/skills/poc/scripts/render.py +0 -713
  145. package/skills/poc/scripts/render_summary.py +0 -41
  146. package/skills/runtime-inspector/SKILL.md +0 -51
  147. package/skills/runtime-inspector/references/frontend-rendering.md +0 -126
  148. package/skills/runtime-inspector/references/model.md +0 -59
  149. package/skills/runtime-inspector/references/sandbox.md +0 -131
  150. package/skills/runtime-inspector/scripts/model_inspect.py +0 -68
  151. package/skills/runtime-inspector/scripts/prepare.py +0 -98
  152. package/skills/runtime-inspector/scripts/process.py +0 -84
  153. package/skills/runtime-inspector/scripts/read_state.py +0 -32
  154. package/skills/runtime-inspector/scripts/sandbox_inspect.py +0 -463
  155. package/skills/runtime-inspector/scripts/test_claude_proxy.py +0 -49
  156. package/skills/runtime-inspector/scripts/test_openai_proxy.py +0 -41
  157. package/skills/runtime-inspector/scripts/verify_persistence.py +0 -101
  158. package/skills/runtime-inspector/scripts/write_test.py +0 -41
  159. package/skills/setup/SKILL.md +0 -17
  160. package/skills/shulex-support/SKILL.md +0 -39
  161. package/skills/shulex-support/references/badcase-analysis/overview.md +0 -34
  162. package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +0 -105
  163. package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +0 -89
  164. package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +0 -36
  165. package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +0 -53
  166. package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +0 -259
  167. package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +0 -148
  168. package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +0 -57
  169. package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +0 -120
  170. package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +0 -126
  171. package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +0 -121
  172. package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +0 -208
  173. package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +0 -115
  174. package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +0 -72
  175. package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +0 -92
  176. package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +0 -147
  177. package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +0 -283
  178. package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +0 -77
  179. package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +0 -19
  180. package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +0 -23
  181. package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +0 -31
  182. package/skills/shulex-support/references/product-docs.md +0 -70
  183. package/skills/shulex-support/references/tickets.md +0 -184
  184. package/skills/shulex-support/scripts/support.py +0 -427
  185. package/skills/solvea-desk/SKILL.md +0 -39
  186. package/skills/solvea-desk/references/report/fields.md +0 -82
  187. package/skills/solvea-desk/references/report/report-design.md +0 -200
  188. package/skills/solvea-desk/references/report/report-template.html +0 -44
  189. package/skills/solvea-desk/references/report/report.css +0 -517
  190. package/skills/solvea-desk/references/report/workflow.md +0 -464
  191. package/skills/solvea-desk/references/tickets/query.md +0 -103
  192. package/skills/solvea-desk/references/tickets/tags.md +0 -83
  193. package/skills/solvea-desk/scripts/report/fetch_sources.py +0 -580
  194. package/skills/solvea-desk/scripts/report/render_report.py +0 -763
  195. package/skills/solvea-desk/scripts/tickets/desk_api.py +0 -73
  196. package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +0 -403
  197. package/skills/solvea-desk/scripts/tickets/ticket_tags.py +0 -305
  198. package/skills/solvea-kit/SKILL.md +0 -84
  199. package/skills/solvea-kit/references/agent.md +0 -145
  200. package/skills/solvea-kit/references/chat.md +0 -244
  201. package/skills/solvea-kit/references/intent.md +0 -69
  202. package/skills/solvea-kit/references/plugin.md +0 -53
  203. package/skills/solvea-kit/references/quality-inspection.md +0 -75
  204. package/skills/solvea-kit/references/strategy.md +0 -62
  205. package/skills/solvea-kit/references/tagging.md +0 -231
  206. package/skills/solvea-kit/references/variables.md +0 -53
  207. package/skills/solvea-kit/scripts/generate_label_import_file.py +0 -151
  208. package/skills/solvea-kit/scripts/intent_classify.py +0 -843
@@ -1,433 +0,0 @@
1
- #!/usr/bin/env python3
2
- """KB website crawl pipeline entry.
3
-
4
- Phase 4 implementation:
5
- - 固定 `--mode auto|crawl|legacy` 与 `--provider auto|crawl4ai|tavily` CLI。
6
- - `legacy` 明确输出兼容旧 reference 路径的结果,不执行抓取。
7
- - `crawl/provider=auto` 使用内置轻量 HTTP BFS crawler,输出稳定 crawl_result。
8
- - `crawl4ai` provider 使用 sandbox 内 Python 依赖抓取 JS/动态站点,失败时给出显式错误。
9
-
10
- 本脚本只负责 KB builder 内部网页抓取能力,不暴露给 POC / Delivery 业务细节。
11
- """
12
- from __future__ import annotations
13
-
14
- import argparse
15
- import asyncio
16
- import html
17
- import json
18
- import os
19
- import re
20
- import sys
21
- import time
22
- from collections import deque
23
- from datetime import datetime, timezone
24
- from pathlib import Path
25
- from typing import Any
26
- from urllib.parse import urldefrag, urljoin, urlparse
27
- from urllib.request import Request, urlopen
28
-
29
- sys.path.insert(0, os.path.join(os.environ.get("SOLVEA_LIB_DIR", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "__lib__"))), "skill-runtime"))
30
- from bootstrap import * # noqa: F401,F403
31
-
32
-
33
- USER_AGENT = "SolveaKit-KB-Crawler/1.0"
34
-
35
-
36
- def resolve_workspace_path(path: str | Path) -> Path:
37
- p = Path(path)
38
- if p.is_absolute() and str(p).startswith("/workspace"):
39
- return WORKSPACE / p.relative_to("/workspace")
40
- return p
41
-
42
-
43
- def virtual_workspace_path(path: Path) -> str:
44
- try:
45
- rel = path.resolve().relative_to(WORKSPACE.resolve())
46
- return str(Path("/workspace") / rel)
47
- except Exception:
48
- return str(path)
49
-
50
-
51
- def slugify(value: str, fallback: str) -> str:
52
- text = re.sub(r"[^a-zA-Z0-9._-]+", "-", value.strip().lower()).strip("-")
53
- return text[:80] or fallback
54
-
55
-
56
- def normalize_url(url: str) -> str:
57
- clean, _frag = urldefrag(url.strip())
58
- return clean.rstrip("/")
59
-
60
-
61
- def same_domain(url: str, base_netloc: str) -> bool:
62
- netloc = urlparse(url).netloc.lower()
63
- return netloc == base_netloc.lower()
64
-
65
-
66
- def fetch_html(url: str, timeout: int) -> tuple[int, str, str]:
67
- req = Request(url, headers={"User-Agent": USER_AGENT})
68
- with urlopen(req, timeout=timeout) as resp:
69
- status = getattr(resp, "status", 200)
70
- final_url = resp.geturl()
71
- raw = resp.read()
72
- charset = resp.headers.get_content_charset() or "utf-8"
73
- try:
74
- text = raw.decode(charset, errors="replace")
75
- except LookupError:
76
- text = raw.decode("utf-8", errors="replace")
77
- return status, final_url, text
78
-
79
-
80
- def extract_title(html_text: str, url: str) -> str:
81
- m = re.search(r"<title[^>]*>(.*?)</title>", html_text, flags=re.I | re.S)
82
- if not m:
83
- return urlparse(url).path.strip("/") or url
84
- title = re.sub(r"\s+", " ", html.unescape(m.group(1))).strip()
85
- return title or url
86
-
87
-
88
- def strip_noise(html_text: str) -> str:
89
- text = re.sub(r"(?is)<(script|style|noscript|svg|canvas).*?</\1>", " ", html_text)
90
- text = re.sub(r"(?is)<(nav|footer|header)[^>]*>.*?</\1>", " ", text)
91
- return text
92
-
93
-
94
- def html_to_markdown(html_text: str) -> str:
95
- cleaned = strip_noise(html_text)
96
- try:
97
- from markdownify import markdownify as md # type: ignore
98
-
99
- markdown = md(cleaned, heading_style="ATX")
100
- except Exception:
101
- markdown = re.sub(r"(?i)<br\s*/?>", "\n", cleaned)
102
- markdown = re.sub(r"(?i)</(p|div|section|article|li|h[1-6])>", "\n", markdown)
103
- markdown = re.sub(r"<[^>]+>", " ", markdown)
104
- markdown = html.unescape(markdown)
105
- lines = [re.sub(r"[ \t]+", " ", line).strip() for line in markdown.splitlines()]
106
- compact: list[str] = []
107
- blank = False
108
- for line in lines:
109
- if not line:
110
- if not blank:
111
- compact.append("")
112
- blank = True
113
- continue
114
- compact.append(line)
115
- blank = False
116
- return "\n".join(compact).strip()
117
-
118
-
119
- def extract_links(html_text: str, page_url: str, base_netloc: str) -> list[str]:
120
- links: list[str] = []
121
- for href in re.findall(r'''(?is)<a[^>]+href=["']([^"']+)["']''', html_text):
122
- if href.startswith(("mailto:", "tel:", "javascript:")):
123
- continue
124
- absolute = normalize_url(urljoin(page_url, html.unescape(href)))
125
- parsed = urlparse(absolute)
126
- if parsed.scheme not in ("http", "https"):
127
- continue
128
- if not same_domain(absolute, base_netloc):
129
- continue
130
- if re.search(r"\.(jpg|jpeg|png|gif|webp|svg|pdf|zip|rar|7z|mp4|mov|avi)$", parsed.path, re.I):
131
- continue
132
- links.append(absolute)
133
- return sorted(set(links))
134
-
135
-
136
- def classify_page(url: str, title: str, markdown: str) -> str:
137
- path = urlparse(url).path.lower()
138
- if path.startswith("/products/") or path.startswith("/collections/"):
139
- return "product"
140
- if path.startswith("/policies/") or re.search(r"/(warranty|return|refund|shipping|privacy|terms|policy)", path):
141
- return "policy"
142
- if re.search(r"/(help|support|docs|documentation|faq|knowledge-base|troubleshoot)", path):
143
- return "help_center"
144
-
145
- hay = f"{url}\n{title}\n{markdown[:3000]}".lower()
146
- if re.search(r"\b(help|support|docs|documentation|faq|knowledge-base|troubleshoot)\b", hay):
147
- return "help_center"
148
- if re.search(r"\b(product|products|collection|collections|sku|model|add to cart|buy now|shop)\b", hay):
149
- return "product"
150
- if re.search(r"\b(warranty|return|refund|shipping|privacy|terms|policy|guarantee)\b", hay):
151
- return "policy"
152
- return "other"
153
-
154
-
155
- def write_page_markdown(pages_dir: Path, index: int, url: str, title: str, markdown: str) -> Path:
156
- pages_dir.mkdir(parents=True, exist_ok=True)
157
- slug = slugify(title or urlparse(url).path, f"page-{index:03d}")
158
- out = pages_dir / f"{index:03d}_{slug}.md"
159
- out.write_text(f"# {title}\n\nURL: {url}\n\n{markdown}\n", encoding="utf-8")
160
- return out
161
-
162
-
163
- def crawl_builtin(base_url: str, *, max_depth: int, max_pages: int, timeout: int, output_path: Path) -> dict[str, Any]:
164
- parsed = urlparse(base_url)
165
- if parsed.scheme not in ("http", "https") or not parsed.netloc:
166
- raise ValueError(f"invalid URL: {base_url}")
167
-
168
- base_url = normalize_url(base_url)
169
- queue: deque[tuple[str, int]] = deque([(base_url, 0)])
170
- seen: set[str] = set()
171
- pages: list[dict[str, Any]] = []
172
- errors: list[dict[str, Any]] = []
173
- pages_dir = output_path.parent / f"{output_path.stem}_pages"
174
-
175
- while queue and len(pages) < max_pages:
176
- url, depth = queue.popleft()
177
- url = normalize_url(url)
178
- if url in seen:
179
- continue
180
- seen.add(url)
181
- try:
182
- status, final_url, html_text = fetch_html(url, timeout=timeout)
183
- title = extract_title(html_text, final_url)
184
- markdown = html_to_markdown(html_text)
185
- links = extract_links(html_text, final_url, parsed.netloc)
186
- page_type = classify_page(final_url, title, markdown)
187
- md_path = write_page_markdown(pages_dir, len(pages) + 1, final_url, title, markdown)
188
- pages.append({
189
- "url": final_url,
190
- "title": title,
191
- "type": page_type,
192
- "depth": depth,
193
- "status": status,
194
- "markdown_path": virtual_workspace_path(md_path),
195
- "fit_markdown_path": virtual_workspace_path(md_path),
196
- "links": links[:100],
197
- "images": [],
198
- })
199
- if depth < max_depth:
200
- for link in links:
201
- if link not in seen:
202
- queue.append((link, depth + 1))
203
- except Exception as exc:
204
- errors.append({"url": url, "depth": depth, "error": str(exc)})
205
-
206
- return {
207
- "base_url": base_url,
208
- "provider": "builtin",
209
- "mode": "crawl",
210
- "crawled_at": datetime.now(timezone.utc).isoformat(),
211
- "max_depth": max_depth,
212
- "max_pages": max_pages,
213
- "pages": pages,
214
- "errors": errors,
215
- }
216
-
217
-
218
- def result_value(obj: Any, name: str, default: Any = None) -> Any:
219
- if isinstance(obj, dict):
220
- return obj.get(name, default)
221
- return getattr(obj, name, default)
222
-
223
-
224
- def markdown_value(result: Any) -> tuple[str, str]:
225
- markdown_obj = result_value(result, "markdown")
226
- fit_markdown = result_value(markdown_obj, "fit_markdown") or result_value(result, "fit_markdown") or ""
227
- markdown = (
228
- result_value(markdown_obj, "raw_markdown")
229
- or result_value(markdown_obj, "markdown_with_citations")
230
- or result_value(result, "markdown")
231
- or ""
232
- )
233
- if not isinstance(markdown, str):
234
- markdown = str(markdown or "")
235
- if not isinstance(fit_markdown, str):
236
- fit_markdown = str(fit_markdown or "")
237
- return markdown.strip(), fit_markdown.strip()
238
-
239
-
240
- def links_from_crawl4ai(result: Any, page_url: str, base_netloc: str) -> list[str]:
241
- raw_links = result_value(result, "links", {}) or {}
242
- candidates: list[Any] = []
243
- if isinstance(raw_links, dict):
244
- for key in ("internal", "external"):
245
- value = raw_links.get(key) or []
246
- if isinstance(value, list):
247
- candidates.extend(value)
248
- elif isinstance(raw_links, list):
249
- candidates.extend(raw_links)
250
-
251
- links: list[str] = []
252
- for item in candidates:
253
- href = item.get("href") if isinstance(item, dict) else str(item or "")
254
- if not href or href.startswith(("mailto:", "tel:", "javascript:")):
255
- continue
256
- absolute = normalize_url(urljoin(page_url, html.unescape(href)))
257
- parsed = urlparse(absolute)
258
- if parsed.scheme not in ("http", "https"):
259
- continue
260
- if not same_domain(absolute, base_netloc):
261
- continue
262
- if re.search(r"\.(jpg|jpeg|png|gif|webp|svg|pdf|zip|rar|7z|mp4|mov|avi)$", parsed.path, re.I):
263
- continue
264
- links.append(absolute)
265
- return sorted(set(links))
266
-
267
-
268
- async def crawl4ai_page(crawler: Any, url: str, timeout: int) -> Any:
269
- try:
270
- from crawl4ai import CacheMode, CrawlerRunConfig # type: ignore
271
-
272
- config = CrawlerRunConfig(
273
- cache_mode=CacheMode.BYPASS,
274
- page_timeout=timeout * 1000,
275
- remove_overlay_elements=True,
276
- word_count_threshold=10,
277
- )
278
- return await crawler.arun(url=url, config=config)
279
- except TypeError:
280
- return await crawler.arun(url=url)
281
-
282
-
283
- async def crawl4ai_async(base_url: str, *, max_depth: int, max_pages: int, timeout: int, output_path: Path) -> dict[str, Any]:
284
- try:
285
- from crawl4ai import AsyncWebCrawler, BrowserConfig # type: ignore
286
- except Exception as exc:
287
- raise RuntimeError(
288
- "crawl4ai is not installed in the current runtime; run this script inside the sandbox dependency group"
289
- ) from exc
290
-
291
- parsed = urlparse(base_url)
292
- if parsed.scheme not in ("http", "https") or not parsed.netloc:
293
- raise ValueError(f"invalid URL: {base_url}")
294
-
295
- base_url = normalize_url(base_url)
296
- queue: deque[tuple[str, int]] = deque([(base_url, 0)])
297
- seen: set[str] = set()
298
- pages: list[dict[str, Any]] = []
299
- errors: list[dict[str, Any]] = []
300
- pages_dir = output_path.parent / f"{output_path.stem}_pages"
301
-
302
- browser_config = BrowserConfig(headless=True, verbose=False)
303
- async with AsyncWebCrawler(config=browser_config) as crawler:
304
- while queue and len(pages) < max_pages:
305
- url, depth = queue.popleft()
306
- url = normalize_url(url)
307
- if url in seen:
308
- continue
309
- seen.add(url)
310
- try:
311
- result = await crawl4ai_page(crawler, url, timeout)
312
- success = result_value(result, "success", True)
313
- if success is False:
314
- error = result_value(result, "error_message", "crawl4ai returned success=false")
315
- errors.append({"url": url, "depth": depth, "error": str(error)})
316
- continue
317
-
318
- final_url = normalize_url(result_value(result, "url", url) or url)
319
- title = result_value(result, "metadata", {}) or {}
320
- title_text = title.get("title") if isinstance(title, dict) else ""
321
- markdown, fit_markdown = markdown_value(result)
322
- body = fit_markdown or markdown
323
- links = links_from_crawl4ai(result, final_url, parsed.netloc)
324
- page_type = classify_page(final_url, title_text or final_url, body)
325
- md_path = write_page_markdown(pages_dir, len(pages) + 1, final_url, title_text or final_url, body)
326
- pages.append({
327
- "url": final_url,
328
- "title": title_text or final_url,
329
- "type": page_type,
330
- "depth": depth,
331
- "status": result_value(result, "status_code", 200),
332
- "markdown_path": virtual_workspace_path(md_path),
333
- "fit_markdown_path": virtual_workspace_path(md_path),
334
- "links": links[:100],
335
- "images": [],
336
- })
337
- if depth < max_depth:
338
- for link in links:
339
- if link not in seen:
340
- queue.append((link, depth + 1))
341
- except Exception as exc:
342
- errors.append({"url": url, "depth": depth, "error": str(exc)})
343
-
344
- return {
345
- "base_url": base_url,
346
- "provider": "crawl4ai",
347
- "mode": "crawl",
348
- "crawled_at": datetime.now(timezone.utc).isoformat(),
349
- "max_depth": max_depth,
350
- "max_pages": max_pages,
351
- "pages": pages,
352
- "errors": errors,
353
- }
354
-
355
-
356
- def crawl4ai_provider(base_url: str, *, max_depth: int, max_pages: int, timeout: int, output_path: Path) -> dict[str, Any]:
357
- return asyncio.run(crawl4ai_async(
358
- base_url,
359
- max_depth=max_depth,
360
- max_pages=max_pages,
361
- timeout=timeout,
362
- output_path=output_path,
363
- ))
364
-
365
-
366
- def legacy_result(url: str, reason: str) -> dict[str, Any]:
367
- return {
368
- "base_url": url,
369
- "provider": "legacy",
370
- "mode": "legacy",
371
- "crawled_at": datetime.now(timezone.utc).isoformat(),
372
- "pages": [],
373
- "errors": [{"url": url, "error": reason}],
374
- "next_step": "Use kb-builder legacy references and agent web_crawl/web_fetch tools.",
375
- }
376
-
377
-
378
- def error_result(url: str, provider: str, mode: str, error: Exception) -> dict[str, Any]:
379
- return {
380
- "base_url": url,
381
- "provider": provider,
382
- "mode": mode,
383
- "crawled_at": datetime.now(timezone.utc).isoformat(),
384
- "pages": [],
385
- "errors": [{"url": url, "error": str(error), "error_type": error.__class__.__name__}],
386
- }
387
-
388
-
389
- def main() -> None:
390
- parser = argparse.ArgumentParser(description="Crawl website pages for kb-builder")
391
- parser.add_argument("--url", required=True, help="Website / product / help center URL")
392
- parser.add_argument("--mode", choices=["auto", "crawl", "legacy"], default="auto")
393
- parser.add_argument("--provider", choices=["auto", "crawl4ai", "tavily"], default="auto")
394
- parser.add_argument("--max-depth", type=int, default=2)
395
- parser.add_argument("--max-pages", type=int, default=50)
396
- parser.add_argument("--timeout", type=int, default=20)
397
- parser.add_argument("--output", default="", help="Output crawl_result JSON path")
398
- args = parser.parse_args()
399
-
400
- OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
401
- output = resolve_workspace_path(args.output) if args.output else OUTPUTS_DIR / f"crawl_result_{timestamp()}.json"
402
- output.parent.mkdir(parents=True, exist_ok=True)
403
-
404
- started = time.time()
405
- try:
406
- if args.mode == "legacy" or args.provider == "tavily":
407
- result = legacy_result(args.url, "legacy/Tavily path is intentionally preserved; script provider not used.")
408
- elif args.provider == "crawl4ai":
409
- result = crawl4ai_provider(
410
- args.url,
411
- max_depth=max(0, args.max_depth),
412
- max_pages=max(1, args.max_pages),
413
- timeout=max(1, args.timeout),
414
- output_path=output,
415
- )
416
- else:
417
- result = crawl_builtin(
418
- args.url,
419
- max_depth=max(0, args.max_depth),
420
- max_pages=max(1, args.max_pages),
421
- timeout=max(1, args.timeout),
422
- output_path=output,
423
- )
424
- except Exception as exc:
425
- result = error_result(args.url, args.provider, "legacy" if args.mode == "legacy" else "crawl", exc)
426
-
427
- result["elapsed_seconds"] = round(time.time() - started, 3)
428
- output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
429
- print(f"[crawl_site] output={virtual_workspace_path(output)} pages={len(result.get('pages') or [])} errors={len(result.get('errors') or [])}")
430
-
431
-
432
- if __name__ == "__main__":
433
- main()
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- create_dirs.py — Recursively create Solvea KB folders from confirmed_tree.json.
4
-
5
- Reads `STATE_DIR/confirmed_tree.json`, deep-first creates folders via SolveaClient.
6
- Idempotent: if a folder with same (name, parentId) already exists, reuse its id
7
- instead of creating a duplicate.
8
-
9
- Tree schema (each node):
10
- {"name": "...", "children": [ ... ]} # children optional
11
-
12
- Usage:
13
- python3 /skills/kb-builder/scripts/create_dirs.py
14
- python3 /skills/kb-builder/scripts/create_dirs.py --tree-file <path>
15
- python3 /skills/kb-builder/scripts/create_dirs.py --dry-run
16
- """
17
- from __future__ import annotations
18
- import argparse, json, os, sys
19
- sys.path.insert(0, os.path.join(os.environ.get("SOLVEA_LIB_DIR", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "__lib__"))), "skill-runtime"))
20
- from bootstrap import *
21
- from pathlib import Path
22
-
23
-
24
- def flatten_folders(nodes, result=None):
25
- if result is None:
26
- result = []
27
- for node in nodes:
28
- result.append(node)
29
- children = node.get("children", [])
30
- if children:
31
- flatten_folders(children, result)
32
- return result
33
-
34
-
35
- def fetch_existing_index(client: SolveaClient) -> dict:
36
- """Return {(parent_id, name): folder_id} for all existing folders."""
37
- folders = client.list_directories()
38
- if isinstance(folders, list):
39
- flat = flatten_folders(folders)
40
- else:
41
- raw = folders.get("list") or folders.get("records") or folders.get("data") or []
42
- flat = flatten_folders(raw)
43
- return {
44
- ((f.get("parentId") or 0), f.get("name", "")): f["id"]
45
- for f in flat if f.get("id") is not None
46
- }
47
-
48
-
49
- def walk(nodes, parent_id, depth, *, client, existing, dry_run, stats, max_depth=3):
50
- """Depth-first create folders. Mutates existing index as new folders are created.
51
-
52
- In dry_run mode, only existing folders have a real id for child placement;
53
- not-yet-created folders use parent_id=None for their children (children counted
54
- but not nested under a real parent).
55
- """
56
- for node in nodes:
57
- if depth >= max_depth:
58
- raise ValueError(
59
- f"directory tree exceeds the default {max_depth}-level limit; "
60
- f"cannot create '{node.get('name', '')}' at level {depth + 1}. "
61
- "Pass an explicitly approved --max-depth to override."
62
- )
63
- name = node.get("name", "").strip()
64
- if not name:
65
- print(f" {' ' * depth}[skip] empty name at depth {depth}", file=sys.stderr)
66
- continue
67
-
68
- key = (parent_id, name)
69
- fid = None
70
- if key in existing:
71
- fid = existing[key]
72
- stats["existing"] += 1
73
- print(f" {' ' * depth}[exist] {name} (id={fid})")
74
- elif dry_run:
75
- stats["to_create"] += 1
76
- print(f" {' ' * depth}[dry] {name}")
77
- else:
78
- result = client.create_folder(name, parent_id=parent_id)
79
- fid = _extract_folder_id(result)
80
- if fid is None:
81
- stats["failed"] += 1
82
- print(f" {' ' * depth}[FAIL] {name} parent={parent_id} resp={result}", file=sys.stderr)
83
- continue
84
- existing[key] = fid
85
- stats["created"] += 1
86
- print(f" {' ' * depth}[ok] {name} (id={fid})")
87
-
88
- children = node.get("children", [])
89
- if children:
90
- walk(children, fid, depth + 1, client=client, existing=existing,
91
- dry_run=dry_run, stats=stats, max_depth=max_depth)
92
-
93
-
94
- def _extract_folder_id(result):
95
- """Pull folder id from create_folder response. Returns None on failure."""
96
- if not isinstance(result, dict):
97
- return None
98
- if "id" in result:
99
- return result["id"]
100
- data = result.get("data")
101
- if isinstance(data, dict) and "id" in data:
102
- return data["id"]
103
- return None
104
-
105
-
106
- def run(tree_file: str, dry_run: bool, max_depth: int = 3) -> None:
107
- from client import SolveaClient
108
-
109
- tree_path = STATE_DIR / "confirmed_tree.json" if tree_file == "" else Path(tree_file)
110
- if not tree_path.exists():
111
- print(f"[error] tree file not found: {tree_path}", file=sys.stderr)
112
- sys.exit(1)
113
-
114
- tree = json.loads(tree_path.read_text(encoding="utf-8"))
115
- if not isinstance(tree, list):
116
- print(f"[error] tree must be a list of root nodes, got {type(tree).__name__}", file=sys.stderr)
117
- sys.exit(1)
118
-
119
- print(f"[create_dirs] tree={tree_path} root_nodes={len(tree)} dry_run={dry_run}")
120
- client = SolveaClient()
121
- existing = fetch_existing_index(client)
122
- print(f"[create_dirs] existing folders: {len(existing)}")
123
-
124
- stats = {"created": 0, "existing": 0, "to_create": 0, "failed": 0}
125
- walk(tree, parent_id=0, depth=0, client=client, existing=existing,
126
- dry_run=dry_run, stats=stats, max_depth=max_depth)
127
-
128
- print(f"\n[create_dirs] Done: created={stats['created']} existing={stats['existing']} "
129
- f"to_create={stats['to_create']} failed={stats['failed']}")
130
-
131
-
132
- def main():
133
- ap = argparse.ArgumentParser()
134
- ap.add_argument("--tree-file", default="", dest="tree_file",
135
- help="Path to confirmed_tree.json (default: STATE_DIR/confirmed_tree.json)")
136
- ap.add_argument("--dry-run", action="store_true", dest="dry_run")
137
- ap.add_argument("--max-depth", type=int, default=3,
138
- help="Maximum folder depth (default: 3; override only when explicitly requested)")
139
- args = ap.parse_args()
140
- if args.max_depth < 1:
141
- ap.error("--max-depth must be positive")
142
- run(args.tree_file, args.dry_run, args.max_depth)
143
-
144
-
145
- if __name__ == "__main__":
146
- main()