@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,335 +0,0 @@
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()
@@ -1,234 +0,0 @@
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()