@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,335 @@
1
+ #!/usr/bin/env python3
2
+ """Delivery 批测报告生成器:读 batch_test.py 输出的 results JSON,渲染 HTML+MD。
3
+
4
+ 输入 results JSON schema(与 batch_test.py 输出一致):
5
+ {
6
+ "summary": {total_conversations, total_messages, total_replied, reply_rate,
7
+ agents:[...], handoff:[...], errors:[...], no_reply_reasons:[...]},
8
+ "details": [{source_chat_id, new_chat_id, intent, action, round,
9
+ question, answer, no_reply_reason, judge, correct}, ...]
10
+ }
11
+
12
+ 输出:
13
+ - HTML:交互式(KPI / agent / handoff / error / 详情 / Judge / CSV 下载)
14
+ - MD :纯文本表格
15
+
16
+ 用法:
17
+ python3 gen_delivery_report.py --results results.json
18
+ python3 gen_delivery_report.py --results results.json --html out.html --md out.md
19
+ python3 gen_delivery_report.py --results results.json --no-md
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+
27
+ 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"))
28
+ from bootstrap import * # noqa: F401,F403
29
+
30
+
31
+ def _pct(v):
32
+ """Format float 0-1 as percentage string, or '-' if None."""
33
+ if v is None:
34
+ return '-'
35
+ return f"{v * 100:.1f}%"
36
+
37
+
38
+ def generate_html_report(summary: dict, details: list, output_path: str):
39
+ """Generate a self-contained HTML report and write to output_path."""
40
+ agents_rows = ""
41
+ for a in summary.get('agents', []):
42
+ agents_rows += f"""
43
+ <tr>
44
+ <td>{a.get('name','')}</td>
45
+ <td>{a.get('total_conversations',0)}</td>
46
+ <td>{a.get('total_messages',0)}</td>
47
+ <td>{_pct(a.get('reply_rate'))}</td>
48
+ <td>{_pct(a.get('r1_reply_rate'))}</td>
49
+ <td>{_pct(a.get('r2_reply_rate'))}</td>
50
+ <td>{_pct(a.get('r3_reply_rate'))}</td>
51
+ <td>{_pct(a.get('rs_reply_rate'))}</td>
52
+ </tr>"""
53
+
54
+ handoff_rows = "".join(
55
+ f"<tr><td>{h['type']}</td><td>{h['count']}</td><td>{_pct(h.get('rate'))}</td></tr>"
56
+ for h in summary.get('handoff', [])
57
+ ) or "<tr><td colspan='3' style='color:#999'>无</td></tr>"
58
+
59
+ error_rows = "".join(
60
+ f"<tr><td>{e['type']}</td><td>{e['count']}</td><td>{_pct(e.get('rate'))}</td></tr>"
61
+ for e in summary.get('errors', [])
62
+ ) or "<tr><td colspan='3' style='color:#999'>无</td></tr>"
63
+
64
+ detail_rows = ""
65
+ for d in details:
66
+ src = d.get('source_chat_id', '')
67
+ new = d.get('new_chat_id', '')
68
+ chat_display = f"{new}<br><small style='color:#999'>{src}</small>" if src else new
69
+ answer_raw = d.get('answer', '')
70
+ if isinstance(answer_raw, dict):
71
+ answer_raw = answer_raw.get('content', str(answer_raw))
72
+ answer = str(answer_raw).replace('<', '&lt;').replace('>', '&gt;')
73
+ question_raw = d.get('question', '')
74
+ if isinstance(question_raw, dict):
75
+ question_raw = question_raw.get('content', str(question_raw))
76
+ question = str(question_raw).replace('<', '&lt;').replace('>', '&gt;')
77
+ ng_cls = ' active' if d.get('judge', '') in ('✗', 'ng', 'fail', 'FAIL', '0') else ''
78
+ correct_esc = d.get('correct', '').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
79
+ detail_rows += f"""
80
+ <tr>
81
+ <td class='col-chatid'><div class='cc'>{chat_display}</div></td>
82
+ <td class='col-round' style='text-align:center'>{d.get('round','')}</td>
83
+ <td class='col-intent'><div class='cc'>{d.get('intent','')}</div></td>
84
+ <td class='col-agent'><div class='cc'>{d.get('action','')}</div></td>
85
+ <td class='col-q'><div class='cc'>{question}</div></td>
86
+ <td class='col-a'><div class='cc'>{answer}</div></td>
87
+ <td class='col-judge'><div class='jcell'><button type='button' class='jbtn ng{ng_cls}' onclick='toggleJudge(this)'>&#10007;</button></div></td>
88
+ <td class='col-correct'><textarea class='rinp' rows='3' placeholder='填写正确回答...'>{correct_esc}</textarea></td>
89
+ </tr>"""
90
+
91
+ reply_rate_pct = _pct(summary.get('reply_rate'))
92
+ html = rf"""<!DOCTYPE html>
93
+ <html lang="zh">
94
+ <head>
95
+ <meta charset="UTF-8">
96
+ <meta name="viewport" content="width=device-width,initial-scale=1">
97
+ <title>批测报告</title>
98
+ <style>
99
+ *{{box-sizing:border-box;margin:0;padding:0}}
100
+ body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
101
+ background:#f5f6fa;color:#2d3436;font-size:14px;padding:24px}}
102
+ h1{{font-size:20px;font-weight:700;margin-bottom:20px;color:#1a1a2e}}
103
+ h2{{font-size:15px;font-weight:600;margin:28px 0 12px;color:#444}}
104
+ .kpi-grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:8px}}
105
+ .kpi{{background:#fff;border-radius:10px;padding:18px 20px;
106
+ box-shadow:0 1px 4px rgba(0,0,0,.08)}}
107
+ .kpi .label{{font-size:12px;color:#888;margin-bottom:6px}}
108
+ .kpi .value{{font-size:26px;font-weight:700;color:#2d3436}}
109
+ .kpi .value.green{{color:#00b894}}
110
+ table{{width:100%;border-collapse:collapse;background:#fff;border-radius:10px;
111
+ box-shadow:0 1px 4px rgba(0,0,0,.08);overflow:hidden}}
112
+ th{{background:#f0f0f5;padding:10px 14px;text-align:left;
113
+ font-size:12px;font-weight:600;color:#555;white-space:nowrap}}
114
+ td{{padding:9px 14px;border-top:1px solid #f0f0f0;vertical-align:top;line-height:1.5}}
115
+ tr:hover td{{background:#fafbff}}
116
+ .dist-grid{{display:grid;grid-template-columns:1fr 1fr;gap:16px}}
117
+ .detail-wrap{{overflow-x:auto;border-radius:10px;
118
+ box-shadow:0 1px 4px rgba(0,0,0,.08)}}
119
+ .detail-wrap table{{border-radius:0;table-layout:fixed;width:100%}}
120
+ .col-chatid{{width:155px}}.col-round{{width:44px}}.col-intent{{width:110px}}
121
+ .col-agent{{width:130px}}.col-q{{width:240px}}.col-a{{width:280px}}
122
+ .col-judge{{width:44px}}.col-correct{{width:222px}}
123
+ .cc{{max-height:80px;overflow:hidden;word-break:break-word;
124
+ white-space:pre-wrap;font-size:13px;line-height:1.45;position:relative}}
125
+ .cc.open{{max-height:none}}
126
+ .xbtn{{display:none;color:#0984e3;font-size:11px;cursor:pointer;
127
+ margin-top:3px;user-select:none;white-space:nowrap}}
128
+ .xbtn.vis{{display:inline-block}}
129
+ .jcell{{display:flex;gap:5px;justify-content:center;padding-top:2px}}
130
+ .jbtn{{-webkit-appearance:none;appearance:none;border:1px solid #ddd;
131
+ background:#f7f7f7;border-radius:5px;padding:4px 9px;cursor:pointer;
132
+ font-size:15px;color:#bbb;
133
+ transition:background .15s,color .15s,border-color .15s;line-height:1}}
134
+ .jbtn.ng.active{{background:#e17055 !important;border-color:#e17055 !important;color:#fff !important}}
135
+ .jbtn:hover:not(.active){{background:#eee;color:#666}}
136
+ .rinp{{width:100%;box-sizing:border-box;border:1px solid #e0e0e0;border-radius:5px;
137
+ padding:5px 7px;font-size:12px;font-family:inherit;color:#444;
138
+ resize:vertical;min-height:54px;line-height:1.4}}
139
+ .rinp:focus{{outline:none;border-color:#0984e3;box-shadow:0 0 0 2px rgba(9,132,227,.1)}}
140
+ .dl-btn{{background:#0984e3;color:#fff;border:none;border-radius:6px;
141
+ padding:6px 14px;font-size:13px;cursor:pointer;white-space:nowrap}}
142
+ .dl-btn:hover{{background:#076dc1}}
143
+ small{{font-size:11px}}
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <h1>批测报告</h1>
148
+
149
+ <h2>KPI 总览</h2>
150
+ <div class="kpi-grid">
151
+ <div class="kpi"><div class="label">总会话数</div>
152
+ <div class="value">{summary.get('total_conversations',0)}</div></div>
153
+ <div class="kpi"><div class="label">总消息数</div>
154
+ <div class="value">{summary.get('total_messages',0)}</div></div>
155
+ <div class="kpi"><div class="label">回复消息数</div>
156
+ <div class="value">{summary.get('total_replied',0)}</div></div>
157
+ <div class="kpi"><div class="label">整体回复率</div>
158
+ <div class="value green">{reply_rate_pct}</div></div>
159
+ </div>
160
+
161
+ <h2>Agent Performance</h2>
162
+ <table>
163
+ <thead><tr>
164
+ <th>Action</th><th>会话数</th><th>消息数</th><th>回复率</th>
165
+ <th>R1</th><th>R2</th><th>R3</th><th>Rs</th>
166
+ </tr></thead>
167
+ <tbody>{agents_rows}</tbody>
168
+ </table>
169
+
170
+ <h2>分布详情</h2>
171
+ <div class="dist-grid">
172
+ <div>
173
+ <h2 style="margin-top:0">Handoff 分布</h2>
174
+ <table>
175
+ <thead><tr><th>类型</th><th>数量</th><th>占比</th></tr></thead>
176
+ <tbody>{handoff_rows}</tbody>
177
+ </table>
178
+ </div>
179
+ <div>
180
+ <h2 style="margin-top:0">Error 分布</h2>
181
+ <table>
182
+ <thead><tr><th>类型</th><th>数量</th><th>占比</th></tr></thead>
183
+ <tbody>{error_rows}</tbody>
184
+ </table>
185
+ </div>
186
+ </div>
187
+
188
+ <div style='display:flex;align-items:center;justify-content:space-between;margin:28px 0 12px'><h2 style='margin:0'>回复详情</h2><button class='dl-btn' onclick='downloadCSV()'>&#11015; 下载 CSV</button></div>
189
+ <div class="detail-wrap">
190
+ <table>
191
+ <thead><tr>
192
+ <th class='col-chatid'>Chat ID</th><th class='col-round'>Round</th>
193
+ <th class='col-intent'>意图</th><th class='col-agent'>Action</th>
194
+ <th class='col-q'>Question</th><th class='col-a'>Answer</th>
195
+ <th class='col-judge'>Judge</th><th class='col-correct'>Correct</th>
196
+ </tr></thead>
197
+ <tbody>{detail_rows}</tbody>
198
+ </table>
199
+ </div>
200
+
201
+ <script>
202
+ document.querySelectorAll('.cc').forEach(function(el){{
203
+ if(el.scrollHeight > el.clientHeight + 4){{
204
+ var b = document.createElement('span');
205
+ b.className = 'xbtn vis';
206
+ b.textContent = '展开';
207
+ b.onclick = function(){{
208
+ var open = el.classList.toggle('open');
209
+ b.textContent = open ? '收起' : '展开';
210
+ }};
211
+ el.parentNode.appendChild(b);
212
+ }}
213
+ }});
214
+ function toggleJudge(btn){{ btn.classList.toggle('active'); }}
215
+ function downloadCSV(){{
216
+ var hdr = ['new_chat_id','source_chat_id','round','intent','action','question','answer','judge','correct'];
217
+ var rows = [hdr];
218
+ document.querySelectorAll('.detail-wrap tbody tr').forEach(function(tr){{
219
+ var td = tr.querySelectorAll('td');
220
+ function txt(el){{ return el ? (el.innerText||el.textContent||'').replace(/\s+/g,' ').trim() : ''; }}
221
+ var chatRaw = txt(td[0].querySelector('.cc'));
222
+ var parts = chatRaw.split('\\n');
223
+ var newId = parts[0]||''; var srcId = parts[1]||'';
224
+ var round = txt(td[1]);
225
+ var intent = txt(td[2].querySelector('.cc'));
226
+ var action = txt(td[3].querySelector('.cc'));
227
+ var question = txt(td[4].querySelector('.cc'));
228
+ var answer = txt(td[5].querySelector('.cc'));
229
+ var ab = td[6].querySelector('.jbtn.active');
230
+ var judge = ab ? '✗' : '';
231
+ var correct = td[7].querySelector('.rinp') ? td[7].querySelector('.rinp').value : '';
232
+ rows.push([newId,srcId,round,intent,action,question,answer,judge,correct]);
233
+ }});
234
+ var csv = rows.map(function(r){{
235
+ return r.map(function(v){{
236
+ return '"' + (v||'').replace(/\\r?\\n/g,' ').replace(/"/g,'""') + '"';
237
+ }}).join(',');
238
+ }}).join('\\r\\n');
239
+ var blob = new Blob([''+csv],{{type:'text/csv;charset=utf-8'}});
240
+ var url = URL.createObjectURL(blob);
241
+ var a = document.createElement('a');
242
+ a.href=url; a.download='batch_test_result.csv';
243
+ document.body.appendChild(a); a.click();
244
+ document.body.removeChild(a); URL.revokeObjectURL(url);
245
+ }}
246
+ </script>
247
+ </body></html>"""
248
+
249
+ with open(output_path, 'w', encoding='utf-8') as f:
250
+ f.write(html)
251
+
252
+
253
+ def generate_md_report(summary: dict, details: list, output_path: str):
254
+ """Generate a Markdown report and write to output_path."""
255
+ lines = ["# 批测报告\n"]
256
+
257
+ lines.append("## KPI 总览\n")
258
+ lines.append("| 总会话 | 总消息数 | 回复消息数 | 回复率 |")
259
+ lines.append("|--------|---------|-----------|--------|")
260
+ lines.append(f"| {summary.get('total_conversations',0)} "
261
+ f"| {summary.get('total_messages',0)} "
262
+ f"| {summary.get('total_replied',0)} "
263
+ f"| {_pct(summary.get('reply_rate'))} |\n")
264
+
265
+ lines.append("## Agent Performance\n")
266
+ lines.append("| Action | 会话数 | 消息数 | 回复率 | R1 | R2 | R3 | Rs |")
267
+ lines.append("|--------|--------|--------|--------|----|----|----|----|")
268
+ for a in summary.get('agents', []):
269
+ lines.append(f"| {a.get('name','')} | {a.get('total_conversations',0)} "
270
+ f"| {a.get('total_messages',0)} | {_pct(a.get('reply_rate'))} "
271
+ f"| {_pct(a.get('r1_reply_rate'))} | {_pct(a.get('r2_reply_rate'))} "
272
+ f"| {_pct(a.get('r3_reply_rate'))} | {_pct(a.get('rs_reply_rate'))} |")
273
+ lines.append("")
274
+
275
+ lines.append("## Handoff 分布\n")
276
+ for h in summary.get('handoff', []):
277
+ lines.append(f"- {h['type']}: {h['count']} 条 ({_pct(h.get('rate'))})")
278
+ lines.append("")
279
+
280
+ lines.append("## Error 分布\n")
281
+ for e in summary.get('errors', []):
282
+ lines.append(f"- {e['type']}: {e['count']} 条 ({_pct(e.get('rate'))})")
283
+ lines.append("")
284
+
285
+ lines.append("## 回复详情\n")
286
+ lines.append("| Chat ID | Round | 意图 | Action | Question | Answer | Judge | Correct |")
287
+ lines.append("|---------|-------|------|-------|----------|--------|-------|--------|")
288
+ for d in details:
289
+ q = d.get('question', '').replace('|', '|')
290
+ a = d.get('answer', '').replace('|', '|').replace('\n', ' ')
291
+ lines.append(f"| {d.get('new_chat_id','')} | {d.get('round','')} "
292
+ f"| {d.get('intent','')} | {d.get('action','')} "
293
+ f"| {q} | {a} | {d.get('judge','')} | {d.get('correct','')} |")
294
+
295
+ with open(output_path, 'w', encoding='utf-8') as f:
296
+ f.write('\n'.join(lines) + '\n')
297
+
298
+
299
+ def main():
300
+ parser = argparse.ArgumentParser(description="Delivery 批测报告生成器")
301
+ parser.add_argument("--results", required=True,
302
+ help="batch_test.py 输出的 results JSON 路径")
303
+ parser.add_argument("--html", default=None,
304
+ help="HTML 输出路径,默认 <results基名>.html")
305
+ parser.add_argument("--md", default=None,
306
+ help="MD 输出路径,默认 <results基名>.md")
307
+ parser.add_argument("--no-html", action="store_true", help="跳过 HTML 渲染")
308
+ parser.add_argument("--no-md", action="store_true", help="跳过 MD 渲染")
309
+ args = parser.parse_args()
310
+
311
+ if not os.path.exists(args.results):
312
+ print(f"results file not found: {args.results}", file=sys.stderr)
313
+ sys.exit(1)
314
+
315
+ with open(args.results, "r", encoding="utf-8") as f:
316
+ data = json.load(f)
317
+ summary = data.get("summary", {})
318
+ details = data.get("details", [])
319
+
320
+ base = os.path.splitext(os.path.abspath(args.results))[0]
321
+ html_path = args.html or (base + ".html")
322
+ md_path = args.md or (base + ".md")
323
+
324
+ if not args.no_html:
325
+ os.makedirs(os.path.dirname(os.path.abspath(html_path)) or ".", exist_ok=True)
326
+ generate_html_report(summary, details, html_path)
327
+ print(f"Report rendered → {html_path}", file=sys.stderr)
328
+ if not args.no_md:
329
+ os.makedirs(os.path.dirname(os.path.abspath(md_path)) or ".", exist_ok=True)
330
+ generate_md_report(summary, details, md_path)
331
+ print(f"Report rendered → {md_path}", file=sys.stderr)
332
+
333
+
334
+ if __name__ == "__main__":
335
+ main()
@@ -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()