@shulex/cxclawctl 0.1.2 → 0.1.3

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/dist/cli.js +2202 -127
  3. package/dist/cli.js.map +1 -1
  4. package/package.json +19 -6
  5. package/capabilities.lock.json +0 -898
  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,83 +0,0 @@
1
- # Tars 工单标签
2
-
3
- 本模块操作 Shulex Desk / Tars 工单标签,不是 `/api_v2/gpt/label/project/...` 下的 GPT 标签树。所有命令使用受控脚本:
4
-
5
- ```bash
6
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py <subcommand>
7
- ```
8
-
9
- ## 查询
10
-
11
- 完整标签树:
12
-
13
- ```bash
14
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py list-tags
15
- ```
16
-
17
- 查询单个工单的人工标签和 AI 标签:
18
-
19
- ```bash
20
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py get-ticket-tags \
21
- --ticket-number "<对外工单号>"
22
- ```
23
-
24
- 响应中的 `userTags` 是人工标签,`aiTags` 是只读 AI 标签。
25
-
26
- 批量查询使用工单内部整数 ID,不接受对外工单号。先把参数写入 `/workspace/state/solvea-desk/ticket_ids.json`:
27
-
28
- ```json
29
- {"ticketIdList": [101, 102]}
30
- ```
31
-
32
- ```bash
33
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py batch-get-ticket-tags \
34
- --ticket-ids-file /workspace/state/solvea-desk/ticket_ids.json
35
- ```
36
-
37
- 按节点名称搜索标签路径:
38
-
39
- ```bash
40
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py search-tags \
41
- --name "物流延迟"
42
- ```
43
-
44
- 搜索结果是路径数组,节点名称字段为 `nodeName`。标签写操作使用搜索结果中的准确 ID;同名或相近路径必须让用户选择,不能自动取第一项。
45
-
46
- ## 写操作边界
47
-
48
- 写操作只接受 Tars 标签查询返回的准确 ID 和名称。把参数写入 `/workspace/state/solvea-desk/tags.json`:
49
-
50
- ```json
51
- {
52
- "tags": [
53
- {"id": 123, "name": "物流延迟"}
54
- ]
55
- }
56
- ```
57
-
58
- 必须依次执行:
59
-
60
- 1. 使用 `fetch_tickets.py` 或准确工单号确认目标工单。
61
- 2. 使用 `get-ticket-tags` 读取当前 `userTags` 和 `aiTags`。
62
- 3. 使用 `list-tags` 或 `search-tags` 获得标签 ID 和名称。
63
- 4. 不带 `--confirm` 运行写命令,取得包含 `before` 和 `requested` 的预览;此时脚本只读取,不发送 POST。
64
- 5. 向用户展示工单号、绑定或解绑、标签名称和 ID,并等待明确确认。
65
- 6. 用户确认后才增加 `--confirm`。脚本会写入后重新读取标签,并输出 `before`、`requested`、`after`、`server_result` 和 `verified`。
66
-
67
- 绑定:
68
-
69
- ```bash
70
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py bind-tags \
71
- --ticket-number "<对外工单号>" \
72
- --tags-file /workspace/state/solvea-desk/tags.json
73
-
74
- # 用户明确确认后
75
- npx --yes @shulex/cxclawctl@latest script skills/solvea-desk/scripts/tickets/ticket_tags.py bind-tags \
76
- --ticket-number "<对外工单号>" \
77
- --tags-file /workspace/state/solvea-desk/tags.json \
78
- --confirm
79
- ```
80
-
81
- 解绑使用相同参数,将子命令换成 `unbind-tags`。
82
-
83
- 脚本只通过 `after.userTags` 核验结果;`aiTags` 永远只读。写接口返回成功但后置核验失败时,命令非零退出并保留前后证据,不得向用户宣称成功。
@@ -1,580 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Fetch authorized report data into a local XLSX snapshot.
3
-
4
- The script calls exactly one ticket-system data-source endpoint. That endpoint is
5
- responsible for account isolation and for deciding which datasets to return.
6
- """
7
-
8
- import os
9
- import sys
10
- sys.path.insert(0, os.path.join(os.environ["SOLVEA_LIB_DIR"], "skill-runtime"))
11
- from bootstrap import ( # noqa: E402
12
- OUTPUTS_DIR,
13
- SOLVEA_ENV,
14
- SOLVEA_PROJECT_ID,
15
- SOLVEA_TENANT_ID,
16
- STATE_DIR,
17
- TMP_DIR,
18
- timestamp,
19
- )
20
-
21
- import argparse
22
- import json
23
- from dataclasses import dataclass
24
- from datetime import datetime, time
25
- from pathlib import Path
26
- from typing import Any
27
- from urllib.parse import urlsplit
28
- from zoneinfo import ZoneInfo
29
-
30
- import httpx
31
- import pandas as pd
32
-
33
-
34
- SOURCE_CONTRACT = "single_time_range_paginated_api"
35
- SOURCE_TYPE = "ticket_report_snapshot"
36
- DEFAULT_TIME_RANGE_TYPE = "report_activity_date"
37
- CREATED_AT = "created_at"
38
- RAW_REPORT_SOURCE_PATH = "/api_v2/tars/api/ticket/report/raw/page"
39
- DEFAULT_DESK_STAGING_URL = "https://desk-staging.shulex.com"
40
- DEFAULT_DESK_PROD_URL = "https://desk.shulex.com"
41
- DEFAULT_PAGE_SIZE = 200
42
- MAX_PAGE_SIZE = 200
43
- DEFAULT_MAX_PAGES = 200
44
- DEFAULT_MAX_ROWS = 200000
45
- DEFAULT_USER_TIMEZONE = "Asia/Shanghai"
46
- RESERVED_DATA_KEYS = {
47
- "hasMore",
48
- "has_more",
49
- "nextPage",
50
- "next_page",
51
- "nextCursor",
52
- "next_cursor",
53
- "pageNo",
54
- "page_no",
55
- "pageSize",
56
- "page_size",
57
- "schema",
58
- "total",
59
- }
60
-
61
-
62
- @dataclass(frozen=True)
63
- class SourceConfig:
64
- endpoint: str
65
- token: str
66
- cookie_name: str
67
-
68
-
69
- @dataclass(frozen=True)
70
- class TimeWindow:
71
- requested: dict[str, str]
72
- fetch_window: dict[str, str]
73
-
74
-
75
- def _now_iso() -> str:
76
- return datetime.now().astimezone().isoformat(timespec="seconds")
77
-
78
-
79
- def _base_origin(endpoint: str) -> str:
80
- parsed = urlsplit(endpoint)
81
- if not parsed.scheme or not parsed.netloc:
82
- return ""
83
- return f"{parsed.scheme}://{parsed.netloc}"
84
-
85
-
86
- def resolve_source_config() -> SourceConfig:
87
- """Resolve the Desk/Tars raw report source endpoint for the current environment."""
88
- env = os.environ.get("SOLVEA_ENV", SOLVEA_ENV) or "prod"
89
- # 鉴权沿用 SLG runtime 注入的 Solvea token;这里不新增 solvea-desk 专属长期 token。
90
- if env == "prod":
91
- token = os.environ.get("SOLVEA_PROD_TOKEN", "") or os.environ.get("SOLVEA_TOKEN", "")
92
- base_url = os.environ.get("DESK_REPORT_PROD_BASE_URL", DEFAULT_DESK_PROD_URL)
93
- cookie_name = "_token"
94
- else:
95
- token = os.environ.get("SOLVEA_TOKEN", "")
96
- base_url = os.environ.get("DESK_REPORT_STAGING_BASE_URL", DEFAULT_DESK_STAGING_URL)
97
- cookie_name = "_token_staging"
98
-
99
- explicit_endpoint = os.environ.get("DESK_REPORT_SOURCE_URL", "").strip()
100
- endpoint = explicit_endpoint or f"{base_url.rstrip('/')}{RAW_REPORT_SOURCE_PATH}"
101
- return SourceConfig(endpoint=endpoint, token=token, cookie_name=cookie_name)
102
-
103
-
104
- def _parse_time(value: str, timezone_name: str) -> datetime:
105
- raw = value.strip()
106
- if not raw:
107
- raise ValueError("time value cannot be empty")
108
- normalized = raw[:-1] + "+00:00" if raw.endswith("Z") else raw
109
- dt = datetime.fromisoformat(normalized)
110
- if dt.tzinfo is None:
111
- return dt.replace(tzinfo=ZoneInfo(timezone_name))
112
- return dt
113
-
114
-
115
- def normalize_time_window(start: str, end: str, *, timezone_name: str = DEFAULT_USER_TIMEZONE) -> TimeWindow:
116
- """Normalize a full-calendar-day business window for the date-based Tars API."""
117
- if timezone_name != DEFAULT_USER_TIMEZONE:
118
- raise ValueError(f"solvea-desk only supports timezone {DEFAULT_USER_TIMEZONE}")
119
- business_timezone = ZoneInfo(timezone_name)
120
- start_dt = _parse_time(start, timezone_name).astimezone(business_timezone)
121
- end_dt = _parse_time(end, timezone_name).astimezone(business_timezone)
122
- if end_dt < start_dt:
123
- raise ValueError("end time must be greater than or equal to start time")
124
- if start_dt.time() != time.min or end_dt.time() != time.min:
125
- raise ValueError(
126
- "solvea-desk only supports full calendar days: start and end must both be 00:00:00"
127
- )
128
-
129
- return TimeWindow(
130
- requested={
131
- "start": start,
132
- "end": end,
133
- "timezone": timezone_name,
134
- },
135
- fetch_window={
136
- "start": start_dt.replace(tzinfo=None).isoformat(timespec="seconds"),
137
- "end": end_dt.replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=None).isoformat(
138
- timespec="seconds"
139
- ),
140
- },
141
- )
142
-
143
-
144
- def validate_page_size(page_size: str | int) -> int:
145
- try:
146
- value = int(page_size)
147
- except (TypeError, ValueError) as exc:
148
- raise argparse.ArgumentTypeError("page-size must be an integer") from exc
149
- if value < 1 or value > MAX_PAGE_SIZE:
150
- raise argparse.ArgumentTypeError(f"page-size must be between 1 and {MAX_PAGE_SIZE}")
151
- return value
152
-
153
-
154
- def _write_json(path: Path, payload: dict[str, Any]) -> None:
155
- path.parent.mkdir(parents=True, exist_ok=True)
156
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
157
-
158
-
159
- def _fixed_files() -> dict[str, dict[str, str]]:
160
- return {
161
- "state": {
162
- "datasource": "datasource.xlsx",
163
- "manifest": "manifest.json",
164
- "analysis": "analysis.py",
165
- "report_data": "report_data.json",
166
- "calculation_notes": "calculation_notes.json",
167
- },
168
- "outputs": {
169
- "report_pattern": "<report_slug>.html",
170
- "detail_pattern": "<report_slug>_detail.xlsx",
171
- },
172
- }
173
-
174
-
175
- def _write_latest_state(
176
- state_task_dir: Path,
177
- status: str,
178
- *,
179
- requested_window: dict[str, str],
180
- fetch_window: dict[str, str],
181
- time_range_type: str = DEFAULT_TIME_RANGE_TYPE,
182
- tmp_task_dir: Path | None = None,
183
- ) -> None:
184
- output_task_dir = OUTPUTS_DIR / state_task_dir.name
185
- latest = {
186
- "skill": "solvea-desk",
187
- "source_type": SOURCE_TYPE,
188
- "status": status,
189
- "task_id": state_task_dir.name,
190
- "task_dir": str(state_task_dir),
191
- "state_task_dir": str(state_task_dir),
192
- "output_task_dir": str(output_task_dir),
193
- "tmp_task_dir": str(tmp_task_dir or (TMP_DIR / state_task_dir.name)),
194
- "requested_window": requested_window,
195
- "fetch_window": fetch_window,
196
- "time_range_type": time_range_type,
197
- "updated_at": _now_iso(),
198
- }
199
- _write_json(STATE_DIR / "latest_task.json", latest)
200
-
201
-
202
- def write_pending_snapshot(
203
- state_task_dir: Path,
204
- *,
205
- start: str,
206
- end: str,
207
- reason: str,
208
- requested_window: dict[str, str] | None = None,
209
- time_range_type: str = DEFAULT_TIME_RANGE_TYPE,
210
- ) -> dict[str, Any]:
211
- state_task_dir.mkdir(parents=True, exist_ok=True)
212
- output_task_dir = OUTPUTS_DIR / state_task_dir.name
213
- tmp_task_dir = TMP_DIR / state_task_dir.name
214
- fetch_window = {"start": start, "end": end}
215
- requested = requested_window or {"start": start, "end": end, "timezone": DEFAULT_USER_TIMEZONE}
216
- manifest = {
217
- "status": "pending_runtime_config",
218
- "source_type": SOURCE_TYPE,
219
- "source_contract": SOURCE_CONTRACT,
220
- "task_id": state_task_dir.name,
221
- "env": SOLVEA_ENV,
222
- "tenant_id": SOLVEA_TENANT_ID,
223
- "project_id": SOLVEA_PROJECT_ID,
224
- "requested_window": requested,
225
- "fetch_window": fetch_window,
226
- "analysis_window": requested,
227
- "time_range_type": time_range_type,
228
- "row_counts": {},
229
- "truncated": False,
230
- "files": _fixed_files(),
231
- "message": "Shulex Desk runtime is not configured yet.",
232
- "reason": reason,
233
- "generated_at": _now_iso(),
234
- }
235
- _write_json(state_task_dir / "manifest.json", manifest)
236
- _write_latest_state(
237
- state_task_dir,
238
- "pending_runtime_config",
239
- requested_window=requested,
240
- fetch_window=fetch_window,
241
- time_range_type=time_range_type,
242
- tmp_task_dir=tmp_task_dir,
243
- )
244
- return {
245
- "ok": False,
246
- "source_type": SOURCE_TYPE,
247
- "task_id": state_task_dir.name,
248
- "task_dir": str(state_task_dir),
249
- "state_task_dir": str(state_task_dir),
250
- "output_task_dir": str(output_task_dir),
251
- "tmp_task_dir": str(tmp_task_dir),
252
- "manifest": str(state_task_dir / "manifest.json"),
253
- "error": "desk_report_runtime_not_configured",
254
- "message": "Check SOLVEA_ENV and the corresponding SOLVEA token environment variable.",
255
- }
256
-
257
-
258
- def _unwrap_payload(payload: Any) -> Any:
259
- if isinstance(payload, dict):
260
- for key in ("data", "result"):
261
- value = payload.get(key)
262
- if isinstance(value, (dict, list)):
263
- return value
264
- return payload
265
-
266
-
267
- def _records_from_value(value: Any) -> list[dict[str, Any]]:
268
- if not isinstance(value, list):
269
- return []
270
- records = []
271
- for item in value:
272
- if isinstance(item, dict):
273
- records.append(item)
274
- else:
275
- records.append({"value": item})
276
- return records
277
-
278
-
279
- def extract_datasets(payload: Any) -> dict[str, list[dict[str, Any]]]:
280
- """Extract generic dataset sheets from one endpoint page payload."""
281
- # 不假设 Tars 背后有几张表;接口返回几个 list dataset,就落几个 XLSX sheet。
282
- body = _unwrap_payload(payload)
283
- datasets: dict[str, list[dict[str, Any]]] = {}
284
-
285
- if isinstance(body, list):
286
- return {"data": _records_from_value(body)}
287
-
288
- if not isinstance(body, dict):
289
- return {"data": [{"value": body}]}
290
-
291
- for key in ("records", "list", "items", "rows"):
292
- if key in body:
293
- records = _records_from_value(body.get(key))
294
- if records:
295
- return {"data": records}
296
-
297
- for key, value in body.items():
298
- if key in RESERVED_DATA_KEYS:
299
- continue
300
- records = _records_from_value(value)
301
- if records:
302
- datasets[key] = records
303
-
304
- if datasets:
305
- return datasets
306
- return {"data": []}
307
-
308
-
309
- def _has_more(payload: Any, page_no: int) -> bool:
310
- body = _unwrap_payload(payload)
311
- candidates = [payload, body] if isinstance(payload, dict) else [body]
312
- for item in candidates:
313
- if not isinstance(item, dict):
314
- continue
315
- for key in ("hasMore", "has_more"):
316
- if key in item:
317
- return bool(item[key])
318
- total = item.get("total")
319
- page_size = item.get("pageSize") or item.get("page_size")
320
- if isinstance(total, int) and isinstance(page_size, int):
321
- return page_no * page_size < total
322
- return False
323
-
324
-
325
- def _normalize_cell(value: Any) -> Any:
326
- if isinstance(value, (dict, list)):
327
- return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
328
- return value
329
-
330
-
331
- def _sheet_name(raw: str, used: set[str]) -> str:
332
- name = "".join(ch if ch not in r'[]:*?/\\' else "_" for ch in raw).strip() or "data"
333
- name = name[:31]
334
- candidate = name
335
- idx = 2
336
- while candidate in used:
337
- suffix = f"_{idx}"
338
- candidate = f"{name[:31 - len(suffix)]}{suffix}"
339
- idx += 1
340
- used.add(candidate)
341
- return candidate
342
-
343
-
344
- def _write_xlsx(path: Path, datasets: dict[str, list[dict[str, Any]]]) -> None:
345
- path.parent.mkdir(parents=True, exist_ok=True)
346
- used_sheet_names: set[str] = set()
347
- with pd.ExcelWriter(path, engine="openpyxl") as writer:
348
- for dataset_name in sorted(datasets):
349
- records = datasets[dataset_name]
350
- rows = [{key: _normalize_cell(value) for key, value in row.items()} for row in records]
351
- df = pd.DataFrame(rows)
352
- if df.empty:
353
- df = pd.DataFrame([{"_empty": ""}])
354
- df.to_excel(writer, sheet_name=_sheet_name(dataset_name, used_sheet_names), index=False)
355
-
356
-
357
- def write_snapshot_from_pages(
358
- state_task_dir: Path,
359
- tmp_task_dir: Path,
360
- pages: list[dict[str, Any]],
361
- *,
362
- start: str,
363
- end: str,
364
- endpoint: str,
365
- page_size: int,
366
- truncated: bool,
367
- requested_window: dict[str, str] | None = None,
368
- time_range_type: str = DEFAULT_TIME_RANGE_TYPE,
369
- ) -> dict[str, Any]:
370
- state_task_dir.mkdir(parents=True, exist_ok=True)
371
- tmp_task_dir.mkdir(parents=True, exist_ok=True)
372
- output_task_dir = OUTPUTS_DIR / state_task_dir.name
373
-
374
- datasets: dict[str, list[dict[str, Any]]] = {}
375
- for idx, page in enumerate(pages, start=1):
376
- _write_json(tmp_task_dir / f"raw_page_{idx:03d}.json", page)
377
- page_datasets = extract_datasets(page)
378
- for name, records in page_datasets.items():
379
- datasets.setdefault(name, []).extend(records)
380
-
381
- if not datasets:
382
- datasets = {"data": []}
383
-
384
- row_counts = {name: len(records) for name, records in sorted(datasets.items())}
385
- fetch_window = {"start": start, "end": end}
386
- requested = requested_window or {"start": start, "end": end, "timezone": DEFAULT_USER_TIMEZONE}
387
- _write_xlsx(state_task_dir / "datasource.xlsx", datasets)
388
- manifest = {
389
- "status": "ready",
390
- "source_type": SOURCE_TYPE,
391
- "source_contract": SOURCE_CONTRACT,
392
- "task_id": state_task_dir.name,
393
- "env": SOLVEA_ENV,
394
- "tenant_id": SOLVEA_TENANT_ID,
395
- "project_id": SOLVEA_PROJECT_ID,
396
- "requested_window": requested,
397
- "fetch_window": fetch_window,
398
- "analysis_window": requested,
399
- "time_range_type": time_range_type,
400
- "endpoint": endpoint,
401
- "page_size": page_size,
402
- "page_count": len(pages),
403
- "row_counts": row_counts,
404
- "truncated": truncated,
405
- "files": _fixed_files(),
406
- "tmp_raw_pages": str(tmp_task_dir),
407
- "generated_at": _now_iso(),
408
- }
409
- _write_json(state_task_dir / "manifest.json", manifest)
410
- _write_latest_state(
411
- state_task_dir,
412
- "ready",
413
- requested_window=requested,
414
- fetch_window=fetch_window,
415
- time_range_type=time_range_type,
416
- tmp_task_dir=tmp_task_dir,
417
- )
418
- return {
419
- "ok": True,
420
- "source_type": SOURCE_TYPE,
421
- "task_id": state_task_dir.name,
422
- "task_dir": str(state_task_dir),
423
- "state_task_dir": str(state_task_dir),
424
- "output_task_dir": str(output_task_dir),
425
- "tmp_task_dir": str(tmp_task_dir),
426
- "datasource": str(state_task_dir / "datasource.xlsx"),
427
- "manifest": str(state_task_dir / "manifest.json"),
428
- "row_counts": row_counts,
429
- "truncated": truncated,
430
- }
431
-
432
-
433
- def _request_page(
434
- client: httpx.Client,
435
- endpoint: str,
436
- token: str,
437
- cookie_name: str,
438
- start: str,
439
- end: str,
440
- page: int,
441
- page_size: int,
442
- time_range_type: str = "",
443
- ) -> dict[str, Any]:
444
- origin = _base_origin(endpoint)
445
- headers = {
446
- "Accept": "application/json, text/plain, */*",
447
- "Origin": origin,
448
- "Referer": f"{origin}/botdesk",
449
- }
450
- if token:
451
- headers["Cookie"] = f"{cookie_name}={token}"
452
- request_payload = {
453
- "startTime": start,
454
- "endTime": end,
455
- "page": page,
456
- "pageSize": page_size,
457
- }
458
- if time_range_type:
459
- request_payload["timeRangeType"] = time_range_type
460
- response = client.post(endpoint, json=request_payload, headers=headers)
461
- response.raise_for_status()
462
- payload = response.json()
463
- if not isinstance(payload, dict):
464
- raise ValueError("Shulex Desk source endpoint must return a JSON object")
465
- return payload
466
-
467
-
468
- def fetch_pages(
469
- config: SourceConfig,
470
- *,
471
- start: str,
472
- end: str,
473
- page_size: int,
474
- max_pages: int,
475
- max_rows: int,
476
- time_range_type: str = "",
477
- ) -> tuple[list[dict[str, Any]], bool]:
478
- page_size = validate_page_size(page_size)
479
- pages: list[dict[str, Any]] = []
480
- total_rows = 0
481
- truncated = False
482
- with httpx.Client(timeout=60.0) as client:
483
- for page_no in range(1, max_pages + 1):
484
- payload = _request_page(
485
- client,
486
- config.endpoint,
487
- config.token,
488
- config.cookie_name,
489
- start,
490
- end,
491
- page_no,
492
- page_size,
493
- time_range_type,
494
- )
495
- pages.append(payload)
496
- page_datasets = extract_datasets(payload)
497
- total_rows += sum(len(records) for records in page_datasets.values())
498
- if total_rows >= max_rows:
499
- truncated = True
500
- break
501
- if not _has_more(payload, page_no):
502
- break
503
- else:
504
- truncated = True
505
- return pages, truncated
506
-
507
-
508
- def main() -> int:
509
- parser = argparse.ArgumentParser(description="Fetch solvea-desk data sources into datasource.xlsx")
510
- parser.add_argument("--start", required=True, help="Inclusive start time, ISO-8601 preferred")
511
- parser.add_argument("--end", required=True, help="Inclusive end date at 00:00:00, ISO-8601 preferred")
512
- parser.add_argument(
513
- "--timezone",
514
- default=DEFAULT_USER_TIMEZONE,
515
- choices=[DEFAULT_USER_TIMEZONE],
516
- help="Business timezone; solvea-desk currently only supports Asia/Shanghai",
517
- )
518
- parser.add_argument(
519
- "--time-range-type",
520
- default="",
521
- choices=[CREATED_AT],
522
- help="Only set to created_at when the user explicitly requests ticket creation-date filtering",
523
- )
524
- parser.add_argument("--task-id", default="", help="Optional task id; defaults to timestamp")
525
- parser.add_argument(
526
- "--page-size",
527
- type=validate_page_size,
528
- default=DEFAULT_PAGE_SIZE,
529
- help=f"Rows per page, between 1 and {MAX_PAGE_SIZE}",
530
- )
531
- parser.add_argument("--max-pages", type=int, default=DEFAULT_MAX_PAGES)
532
- parser.add_argument("--max-rows", type=int, default=DEFAULT_MAX_ROWS)
533
- args = parser.parse_args()
534
-
535
- task_id = args.task_id.strip() or timestamp()
536
- state_task_dir = STATE_DIR / task_id
537
- tmp_task_dir = TMP_DIR / task_id
538
-
539
- config = resolve_source_config()
540
- time_window = normalize_time_window(args.start, args.end, timezone_name=args.timezone)
541
- effective_time_range_type = args.time_range_type or DEFAULT_TIME_RANGE_TYPE
542
- if not config.token:
543
- result = write_pending_snapshot(
544
- state_task_dir,
545
- start=time_window.fetch_window["start"],
546
- end=time_window.fetch_window["end"],
547
- reason="missing Solvea token for solvea-desk source",
548
- requested_window=time_window.requested,
549
- time_range_type=effective_time_range_type,
550
- )
551
- print(json.dumps(result, ensure_ascii=False, indent=2))
552
- return 2
553
-
554
- pages, truncated = fetch_pages(
555
- config,
556
- start=time_window.fetch_window["start"],
557
- end=time_window.fetch_window["end"],
558
- page_size=args.page_size,
559
- max_pages=args.max_pages,
560
- max_rows=args.max_rows,
561
- time_range_type=args.time_range_type,
562
- )
563
- result = write_snapshot_from_pages(
564
- state_task_dir,
565
- tmp_task_dir,
566
- pages,
567
- start=time_window.fetch_window["start"],
568
- end=time_window.fetch_window["end"],
569
- endpoint=config.endpoint,
570
- page_size=args.page_size,
571
- truncated=truncated,
572
- requested_window=time_window.requested,
573
- time_range_type=effective_time_range_type,
574
- )
575
- print(json.dumps(result, ensure_ascii=False, indent=2))
576
- return 0 if not truncated else 3
577
-
578
-
579
- if __name__ == "__main__":
580
- raise SystemExit(main())