@shulex/cxclawctl 0.0.0-stage → 0.1.2

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 (209) hide show
  1. package/README.md +228 -2
  2. package/bin/cxclawctl.mjs +11 -0
  3. package/capabilities.lock.json +898 -0
  4. package/dist/cli.js +363 -0
  5. package/dist/cli.js.map +1 -0
  6. package/package.json +33 -4
  7. package/requirements-cli.txt +2 -0
  8. package/skills/__lib__/__init__.py +0 -0
  9. package/skills/__lib__/skill-runtime/bootstrap.py +134 -0
  10. package/skills/__lib__/solvea-api/__init__.py +0 -0
  11. package/skills/__lib__/solvea-api/client.py +1944 -0
  12. package/skills/__lib__/solvea-api/dashboard.py +100 -0
  13. package/skills/__lib__/solvea-api/solvea_api.py +1341 -0
  14. package/skills/agent-analytics/SKILL.md +74 -0
  15. package/skills/agent-analytics/references/chat-report-fields.md +36 -0
  16. package/skills/agent-analytics/references/report-fields.md +130 -0
  17. package/skills/agent-analytics/references/transcript.md +110 -0
  18. package/skills/agent-builder/SKILL.md +42 -0
  19. package/skills/agent-builder/interfaces/fallback-chatflow.md +47 -0
  20. package/skills/agent-builder/references/chatflow/graph-spec.md +259 -0
  21. package/skills/agent-builder/references/chatflow/node-selection.md +89 -0
  22. package/skills/agent-builder/references/chatflow/nodes/agent.md +350 -0
  23. package/skills/agent-builder/references/chatflow/nodes/business-track.md +43 -0
  24. package/skills/agent-builder/references/chatflow/nodes/code.md +157 -0
  25. package/skills/agent-builder/references/chatflow/nodes/collect-order.md +47 -0
  26. package/skills/agent-builder/references/chatflow/nodes/finish.md +34 -0
  27. package/skills/agent-builder/references/chatflow/nodes/human-review.md +60 -0
  28. package/skills/agent-builder/references/chatflow/nodes/if-else.md +100 -0
  29. package/skills/agent-builder/references/chatflow/nodes/intent.md +79 -0
  30. package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +43 -0
  31. package/skills/agent-builder/references/chatflow/nodes/llm.md +117 -0
  32. package/skills/agent-builder/references/chatflow/nodes/note.md +38 -0
  33. package/skills/agent-builder/references/chatflow/nodes/question.md +97 -0
  34. package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +68 -0
  35. package/skills/agent-builder/references/chatflow/nodes/reply.md +97 -0
  36. package/skills/agent-builder/references/chatflow/nodes/start.md +35 -0
  37. package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +48 -0
  38. package/skills/agent-builder/references/chatflow/nodes/tool.md +138 -0
  39. package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +63 -0
  40. package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +52 -0
  41. package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +50 -0
  42. package/skills/agent-builder/references/chatflow/nodes/variable-process.md +70 -0
  43. package/skills/agent-builder/references/chatflow/overview.md +53 -0
  44. package/skills/agent-builder/references/chatflow/visual-translation.md +64 -0
  45. package/skills/agent-builder/references/chatflow/workflow.md +134 -0
  46. package/skills/agent-builder/references/multi-agent/create-update.md +185 -0
  47. package/skills/agent-builder/references/multi-agent/graph-spec.md +246 -0
  48. package/skills/agent-builder/references/multi-agent/graph-structure.md +12 -0
  49. package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +13 -0
  50. package/skills/agent-builder/references/multi-agent/migration/large.md +554 -0
  51. package/skills/agent-builder/references/multi-agent/migration/principles.md +446 -0
  52. package/skills/agent-builder/references/multi-agent/migration/small.md +225 -0
  53. package/skills/agent-builder/references/multi-agent/orchestration.md +11 -0
  54. package/skills/agent-builder/references/multi-agent/overview.md +48 -0
  55. package/skills/agent-builder/references/multi-agent/update.md +128 -0
  56. package/skills/agent-builder/references/multi-agent/workflow.md +87 -0
  57. package/skills/agent-builder/references/single-agent/overview.md +48 -0
  58. package/skills/agent-builder/references/single-agent/prompt.md +16 -0
  59. package/skills/agent-builder/references/single-agent/tools.md +36 -0
  60. package/skills/agent-builder/references/single-agent/voice.md +46 -0
  61. package/skills/agent-builder/references/tools/http-tool.md +315 -0
  62. package/skills/agent-builder/references/tools/mock-tool.md +279 -0
  63. package/skills/agent-builder/references/tools/overview.md +46 -0
  64. package/skills/agent-builder/references/tools/tool-config.md +43 -0
  65. package/skills/agent-builder/references/tools/x-token.md +34 -0
  66. package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +3458 -0
  67. package/skills/agent-builder/scripts/chatflow/validate_graph.py +1697 -0
  68. package/skills/agent-builder/scripts/copy_marketplace_agent.py +61 -0
  69. package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +5974 -0
  70. package/skills/agent-builder/scripts/multi-agent/replay_compare.py +679 -0
  71. package/skills/agent-builder/scripts/patch_agent_tools.py +135 -0
  72. package/skills/agent-builder/scripts/tools/validate_tool_config.py +384 -0
  73. package/skills/agent-tester/SKILL.md +35 -0
  74. package/skills/agent-tester/batch-test/overview.md +86 -0
  75. package/skills/agent-tester/scripts/batch_test_sheet.py +213 -0
  76. package/skills/agent-tester/single-test/badcase-retest.md +85 -0
  77. package/skills/agent-tester/single-test/direct-test.md +24 -0
  78. package/skills/agent-tester/single-test/overview.md +142 -0
  79. package/skills/delivery/SKILL.md +112 -0
  80. package/skills/delivery/interfaces/classify.md +38 -0
  81. package/skills/delivery/interfaces/create-agents.md +89 -0
  82. package/skills/delivery/interfaces/delivery-test.md +67 -0
  83. package/skills/delivery/references/flow.md +40 -0
  84. package/skills/delivery/references/validation.md +15 -0
  85. package/skills/delivery/scripts/batch_test.py +704 -0
  86. package/skills/delivery/scripts/classify.py +22 -0
  87. package/skills/delivery/scripts/create_agents.py +695 -0
  88. package/skills/delivery/scripts/gen_delivery_report.py +335 -0
  89. package/skills/delivery/scripts/gen_excel.py +234 -0
  90. package/skills/delivery/scripts/gen_report.py +208 -0
  91. package/skills/delivery/scripts/plan.py +163 -0
  92. package/skills/delivery/scripts/render.py +713 -0
  93. package/skills/delivery/scripts/render_summary.py +38 -0
  94. package/skills/delivery/scripts/sample_history_cases.py +109 -0
  95. package/skills/kb-builder/SKILL.md +80 -0
  96. package/skills/kb-builder/interfaces/qa-upload.md +55 -0
  97. package/skills/kb-builder/interfaces/web-kb.md +84 -0
  98. package/skills/kb-builder/references/directory.md +373 -0
  99. package/skills/kb-builder/references/export.md +282 -0
  100. package/skills/kb-builder/references/isolation-keywords.md +113 -0
  101. package/skills/kb-builder/references/knowledge.md +153 -0
  102. package/skills/kb-builder/references/product.md +52 -0
  103. package/skills/kb-builder/references/qa-from-help-center.md +58 -0
  104. package/skills/kb-builder/references/qa-from-pdf.md +94 -0
  105. package/skills/kb-builder/references/qa-from-tickets.md +168 -0
  106. package/skills/kb-builder/references/qa-from-web.md +98 -0
  107. package/skills/kb-builder/references/qa-quality.md +73 -0
  108. package/skills/kb-builder/references/reply-template.md +237 -0
  109. package/skills/kb-builder/references/tag-isolation-migration.md +61 -0
  110. package/skills/kb-builder/references/tags.md +45 -0
  111. package/skills/kb-builder/references/upload.md +130 -0
  112. package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +333 -0
  113. package/skills/kb-builder/scripts/build_directory_plan.py +144 -0
  114. package/skills/kb-builder/scripts/crawl_site.py +433 -0
  115. package/skills/kb-builder/scripts/create_dirs.py +146 -0
  116. package/skills/kb-builder/scripts/export_knowledge.py +454 -0
  117. package/skills/kb-builder/scripts/extract_ticket_qa.py +441 -0
  118. package/skills/kb-builder/scripts/extract_web_qa.py +118 -0
  119. package/skills/kb-builder/scripts/import_qa.py +219 -0
  120. package/skills/kb-builder/scripts/import_source_documents.py +143 -0
  121. package/skills/kb-builder/scripts/import_templates.py +409 -0
  122. package/skills/poc/SKILL.md +125 -0
  123. package/skills/poc/assets/default_manual.css +279 -0
  124. package/skills/poc/assets/notice_escalate.png +0 -0
  125. package/skills/poc/assets/notice_escalate2.png +0 -0
  126. package/skills/poc/assets/notice_new_chat.png +0 -0
  127. package/skills/poc/assets/poc_test_template.html +645 -0
  128. package/skills/poc/assets/poc_test_template.md +104 -0
  129. package/skills/poc/assets/step3_preview.png +0 -0
  130. package/skills/poc/assets/step4_order_note.png +0 -0
  131. package/skills/poc/interfaces/classify.md +38 -0
  132. package/skills/poc/interfaces/create-agents.md +108 -0
  133. package/skills/poc/interfaces/poc-test.md +94 -0
  134. package/skills/poc/references/flow.md +47 -0
  135. package/skills/poc/references/kb-directory.md +22 -0
  136. package/skills/poc/references/validation.md +17 -0
  137. package/skills/poc/scripts/classify.py +22 -0
  138. package/skills/poc/scripts/create_agents.py +818 -0
  139. package/skills/poc/scripts/gen_excel.py +234 -0
  140. package/skills/poc/scripts/gen_poc_manual.py +249 -0
  141. package/skills/poc/scripts/gen_report.py +208 -0
  142. package/skills/poc/scripts/inspect_chatflow.py +209 -0
  143. package/skills/poc/scripts/plan.py +178 -0
  144. package/skills/poc/scripts/poc_test.py +495 -0
  145. package/skills/poc/scripts/render.py +713 -0
  146. package/skills/poc/scripts/render_summary.py +41 -0
  147. package/skills/runtime-inspector/SKILL.md +51 -0
  148. package/skills/runtime-inspector/references/frontend-rendering.md +126 -0
  149. package/skills/runtime-inspector/references/model.md +59 -0
  150. package/skills/runtime-inspector/references/sandbox.md +131 -0
  151. package/skills/runtime-inspector/scripts/model_inspect.py +68 -0
  152. package/skills/runtime-inspector/scripts/prepare.py +98 -0
  153. package/skills/runtime-inspector/scripts/process.py +84 -0
  154. package/skills/runtime-inspector/scripts/read_state.py +32 -0
  155. package/skills/runtime-inspector/scripts/sandbox_inspect.py +463 -0
  156. package/skills/runtime-inspector/scripts/test_claude_proxy.py +49 -0
  157. package/skills/runtime-inspector/scripts/test_openai_proxy.py +41 -0
  158. package/skills/runtime-inspector/scripts/verify_persistence.py +101 -0
  159. package/skills/runtime-inspector/scripts/write_test.py +41 -0
  160. package/skills/setup/SKILL.md +17 -0
  161. package/skills/shulex-support/SKILL.md +39 -0
  162. package/skills/shulex-support/references/badcase-analysis/overview.md +34 -0
  163. package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +105 -0
  164. package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +89 -0
  165. package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +36 -0
  166. package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +53 -0
  167. package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +259 -0
  168. package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +148 -0
  169. package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +57 -0
  170. package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +120 -0
  171. package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +126 -0
  172. package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +121 -0
  173. package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +208 -0
  174. package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +115 -0
  175. package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +72 -0
  176. package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +92 -0
  177. package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +147 -0
  178. package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +283 -0
  179. package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +77 -0
  180. package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +19 -0
  181. package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +23 -0
  182. package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +31 -0
  183. package/skills/shulex-support/references/product-docs.md +70 -0
  184. package/skills/shulex-support/references/tickets.md +184 -0
  185. package/skills/shulex-support/scripts/support.py +427 -0
  186. package/skills/solvea-desk/SKILL.md +39 -0
  187. package/skills/solvea-desk/references/report/fields.md +82 -0
  188. package/skills/solvea-desk/references/report/report-design.md +200 -0
  189. package/skills/solvea-desk/references/report/report-template.html +44 -0
  190. package/skills/solvea-desk/references/report/report.css +517 -0
  191. package/skills/solvea-desk/references/report/workflow.md +464 -0
  192. package/skills/solvea-desk/references/tickets/query.md +103 -0
  193. package/skills/solvea-desk/references/tickets/tags.md +83 -0
  194. package/skills/solvea-desk/scripts/report/fetch_sources.py +580 -0
  195. package/skills/solvea-desk/scripts/report/render_report.py +763 -0
  196. package/skills/solvea-desk/scripts/tickets/desk_api.py +73 -0
  197. package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +403 -0
  198. package/skills/solvea-desk/scripts/tickets/ticket_tags.py +305 -0
  199. package/skills/solvea-kit/SKILL.md +84 -0
  200. package/skills/solvea-kit/references/agent.md +145 -0
  201. package/skills/solvea-kit/references/chat.md +244 -0
  202. package/skills/solvea-kit/references/intent.md +69 -0
  203. package/skills/solvea-kit/references/plugin.md +53 -0
  204. package/skills/solvea-kit/references/quality-inspection.md +75 -0
  205. package/skills/solvea-kit/references/strategy.md +62 -0
  206. package/skills/solvea-kit/references/tagging.md +231 -0
  207. package/skills/solvea-kit/references/variables.md +53 -0
  208. package/skills/solvea-kit/scripts/generate_label_import_file.py +151 -0
  209. package/skills/solvea-kit/scripts/intent_classify.py +843 -0
