@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.
- package/README.md +25 -22
- package/capabilities.lock.json +535 -131
- package/dist/cli.js +2203 -127
- package/dist/cli.js.map +1 -1
- package/package.json +20 -6
- package/requirements-cli.txt +0 -2
- package/skills/__lib__/__init__.py +0 -0
- package/skills/__lib__/skill-runtime/bootstrap.py +0 -134
- package/skills/__lib__/solvea-api/__init__.py +0 -0
- package/skills/__lib__/solvea-api/client.py +0 -1944
- package/skills/__lib__/solvea-api/dashboard.py +0 -100
- package/skills/__lib__/solvea-api/solvea_api.py +0 -1341
- package/skills/agent-analytics/SKILL.md +0 -74
- package/skills/agent-analytics/references/chat-report-fields.md +0 -36
- package/skills/agent-analytics/references/report-fields.md +0 -130
- package/skills/agent-analytics/references/transcript.md +0 -110
- package/skills/agent-builder/SKILL.md +0 -42
- package/skills/agent-builder/interfaces/fallback-chatflow.md +0 -47
- package/skills/agent-builder/references/chatflow/graph-spec.md +0 -259
- package/skills/agent-builder/references/chatflow/node-selection.md +0 -89
- package/skills/agent-builder/references/chatflow/nodes/agent.md +0 -350
- package/skills/agent-builder/references/chatflow/nodes/business-track.md +0 -43
- package/skills/agent-builder/references/chatflow/nodes/code.md +0 -157
- package/skills/agent-builder/references/chatflow/nodes/collect-order.md +0 -47
- package/skills/agent-builder/references/chatflow/nodes/finish.md +0 -34
- package/skills/agent-builder/references/chatflow/nodes/human-review.md +0 -60
- package/skills/agent-builder/references/chatflow/nodes/if-else.md +0 -100
- package/skills/agent-builder/references/chatflow/nodes/intent.md +0 -79
- package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +0 -43
- package/skills/agent-builder/references/chatflow/nodes/llm.md +0 -117
- package/skills/agent-builder/references/chatflow/nodes/note.md +0 -38
- package/skills/agent-builder/references/chatflow/nodes/question.md +0 -97
- package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +0 -68
- package/skills/agent-builder/references/chatflow/nodes/reply.md +0 -97
- package/skills/agent-builder/references/chatflow/nodes/start.md +0 -35
- package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +0 -48
- package/skills/agent-builder/references/chatflow/nodes/tool.md +0 -138
- package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +0 -63
- package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +0 -52
- package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +0 -50
- package/skills/agent-builder/references/chatflow/nodes/variable-process.md +0 -70
- package/skills/agent-builder/references/chatflow/overview.md +0 -53
- package/skills/agent-builder/references/chatflow/visual-translation.md +0 -64
- package/skills/agent-builder/references/chatflow/workflow.md +0 -134
- package/skills/agent-builder/references/multi-agent/create-update.md +0 -185
- package/skills/agent-builder/references/multi-agent/graph-spec.md +0 -246
- package/skills/agent-builder/references/multi-agent/graph-structure.md +0 -12
- package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +0 -13
- package/skills/agent-builder/references/multi-agent/migration/large.md +0 -554
- package/skills/agent-builder/references/multi-agent/migration/principles.md +0 -446
- package/skills/agent-builder/references/multi-agent/migration/small.md +0 -225
- package/skills/agent-builder/references/multi-agent/orchestration.md +0 -11
- package/skills/agent-builder/references/multi-agent/overview.md +0 -48
- package/skills/agent-builder/references/multi-agent/update.md +0 -128
- package/skills/agent-builder/references/multi-agent/workflow.md +0 -87
- package/skills/agent-builder/references/single-agent/overview.md +0 -48
- package/skills/agent-builder/references/single-agent/prompt.md +0 -16
- package/skills/agent-builder/references/single-agent/tools.md +0 -36
- package/skills/agent-builder/references/single-agent/voice.md +0 -46
- package/skills/agent-builder/references/tools/http-tool.md +0 -315
- package/skills/agent-builder/references/tools/mock-tool.md +0 -279
- package/skills/agent-builder/references/tools/overview.md +0 -46
- package/skills/agent-builder/references/tools/tool-config.md +0 -43
- package/skills/agent-builder/references/tools/x-token.md +0 -34
- package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +0 -3458
- package/skills/agent-builder/scripts/chatflow/validate_graph.py +0 -1697
- package/skills/agent-builder/scripts/copy_marketplace_agent.py +0 -61
- package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +0 -5974
- package/skills/agent-builder/scripts/multi-agent/replay_compare.py +0 -679
- package/skills/agent-builder/scripts/patch_agent_tools.py +0 -135
- package/skills/agent-builder/scripts/tools/validate_tool_config.py +0 -384
- package/skills/agent-tester/SKILL.md +0 -35
- package/skills/agent-tester/batch-test/overview.md +0 -86
- package/skills/agent-tester/scripts/batch_test_sheet.py +0 -213
- package/skills/agent-tester/single-test/badcase-retest.md +0 -85
- package/skills/agent-tester/single-test/direct-test.md +0 -24
- package/skills/agent-tester/single-test/overview.md +0 -142
- package/skills/delivery/SKILL.md +0 -112
- package/skills/delivery/interfaces/classify.md +0 -38
- package/skills/delivery/interfaces/create-agents.md +0 -89
- package/skills/delivery/interfaces/delivery-test.md +0 -67
- package/skills/delivery/references/flow.md +0 -40
- package/skills/delivery/references/validation.md +0 -15
- package/skills/delivery/scripts/batch_test.py +0 -704
- package/skills/delivery/scripts/classify.py +0 -22
- package/skills/delivery/scripts/create_agents.py +0 -695
- package/skills/delivery/scripts/gen_delivery_report.py +0 -335
- package/skills/delivery/scripts/gen_excel.py +0 -234
- package/skills/delivery/scripts/gen_report.py +0 -208
- package/skills/delivery/scripts/plan.py +0 -163
- package/skills/delivery/scripts/render.py +0 -713
- package/skills/delivery/scripts/render_summary.py +0 -38
- package/skills/delivery/scripts/sample_history_cases.py +0 -109
- package/skills/kb-builder/SKILL.md +0 -80
- package/skills/kb-builder/interfaces/qa-upload.md +0 -55
- package/skills/kb-builder/interfaces/web-kb.md +0 -84
- package/skills/kb-builder/references/directory.md +0 -373
- package/skills/kb-builder/references/export.md +0 -282
- package/skills/kb-builder/references/isolation-keywords.md +0 -113
- package/skills/kb-builder/references/knowledge.md +0 -153
- package/skills/kb-builder/references/product.md +0 -52
- package/skills/kb-builder/references/qa-from-help-center.md +0 -58
- package/skills/kb-builder/references/qa-from-pdf.md +0 -94
- package/skills/kb-builder/references/qa-from-tickets.md +0 -168
- package/skills/kb-builder/references/qa-from-web.md +0 -98
- package/skills/kb-builder/references/qa-quality.md +0 -73
- package/skills/kb-builder/references/reply-template.md +0 -237
- package/skills/kb-builder/references/tag-isolation-migration.md +0 -61
- package/skills/kb-builder/references/tags.md +0 -45
- package/skills/kb-builder/references/upload.md +0 -130
- package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +0 -333
- package/skills/kb-builder/scripts/build_directory_plan.py +0 -144
- package/skills/kb-builder/scripts/crawl_site.py +0 -433
- package/skills/kb-builder/scripts/create_dirs.py +0 -146
- package/skills/kb-builder/scripts/export_knowledge.py +0 -454
- package/skills/kb-builder/scripts/extract_ticket_qa.py +0 -441
- package/skills/kb-builder/scripts/extract_web_qa.py +0 -118
- package/skills/kb-builder/scripts/import_qa.py +0 -219
- package/skills/kb-builder/scripts/import_source_documents.py +0 -143
- package/skills/kb-builder/scripts/import_templates.py +0 -409
- package/skills/poc/SKILL.md +0 -125
- package/skills/poc/assets/default_manual.css +0 -279
- package/skills/poc/assets/notice_escalate.png +0 -0
- package/skills/poc/assets/notice_escalate2.png +0 -0
- package/skills/poc/assets/notice_new_chat.png +0 -0
- package/skills/poc/assets/poc_test_template.html +0 -645
- package/skills/poc/assets/poc_test_template.md +0 -104
- package/skills/poc/assets/step3_preview.png +0 -0
- package/skills/poc/assets/step4_order_note.png +0 -0
- package/skills/poc/interfaces/classify.md +0 -38
- package/skills/poc/interfaces/create-agents.md +0 -108
- package/skills/poc/interfaces/poc-test.md +0 -94
- package/skills/poc/references/flow.md +0 -47
- package/skills/poc/references/kb-directory.md +0 -22
- package/skills/poc/references/validation.md +0 -17
- package/skills/poc/scripts/classify.py +0 -22
- package/skills/poc/scripts/create_agents.py +0 -818
- package/skills/poc/scripts/gen_excel.py +0 -234
- package/skills/poc/scripts/gen_poc_manual.py +0 -249
- package/skills/poc/scripts/gen_report.py +0 -208
- package/skills/poc/scripts/inspect_chatflow.py +0 -209
- package/skills/poc/scripts/plan.py +0 -178
- package/skills/poc/scripts/poc_test.py +0 -495
- package/skills/poc/scripts/render.py +0 -713
- package/skills/poc/scripts/render_summary.py +0 -41
- package/skills/runtime-inspector/SKILL.md +0 -51
- package/skills/runtime-inspector/references/frontend-rendering.md +0 -126
- package/skills/runtime-inspector/references/model.md +0 -59
- package/skills/runtime-inspector/references/sandbox.md +0 -131
- package/skills/runtime-inspector/scripts/model_inspect.py +0 -68
- package/skills/runtime-inspector/scripts/prepare.py +0 -98
- package/skills/runtime-inspector/scripts/process.py +0 -84
- package/skills/runtime-inspector/scripts/read_state.py +0 -32
- package/skills/runtime-inspector/scripts/sandbox_inspect.py +0 -463
- package/skills/runtime-inspector/scripts/test_claude_proxy.py +0 -49
- package/skills/runtime-inspector/scripts/test_openai_proxy.py +0 -41
- package/skills/runtime-inspector/scripts/verify_persistence.py +0 -101
- package/skills/runtime-inspector/scripts/write_test.py +0 -41
- package/skills/setup/SKILL.md +0 -17
- package/skills/shulex-support/SKILL.md +0 -39
- package/skills/shulex-support/references/badcase-analysis/overview.md +0 -34
- package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +0 -105
- package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +0 -89
- package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +0 -36
- package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +0 -53
- package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +0 -259
- package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +0 -148
- package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +0 -57
- package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +0 -120
- package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +0 -126
- package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +0 -121
- package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +0 -208
- package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +0 -115
- package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +0 -72
- package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +0 -92
- package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +0 -147
- package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +0 -283
- package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +0 -77
- package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +0 -19
- package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +0 -23
- package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +0 -31
- package/skills/shulex-support/references/product-docs.md +0 -70
- package/skills/shulex-support/references/tickets.md +0 -184
- package/skills/shulex-support/scripts/support.py +0 -427
- package/skills/solvea-desk/SKILL.md +0 -39
- package/skills/solvea-desk/references/report/fields.md +0 -82
- package/skills/solvea-desk/references/report/report-design.md +0 -200
- package/skills/solvea-desk/references/report/report-template.html +0 -44
- package/skills/solvea-desk/references/report/report.css +0 -517
- package/skills/solvea-desk/references/report/workflow.md +0 -464
- package/skills/solvea-desk/references/tickets/query.md +0 -103
- package/skills/solvea-desk/references/tickets/tags.md +0 -83
- package/skills/solvea-desk/scripts/report/fetch_sources.py +0 -580
- package/skills/solvea-desk/scripts/report/render_report.py +0 -763
- package/skills/solvea-desk/scripts/tickets/desk_api.py +0 -73
- package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +0 -403
- package/skills/solvea-desk/scripts/tickets/ticket_tags.py +0 -305
- package/skills/solvea-kit/SKILL.md +0 -84
- package/skills/solvea-kit/references/agent.md +0 -145
- package/skills/solvea-kit/references/chat.md +0 -244
- package/skills/solvea-kit/references/intent.md +0 -69
- package/skills/solvea-kit/references/plugin.md +0 -53
- package/skills/solvea-kit/references/quality-inspection.md +0 -75
- package/skills/solvea-kit/references/strategy.md +0 -62
- package/skills/solvea-kit/references/tagging.md +0 -231
- package/skills/solvea-kit/references/variables.md +0 -53
- package/skills/solvea-kit/scripts/generate_label_import_file.py +0 -151
- 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('<', '<').replace('>', '>')
|
|
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('<', '<').replace('>', '>')
|
|
77
|
-
ng_cls = ' active' if d.get('judge', '') in ('✗', 'ng', 'fail', 'FAIL', '0') else ''
|
|
78
|
-
correct_esc = d.get('correct', '').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
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)'>✗</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()'>⬇ 下载 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()
|