@@ -0,0 +1,234 @@
1
+ """
2
+ Generate intent classification detail Excel from items_*.json + results_*.json.
3
+
4
+ Columns: L1 Intent | L2 Intent | Case Text
5
+ L1 and L2 cells are merged across rows of the same value.
6
+
7
+ Usage:
8
+ python3 scripts/gen_excel.py [--brand "Acme"]
9
+ """
10
+
11
+ import os, sys
12
+ 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"))
13
+ from bootstrap import *
14
+
15
+ import json, argparse
16
+ import openpyxl
17
+
18
+
19
+ def _read_state() -> dict:
20
+ _sf = STATE_DIR / "progress.json"
21
+ if _sf.exists():
22
+ try:
23
+ return json.loads(_sf.read_text(encoding="utf-8"))
24
+ except Exception:
25
+ pass
26
+ return {}
27
+
28
+
29
+ def _write_state(data: dict) -> None:
30
+ _sf = STATE_DIR / "progress.json"
31
+ _sf.parent.mkdir(parents=True, exist_ok=True)
32
+ _sf.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
33
+ from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
34
+ from openpyxl.utils import get_column_letter
35
+
36
+
37
+ COLOR_PALETTE = [
38
+ "FEF3C7", "DBEAFE", "D1FAE5", "FCE7F3", "EDE9FE",
39
+ "FEE2E2", "ECFDF5", "FFF7ED", "F0F9FF", "F7FEE7",
40
+ ]
41
+
42
+
43
+ def thin_border():
44
+ s = Side(style="thin", color="D1D5DB")
45
+ return Border(left=s, right=s, top=s, bottom=s)
46
+
47
+
48
+ def build_excel(items, results, brand=""):
49
+ wb = openpyxl.Workbook()
50
+ ws = wb.active
51
+ ws.title = "Intent Detail"
52
+
53
+ all_intents = results.get("intents", []) + results.get("new_intents", [])
54
+ l1_color = {}
55
+ for i, intent in enumerate(all_intents):
56
+ l1_color[intent["name"]] = COLOR_PALETTE[i % len(COLOR_PALETTE)]
57
+
58
+ l1_canonical_l2s = {}
59
+ for intent in all_intents:
60
+ l1_canonical_l2s[intent["name"]] = {
61
+ s["name"] for s in intent.get("sub_intents", []) if s["name"] != "Other"
62
+ }
63
+
64
+ def canonical_l2(l1, l2):
65
+ allowed = l1_canonical_l2s.get(l1, set())
66
+ if not allowed:
67
+ return l2 or "Other"
68
+ return l2 if l2 in allowed else "Other"
69
+
70
+ l1_order = {intent["name"]: i for i, intent in enumerate(all_intents)}
71
+ items_sorted = sorted(items, key=lambda x: (
72
+ l1_order.get(x["l1"], 999),
73
+ canonical_l2(x["l1"], x.get("l2") or ""),
74
+ ))
75
+
76
+ headers = ["L1意图", "L2意图", "分类依据(LLM)", "title", "first_message", "messages"]
77
+ col_widths = [22, 28, 45, 25, 55, 70]
78
+
79
+ header_fill = PatternFill("solid", fgColor="1E3A5F")
80
+ header_font = Font(bold=True, color="FFFFFF", size=11)
81
+ header_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
82
+
83
+ for col, (h, w) in enumerate(zip(headers, col_widths), start=1):
84
+ cell = ws.cell(row=1, column=col, value=h)
85
+ cell.fill = header_fill
86
+ cell.font = header_font
87
+ cell.alignment = header_align
88
+ cell.border = thin_border()
89
+ ws.column_dimensions[get_column_letter(col)].width = w
90
+
91
+ ws.row_dimensions[1].height = 30
92
+ ws.freeze_panes = "A2"
93
+
94
+ row_num = 2
95
+
96
+ for item in items_sorted:
97
+ l1 = item["l1"]
98
+ l2 = item.get("l2") or "(unclassified)"
99
+ title_val = item.get("title", "")
100
+ first_msg_val = item.get("first_message", "") or item.get("text", "")
101
+ messages_val = item.get("messages", "")
102
+ raw_text = item.get("text", "")
103
+ text_val = raw_text[:1000] + f"...[截断 {len(raw_text)-1000} 字]" if len(raw_text) > 1000 else raw_text
104
+ color = l1_color.get(l1, "F9FAFB")
105
+
106
+ fill = PatternFill("solid", fgColor=color)
107
+ border = thin_border()
108
+
109
+ c1 = ws.cell(row=row_num, column=1, value=l1)
110
+ c1.fill = fill
111
+ c1.border = border
112
+ c1.alignment = Alignment(horizontal="center", vertical="top", wrap_text=True)
113
+ c1.font = Font(bold=True, size=10)
114
+
115
+ l2_display = canonical_l2(l1, l2)
116
+ l2_label = l2_display
117
+ if item.get("is_new_l2") and l2_display not in ("Other", ""):
118
+ l2_label = f"★ {l2_display}"
119
+ if item.get("is_new_l1"):
120
+ l2_label = f"[新] {l2_display}"
121
+
122
+ c2 = ws.cell(row=row_num, column=2, value=l2_label)
123
+ c2.fill = PatternFill("solid", fgColor="F8FAFC")
124
+ c2.border = border
125
+ c2.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
126
+ c2.font = Font(size=10)
127
+
128
+ for col_idx, val in [(3, text_val), (4, title_val), (5, first_msg_val), (6, messages_val)]:
129
+ cx = ws.cell(row=row_num, column=col_idx, value=val)
130
+ cx.fill = PatternFill("solid", fgColor="FFFFFF")
131
+ cx.border = border
132
+ cx.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
133
+ cx.font = Font(size=9)
134
+ ws.row_dimensions[row_num].height = max(15, min(80, len(first_msg_val) // 3))
135
+
136
+ row_num += 1
137
+
138
+ # Summary sheet
139
+ ws2 = wb.create_sheet("Summary")
140
+ ws2.column_dimensions["A"].width = 28
141
+ ws2.column_dimensions["B"].width = 28
142
+ ws2.column_dimensions["C"].width = 10
143
+ ws2.column_dimensions["D"].width = 10
144
+
145
+ h_fill = PatternFill("solid", fgColor="1E3A5F")
146
+ h_font = Font(bold=True, color="FFFFFF", size=11)
147
+ for col, val in enumerate(["一级意图", "二级意图", "数量", "占比 %"], start=1):
148
+ c = ws2.cell(row=1, column=col, value=val)
149
+ c.fill = h_fill
150
+ c.font = h_font
151
+ c.alignment = Alignment(horizontal="center", vertical="center")
152
+ c.border = thin_border()
153
+ ws2.row_dimensions[1].height = 28
154
+ ws2.freeze_panes = "A2"
155
+
156
+ sr = 2
157
+ for intent in all_intents:
158
+ l1n = intent["name"]
159
+ color = l1_color.get(l1n, "F9FAFB")
160
+ fill = PatternFill("solid", fgColor=color)
161
+ subs = intent.get("sub_intents", [])
162
+ if not subs:
163
+ c = ws2.cell(row=sr, column=1, value=l1n)
164
+ c.fill = fill; c.font = Font(bold=True, size=10); c.border = thin_border()
165
+ c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
166
+ ws2.cell(row=sr, column=2, value="(no sub-intents)").border = thin_border()
167
+ ws2.cell(row=sr, column=3, value=intent["count"]).border = thin_border()
168
+ ws2.cell(row=sr, column=4, value=round(intent["pct"], 1)).border = thin_border()
169
+ sr += 1
170
+ else:
171
+ l1_start_row = sr
172
+ for sub in subs:
173
+ c = ws2.cell(row=sr, column=1, value=l1n)
174
+ c.fill = fill; c.font = Font(bold=True, size=10); c.border = thin_border()
175
+ c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
176
+ ws2.cell(row=sr, column=2, value=sub["name"]).border = thin_border()
177
+ ws2.cell(row=sr, column=2).alignment = Alignment(horizontal="left", vertical="center")
178
+ ws2.cell(row=sr, column=3, value=sub["count"]).border = thin_border()
179
+ ws2.cell(row=sr, column=3).alignment = Alignment(horizontal="center")
180
+ ws2.cell(row=sr, column=4, value=round(sub["pct"], 1)).border = thin_border()
181
+ ws2.cell(row=sr, column=4).alignment = Alignment(horizontal="center")
182
+ sr += 1
183
+ if sr - 1 > l1_start_row:
184
+ ws2.merge_cells(start_row=l1_start_row, start_column=1,
185
+ end_row=sr - 1, end_column=1)
186
+ ws2.cell(row=l1_start_row, column=1).alignment = Alignment(
187
+ horizontal="center", vertical="center", wrap_text=True)
188
+
189
+ return wb
190
+
191
+
192
+ def main():
193
+ parser = argparse.ArgumentParser()
194
+ parser.add_argument("--brand", default="")
195
+ parser.add_argument("--results", default=None,
196
+ help="Override results JSON path (default: read from state)")
197
+ parser.add_argument("--items", default=None,
198
+ help="Override items JSON path (default: read from state)")
199
+ args = parser.parse_args()
200
+
201
+ state = _read_state()
202
+ results_path = args.results or state.get("results_path")
203
+ items_path = args.items or state.get("items_path")
204
+
205
+ if not results_path or not os.path.exists(results_path):
206
+ raise FileNotFoundError(
207
+ f"Results file missing (state.results_path={results_path}). Run classify.py first."
208
+ )
209
+ if not items_path or not os.path.exists(items_path):
210
+ raise FileNotFoundError(
211
+ f"Items file missing (state.items_path={items_path}). "
212
+ "Run classify.py with --xlsx first."
213
+ )
214
+
215
+ with open(items_path) as f:
216
+ items = json.load(f)
217
+ with open(results_path) as f:
218
+ results = json.load(f)
219
+
220
+ wb = build_excel(items, results, args.brand)
221
+
222
+ OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
223
+ out_path = str(OUTPUTS_DIR / f"detail_{timestamp()}.xlsx")
224
+ wb.save(out_path)
225
+ print(f"[gen_excel] Excel -> {out_path} ({len(items)} rows)")
226
+
227
+ state = _read_state()
228
+ state["excel_path"] = out_path
229
+ state["updated_at"] = timestamp()
230
+ _write_state(state)
231
+
232
+
233
+ if __name__ == "__main__":
234
+ main()
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """POC 测试手册 HTML 生成器。
3
+
4
+ 读取 poc_test.py 输出的 results JSON + 一份模板 + 一份 CSS,
5
+ 渲染成单文件 HTML(图片 base64 嵌入,无外部依赖、可邮件直发)。
6
+
7
+ 模板路由:
8
+ .html → Jinja2 渲染(推荐);py 侧传 by_agent dict,模板自己写循环
9
+ .md → 传统 markdown 渲染(旧模板,保留供回退)
10
+
11
+ Jinja2 模板可用变量:
12
+ 顶层:title, brand_name, brand_website, generated_at, total_cases,
13
+ console_url, agents_url, account_email, account_password,
14
+ scene_count, by_agent
15
+ by_agent:dict[str, list[case]],已过滤掉无 AI 回复的 case 和空 agent,
16
+ case 字段:name, agent_name, agent_id, scenario, results, stopped_reason
17
+ results 字段:turn, input, content, chatId, handoff, ...
18
+
19
+ 旧 .md 模板额外占位符({{var}} 替换):
20
+ scene_tags_html, agent_sections_html
21
+
22
+ 用法:
23
+ python3 gen_poc_manual.py --results results.json \\
24
+ --output manual.html \\
25
+ [--template assets/poc_test_template.html] \\
26
+ [--css assets/default_manual.css] \\
27
+ [--brand-name WOLTU] [--brand-website https://woltu.de] \\
28
+ [--account-email a@b.com] [--password 123456] [--title "POC 测试手册"]
29
+ """
30
+
31
+ import argparse
32
+ import base64
33
+ import json
34
+ import mimetypes
35
+ import os
36
+ import re
37
+ import sys
38
+ from collections import defaultdict
39
+ from datetime import datetime
40
+ from pathlib import Path
41
+
42
+ 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"))
43
+ from bootstrap import * # noqa: F401,F403
44
+
45
+ import markdown # noqa: E402
46
+ from jinja2 import Environment, BaseLoader # noqa: E402
47
+
48
+ ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
49
+ DEFAULT_CONSOLE_URL = "https://apps.voc.ai/chatbot#/setting"
50
+ DEFAULT_AGENTS_URL = "https://apps.voc.ai/chatbot#/agents"
51
+ DEFAULT_TEMPLATE = str(ASSETS_DIR / "poc_test_template.html")
52
+ DEFAULT_CSS = str(ASSETS_DIR / "default_manual.css")
53
+
54
+
55
+ # ── Helpers ──────────────────────────────────────────────────────────────────
56
+
57
+ def _esc(s: str) -> str:
58
+ return (s or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
59
+
60
+
61
+ def _has_reply(case: dict) -> bool:
62
+ """case 中是否有至少一轮 AI 实际回复了内容(过滤掉全部出错 / 无 content 的 case)。"""
63
+ return any(t.get("content") for t in case.get("results", []))
64
+
65
+
66
+ # ── MD-path rendering (legacy, kept for rollback) ────────────────────────────
67
+
68
+ def _render_dialogues(turns) -> str:
69
+ rows = []
70
+ for t in turns:
71
+ u = _esc(t.get("input", ""))
72
+ b = _esc(t.get("content", "")).replace("\n", "<br>")
73
+ rows.append(
74
+ f'<div class="msg"><span class="label">👤 User</span>'
75
+ f'<div class="bubble user-bubble">{u}</div></div>'
76
+ )
77
+ rows.append(
78
+ f'<div class="msg"><span class="label">🤖 AI</span>'
79
+ f'<div class="bubble bot-bubble">{b}</div></div>'
80
+ )
81
+ return "\n".join(rows)
82
+
83
+
84
+ def _render_cases(cases) -> str:
85
+ out = []
86
+ for c in cases:
87
+ results = c.get("results", [])
88
+ chat_id = results[-1].get("chatId", "—") if results else "—"
89
+ scenario = c.get("scenario") or c.get("name", "")
90
+ case_id = c.get("name", "")
91
+ out.append(f"""<div class="case-card">
92
+ <div class="case-header">
93
+ <span class="case-id">{_esc(case_id)}</span>
94
+ <span class="case-scenario">{_esc(scenario)}</span>
95
+ <span class="chat-id-badge">Chat ID: <code>{_esc(chat_id)}</code></span>
96
+ </div>
97
+ <div class="dialogue">{_render_dialogues(results)}</div>
98
+ </div>""")
99
+ return "\n".join(out)
100
+
101
+
102
+ def _render_agent_sections(by_agent: dict) -> str:
103
+ out = []
104
+ for aname, cases in by_agent.items():
105
+ visible = [c for c in cases if _has_reply(c)]
106
+ if not visible:
107
+ continue
108
+ aid = visible[0].get("agent_id", "")
109
+ out.append(
110
+ f'<section class="agent-section">'
111
+ f'<h2>🤖 {_esc(aname)} <span class="agent-meta">Agent ID: {aid}</span></h2>'
112
+ f'{_render_cases(visible)}</section>'
113
+ )
114
+ return "\n".join(out)
115
+
116
+
117
+ def _render_scene_tags(by_agent: dict) -> str:
118
+ return "".join(f'<span class="scene-tag">{_esc(n)}</span>' for n in by_agent.keys())
119
+
120
+
121
+ def render_template(tpl: str, ctx: dict) -> str:
122
+ """轻量模板替换:仅 {{var}} 占位符(旧 .md 路径使用)。"""
123
+ def _sub(match):
124
+ key = match.group(1).strip()
125
+ val = ctx.get(key, "")
126
+ return str(val) if val is not None else ""
127
+ return re.sub(r"\{\{\s*([\w\.]+)\s*\}\}", _sub, tpl)
128
+
129
+
130
+ # ── Image embedding ───────────────────────────────────────────────────────────
131
+
132
+ def _embed_images_as_base64(html: str, base_dir: Path) -> str:
133
+ """把 <img src="<rel_path>"> 替换成 data: base64 内嵌。
134
+
135
+ 跳过远程 URL(http/https/data:)。本地路径找不到时打 warning 但不阻断渲染。
136
+ """
137
+ def _sub(m):
138
+ prefix = m.group(1)
139
+ src = m.group(2)
140
+ suffix = m.group(3)
141
+ if src.startswith(("http://", "https://", "data:")):
142
+ return m.group(0)
143
+ img_path = (base_dir / src).resolve()
144
+ if not img_path.exists():
145
+ print(f"[warn] image not found, kept original src: {img_path}", file=sys.stderr)
146
+ return m.group(0)
147
+ mime, _ = mimetypes.guess_type(str(img_path))
148
+ if not mime:
149
+ mime = "image/png"
150
+ b64 = base64.b64encode(img_path.read_bytes()).decode("ascii")
151
+ return f'{prefix}data:{mime};base64,{b64}{suffix}'
152
+ return re.sub(r'(<img[^>]*\ssrc=")([^"]+)(")', _sub, html)
153
+
154
+
155
+ def _wrap_html(body: str, title: str, css: str) -> str:
156
+ return f"""<!doctype html>
157
+ <html lang="zh-CN">
158
+ <head>
159
+ <meta charset="utf-8">
160
+ <meta name="viewport" content="width=device-width, initial-scale=1">
161
+ <title>{_esc(title)}</title>
162
+ <style>
163
+ {css}
164
+ </style>
165
+ </head>
166
+ <body>
167
+ {body}
168
+ </body>
169
+ </html>
170
+ """
171
+
172
+
173
+ # ── Main ──────────────────────────────────────────────────────────────────────
174
+
175
+ def main():
176
+ parser = argparse.ArgumentParser(description="POC 测试手册 HTML 生成器")
177
+ parser.add_argument("--results", required=True, help="poc_test.py 输出的 results JSON 路径")
178
+ parser.add_argument("--output", required=True, help="输出 HTML 路径")
179
+ parser.add_argument("--title", default="Solvea POC 测试手册", help="手册标题")
180
+ parser.add_argument("--brand-name", default="", help="客户品牌名")
181
+ parser.add_argument("--brand-website", default="", help="客户品牌官网 URL")
182
+ parser.add_argument("--console-url", default=DEFAULT_CONSOLE_URL)
183
+ parser.add_argument("--agents-url", default=DEFAULT_AGENTS_URL)
184
+ parser.add_argument("--account-email", default=os.environ.get("SOLVEA_ACCOUNT_EMAIL", "请联系实施顾问获取"))
185
+ parser.add_argument("--password", default="123456")
186
+ args = parser.parse_args()
187
+
188
+ summary = json.loads(Path(args.results).read_text(encoding="utf-8"))
189
+ results = summary.get("results", [])
190
+
191
+ # 按 agent 分组,并过滤掉无 AI 回复的 case 和全空的 agent section
192
+ by_agent_raw: dict = defaultdict(list)
193
+ for r in results:
194
+ by_agent_raw[r.get("agent_name") or "(unknown)"].append(r)
195
+
196
+ by_agent = {
197
+ aname: visible
198
+ for aname, cases in by_agent_raw.items()
199
+ if (visible := [c for c in cases if _has_reply(c)])
200
+ }
201
+
202
+ tpl_path = Path(DEFAULT_TEMPLATE)
203
+ tpl = tpl_path.read_text(encoding="utf-8")
204
+
205
+ ctx = {
206
+ "title": args.title,
207
+ "brand_name": args.brand_name,
208
+ "brand_website": args.brand_website,
209
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
210
+ "total_cases": len(results),
211
+ "console_url": args.console_url,
212
+ "agents_url": args.agents_url,
213
+ "account_email": args.account_email,
214
+ "account_password": args.password,
215
+ "scene_count": len(by_agent),
216
+ "by_agent": by_agent,
217
+ }
218
+
219
+ if tpl_path.suffix.lower() == ".html":
220
+ # Jinja2 渲染:模板自带 CSS + 循环,直接输出完整 HTML
221
+ env = Environment(loader=BaseLoader(), autoescape=False)
222
+ template = env.from_string(tpl)
223
+ html = template.render(**ctx)
224
+ html = _embed_images_as_base64(html, base_dir=tpl_path.parent)
225
+ else:
226
+ # 旧 .md 路径(保留供回退)
227
+ md_ctx = {
228
+ **ctx,
229
+ "scene_tags_html": _render_scene_tags(by_agent),
230
+ "agent_sections_html": _render_agent_sections(by_agent_raw),
231
+ }
232
+ rendered = render_template(tpl, md_ctx)
233
+ body = markdown.markdown(
234
+ rendered,
235
+ extensions=["tables", "fenced_code", "md_in_html", "sane_lists"],
236
+ )
237
+ body = _embed_images_as_base64(body, base_dir=tpl_path.parent)
238
+ css = Path(args.css).read_text(encoding="utf-8")
239
+ html = _wrap_html(body, title=args.title, css=css)
240
+
241
+ out_path = Path(args.output)
242
+ out_path.parent.mkdir(parents=True, exist_ok=True)
243
+ out_path.write_text(html, encoding="utf-8")
244
+ size_kb = out_path.stat().st_size / 1024
245
+ print(f" Manual rendered → {out_path} ({size_kb:.1f} KB)", file=sys.stderr)
246
+
247
+
248
+ if __name__ == "__main__":
249
+ main()
@@ -0,0 +1,208 @@
1
+ """
2
+ HTML report generator for poc skill.
3
+
4
+ Reads classification results (workspace/state/poc/progress.json -> results_path) and
5
+ renders a polished HTML report under workspace/outputs/poc/.
6
+
7
+ Usage:
8
+ python3 scripts/gen_report.py [--brand "Acme"]
9
+ """
10
+
11
+ import os, sys
12
+ 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"))
13
+ from bootstrap import *
14
+
15
+ import json, argparse
16
+ from uuid import uuid4
17
+
18
+
19
+ def _read_state() -> dict:
20
+ _sf = STATE_DIR / "progress.json"
21
+ if _sf.exists():
22
+ try:
23
+ return json.loads(_sf.read_text(encoding="utf-8"))
24
+ except Exception:
25
+ pass
26
+ return {}
27
+
28
+
29
+ def _write_state(data: dict) -> None:
30
+ _sf = STATE_DIR / "progress.json"
31
+ _sf.parent.mkdir(parents=True, exist_ok=True)
32
+ _sf.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
33
+
34
+ from render import render_html
35
+
36
+ COLOR_PALETTE = [
37
+ "#ef4444", "#f97316", "#eab308", "#22c55e",
38
+ "#14b8a6", "#3b82f6", "#8b5cf6", "#ec4899",
39
+ "#06b6d4", "#84cc16",
40
+ ]
41
+
42
+ CIRCLE_NUMS = "①②③④⑤⑥⑦⑧⑨⑩"
43
+
44
+
45
+ def build_config_from_results(results, brand_name=""):
46
+ def uid():
47
+ return uuid4().hex
48
+
49
+ agents = []
50
+ all_intents = results.get("intents", []) + results.get("new_intents", [])
51
+ meta = results.get("meta", {})
52
+ total = meta.get("total", 0)
53
+
54
+ for i, intent in enumerate(all_intents):
55
+ sub_intents = intent.get("sub_intents", [])
56
+ is_new = intent.get("is_new", False)
57
+ agent_id = f"intent-{i}"
58
+
59
+ l2_nodes = []
60
+ for j, s in enumerate(sub_intents):
61
+ sample_preview = s["samples"][0][:50] + "..." if s.get("samples") else ""
62
+ l2_nodes.append({
63
+ "id": f"n2-{uid()[:8]}",
64
+ "name": s["name"],
65
+ "desc": f"{s['pct']:.1f}% — {sample_preview}" if sample_preview else f"{s['pct']:.1f}%",
66
+ "count": s["count"],
67
+ "definition": s.get("definition", ""),
68
+ "is_shared": False,
69
+ "shared_agents": [],
70
+ "samples": s.get("samples", []),
71
+ })
72
+
73
+ nodes_ac = []
74
+ for s in sub_intents[:3]:
75
+ nodes_ac.append({
76
+ "name": f"节点 · {s['name']}",
77
+ "trigger": f"客户投诉:{s['name']}",
78
+ "action": f"<code>LLM</code> 处理 {s['name']},引导解决",
79
+ })
80
+
81
+ agents.append({
82
+ "id": agent_id,
83
+ "name": f"{CIRCLE_NUMS[i % len(CIRCLE_NUMS)]} {intent['name']}",
84
+ "color": COLOR_PALETTE[i % len(COLOR_PALETTE)],
85
+ "l1_intents": [intent["name"]],
86
+ "total_count": intent["count"],
87
+ "total_pct": intent["pct"],
88
+ "is_new": is_new,
89
+ "intent_config": {
90
+ "definition": intent.get("definition", ""),
91
+ "excludes": [],
92
+ "examples": intent.get("samples", [])[:3],
93
+ "covers": [s["name"] for s in sub_intents],
94
+ },
95
+ "agent_config": {
96
+ "type": "chatflow",
97
+ "nodes": nodes_ac,
98
+ "footer": f"<b>结束:</b>解决{intent['name']}问题 / 转人工",
99
+ },
100
+ "l2_nodes": l2_nodes,
101
+ })
102
+
103
+ edges = []
104
+ for a in agents:
105
+ ag_html_id = f"ag-{a['id'].replace('_', '-')}"
106
+ color = a.get("color", "#6b7280")
107
+ for node in a["l2_nodes"]:
108
+ count = node.get("count", 0)
109
+ if count > 0:
110
+ edges.append({"from": ag_html_id, "to": node["id"], "count": count, "color": color})
111
+
112
+ total_nodes = sum(len(a["l2_nodes"]) for a in agents)
113
+ n_known = meta.get("n_known", len(results.get("intents", [])))
114
+ n_new = meta.get("n_new", len(results.get("new_intents", [])))
115
+
116
+ rows = []
117
+ for a in agents:
118
+ n = len(a["l2_nodes"])
119
+ badge = f"{n} 节点 ✓" if n <= 2 else (f"{n} 节点 ⚠️" if n <= 3 else f"{n} 节点 ❌")
120
+ rows.append({
121
+ "agent_name": a["name"],
122
+ "color": a["color"],
123
+ "node_count": n,
124
+ "badge": badge,
125
+ })
126
+
127
+ l2_total = sum(s["count"] for intent in all_intents for s in intent.get("sub_intents", []))
128
+ opt_notes = (
129
+ f"✅ 已知意图 {n_known} 个,新发现意图 {n_new} 个,共 {n_known + n_new} 个 L1 意图\n\n"
130
+ f"📊 数据基础:{total:,} 条对话 / L2 分类:{l2_total:,} 条"
131
+ )
132
+ if n_new > 0:
133
+ new_names = [i["name"] for i in results.get("new_intents", [])]
134
+ opt_notes += f"\n\n⚡ 新发现意图:{' · '.join(new_names)}"
135
+
136
+ return {
137
+ "agents": agents,
138
+ "filter_block": {"types": ["未分类投诉"], "count": 0},
139
+ "node_summary": {
140
+ "rows": rows,
141
+ "total_nodes": total_nodes,
142
+ "optimization_notes": opt_notes,
143
+ },
144
+ "shared_nodes": [],
145
+ "edges": edges,
146
+ }
147
+
148
+
149
+ def _resolve_results_path(state: dict) -> str:
150
+ path = state.get("results_path")
151
+ if not path:
152
+ raise FileNotFoundError(
153
+ "No results_path in state. Run classify.py first."
154
+ )
155
+ if not os.path.exists(path):
156
+ raise FileNotFoundError(
157
+ f"Results file missing: {path}. Run classify.py first."
158
+ )
159
+ return path
160
+
161
+
162
+ def main():
163
+ parser = argparse.ArgumentParser(description="Generate poc intent classification HTML report")
164
+ parser.add_argument("--brand", default="")
165
+ parser.add_argument("--results", default=None,
166
+ help="Override results JSON path (default: read from progress.json)")
167
+ args = parser.parse_args()
168
+
169
+ if args.results:
170
+ results_path = args.results
171
+ if not os.path.exists(results_path):
172
+ raise FileNotFoundError(f"Results file missing: {results_path}")
173
+ else:
174
+ state = _read_state()
175
+ results_path = _resolve_results_path(state)
176
+
177
+ with open(results_path) as f:
178
+ results = json.load(f)
179
+
180
+ config = build_config_from_results(results, brand_name=args.brand)
181
+
182
+ meta = results.get("meta", {})
183
+ data_compat = {
184
+ "_meta": {"total": meta.get("total", 0), "brand": args.brand},
185
+ "tree": {},
186
+ "turn_distribution": {},
187
+ "fuzzy_samples": {},
188
+ }
189
+
190
+ project_id = os.environ.get("SOLVEA_PROJECT_ID", "")
191
+ html = render_html(config, data_compat, brand=args.brand, project_id=project_id)
192
+
193
+ OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
194
+ report_path = str(OUTPUTS_DIR / f"report_{timestamp()}.html")
195
+ with open(report_path, "w", encoding="utf-8") as f:
196
+ f.write(html)
197
+ print(f"[gen_report] Report -> {report_path}")
198
+
199
+ state = _read_state()
200
+ state["report_path"] = report_path
201
+ state["updated_at"] = timestamp()
202
+ _write_state(state)
203
+
204
+ print(f"[gen_report] Done.")
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()