@shulex/cxclawctl 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +228 -2
  2. package/bin/cxclawctl.mjs +11 -0
  3. package/capabilities.lock.json +898 -0
  4. package/dist/cli.js +363 -0
  5. package/dist/cli.js.map +1 -0
  6. package/package.json +33 -4
  7. package/requirements-cli.txt +2 -0
  8. package/skills/__lib__/__init__.py +0 -0
  9. package/skills/__lib__/skill-runtime/bootstrap.py +134 -0
  10. package/skills/__lib__/solvea-api/__init__.py +0 -0
  11. package/skills/__lib__/solvea-api/client.py +1944 -0
  12. package/skills/__lib__/solvea-api/dashboard.py +100 -0
  13. package/skills/__lib__/solvea-api/solvea_api.py +1341 -0
  14. package/skills/agent-analytics/SKILL.md +74 -0
  15. package/skills/agent-analytics/references/chat-report-fields.md +36 -0
  16. package/skills/agent-analytics/references/report-fields.md +130 -0
  17. package/skills/agent-analytics/references/transcript.md +110 -0
  18. package/skills/agent-builder/SKILL.md +42 -0
  19. package/skills/agent-builder/interfaces/fallback-chatflow.md +47 -0
  20. package/skills/agent-builder/references/chatflow/graph-spec.md +259 -0
  21. package/skills/agent-builder/references/chatflow/node-selection.md +89 -0
  22. package/skills/agent-builder/references/chatflow/nodes/agent.md +350 -0
  23. package/skills/agent-builder/references/chatflow/nodes/business-track.md +43 -0
  24. package/skills/agent-builder/references/chatflow/nodes/code.md +157 -0
  25. package/skills/agent-builder/references/chatflow/nodes/collect-order.md +47 -0
  26. package/skills/agent-builder/references/chatflow/nodes/finish.md +34 -0
  27. package/skills/agent-builder/references/chatflow/nodes/human-review.md +60 -0
  28. package/skills/agent-builder/references/chatflow/nodes/if-else.md +100 -0
  29. package/skills/agent-builder/references/chatflow/nodes/intent.md +79 -0
  30. package/skills/agent-builder/references/chatflow/nodes/knowledge-reply.md +43 -0
  31. package/skills/agent-builder/references/chatflow/nodes/llm.md +117 -0
  32. package/skills/agent-builder/references/chatflow/nodes/note.md +38 -0
  33. package/skills/agent-builder/references/chatflow/nodes/question.md +97 -0
  34. package/skills/agent-builder/references/chatflow/nodes/rag-reply.md +68 -0
  35. package/skills/agent-builder/references/chatflow/nodes/reply.md +97 -0
  36. package/skills/agent-builder/references/chatflow/nodes/start.md +35 -0
  37. package/skills/agent-builder/references/chatflow/nodes/sub-graph.md +48 -0
  38. package/skills/agent-builder/references/chatflow/nodes/tool.md +138 -0
  39. package/skills/agent-builder/references/chatflow/nodes/transfer-to-human.md +63 -0
  40. package/skills/agent-builder/references/chatflow/nodes/variable-assignment.md +52 -0
  41. package/skills/agent-builder/references/chatflow/nodes/variable-extract.md +50 -0
  42. package/skills/agent-builder/references/chatflow/nodes/variable-process.md +70 -0
  43. package/skills/agent-builder/references/chatflow/overview.md +53 -0
  44. package/skills/agent-builder/references/chatflow/visual-translation.md +64 -0
  45. package/skills/agent-builder/references/chatflow/workflow.md +134 -0
  46. package/skills/agent-builder/references/multi-agent/create-update.md +185 -0
  47. package/skills/agent-builder/references/multi-agent/graph-spec.md +246 -0
  48. package/skills/agent-builder/references/multi-agent/graph-structure.md +12 -0
  49. package/skills/agent-builder/references/multi-agent/migrate-to-chatflow.md +13 -0
  50. package/skills/agent-builder/references/multi-agent/migration/large.md +554 -0
  51. package/skills/agent-builder/references/multi-agent/migration/principles.md +446 -0
  52. package/skills/agent-builder/references/multi-agent/migration/small.md +225 -0
  53. package/skills/agent-builder/references/multi-agent/orchestration.md +11 -0
  54. package/skills/agent-builder/references/multi-agent/overview.md +48 -0
  55. package/skills/agent-builder/references/multi-agent/update.md +128 -0
  56. package/skills/agent-builder/references/multi-agent/workflow.md +87 -0
  57. package/skills/agent-builder/references/single-agent/overview.md +48 -0
  58. package/skills/agent-builder/references/single-agent/prompt.md +16 -0
  59. package/skills/agent-builder/references/single-agent/tools.md +36 -0
  60. package/skills/agent-builder/references/single-agent/voice.md +46 -0
  61. package/skills/agent-builder/references/tools/http-tool.md +315 -0
  62. package/skills/agent-builder/references/tools/mock-tool.md +279 -0
  63. package/skills/agent-builder/references/tools/overview.md +46 -0
  64. package/skills/agent-builder/references/tools/tool-config.md +43 -0
  65. package/skills/agent-builder/references/tools/x-token.md +34 -0
  66. package/skills/agent-builder/scripts/chatflow/chatflow_builder.py +3458 -0
  67. package/skills/agent-builder/scripts/chatflow/validate_graph.py +1697 -0
  68. package/skills/agent-builder/scripts/copy_marketplace_agent.py +61 -0
  69. package/skills/agent-builder/scripts/multi-agent/legacy_graph_analyzer.py +5974 -0
  70. package/skills/agent-builder/scripts/multi-agent/replay_compare.py +679 -0
  71. package/skills/agent-builder/scripts/patch_agent_tools.py +135 -0
  72. package/skills/agent-builder/scripts/tools/validate_tool_config.py +384 -0
  73. package/skills/agent-tester/SKILL.md +35 -0
  74. package/skills/agent-tester/batch-test/overview.md +86 -0
  75. package/skills/agent-tester/scripts/batch_test_sheet.py +213 -0
  76. package/skills/agent-tester/single-test/badcase-retest.md +85 -0
  77. package/skills/agent-tester/single-test/direct-test.md +24 -0
  78. package/skills/agent-tester/single-test/overview.md +142 -0
  79. package/skills/delivery/SKILL.md +112 -0
  80. package/skills/delivery/interfaces/classify.md +38 -0
  81. package/skills/delivery/interfaces/create-agents.md +89 -0
  82. package/skills/delivery/interfaces/delivery-test.md +67 -0
  83. package/skills/delivery/references/flow.md +40 -0
  84. package/skills/delivery/references/validation.md +15 -0
  85. package/skills/delivery/scripts/batch_test.py +704 -0
  86. package/skills/delivery/scripts/classify.py +22 -0
  87. package/skills/delivery/scripts/create_agents.py +695 -0
  88. package/skills/delivery/scripts/gen_delivery_report.py +335 -0
  89. package/skills/delivery/scripts/gen_excel.py +234 -0
  90. package/skills/delivery/scripts/gen_report.py +208 -0
  91. package/skills/delivery/scripts/plan.py +163 -0
  92. package/skills/delivery/scripts/render.py +713 -0
  93. package/skills/delivery/scripts/render_summary.py +38 -0
  94. package/skills/delivery/scripts/sample_history_cases.py +109 -0
  95. package/skills/kb-builder/SKILL.md +80 -0
  96. package/skills/kb-builder/interfaces/qa-upload.md +55 -0
  97. package/skills/kb-builder/interfaces/web-kb.md +84 -0
  98. package/skills/kb-builder/references/directory.md +373 -0
  99. package/skills/kb-builder/references/export.md +282 -0
  100. package/skills/kb-builder/references/isolation-keywords.md +113 -0
  101. package/skills/kb-builder/references/knowledge.md +153 -0
  102. package/skills/kb-builder/references/product.md +52 -0
  103. package/skills/kb-builder/references/qa-from-help-center.md +58 -0
  104. package/skills/kb-builder/references/qa-from-pdf.md +94 -0
  105. package/skills/kb-builder/references/qa-from-tickets.md +168 -0
  106. package/skills/kb-builder/references/qa-from-web.md +98 -0
  107. package/skills/kb-builder/references/qa-quality.md +73 -0
  108. package/skills/kb-builder/references/reply-template.md +237 -0
  109. package/skills/kb-builder/references/tag-isolation-migration.md +61 -0
  110. package/skills/kb-builder/references/tags.md +45 -0
  111. package/skills/kb-builder/references/upload.md +130 -0
  112. package/skills/kb-builder/scripts/batch_extract_ticket_qa.py +333 -0
  113. package/skills/kb-builder/scripts/build_directory_plan.py +144 -0
  114. package/skills/kb-builder/scripts/crawl_site.py +433 -0
  115. package/skills/kb-builder/scripts/create_dirs.py +146 -0
  116. package/skills/kb-builder/scripts/export_knowledge.py +454 -0
  117. package/skills/kb-builder/scripts/extract_ticket_qa.py +441 -0
  118. package/skills/kb-builder/scripts/extract_web_qa.py +118 -0
  119. package/skills/kb-builder/scripts/import_qa.py +219 -0
  120. package/skills/kb-builder/scripts/import_source_documents.py +143 -0
  121. package/skills/kb-builder/scripts/import_templates.py +409 -0
  122. package/skills/poc/SKILL.md +125 -0
  123. package/skills/poc/assets/default_manual.css +279 -0
  124. package/skills/poc/assets/notice_escalate.png +0 -0
  125. package/skills/poc/assets/notice_escalate2.png +0 -0
  126. package/skills/poc/assets/notice_new_chat.png +0 -0
  127. package/skills/poc/assets/poc_test_template.html +645 -0
  128. package/skills/poc/assets/poc_test_template.md +104 -0
  129. package/skills/poc/assets/step3_preview.png +0 -0
  130. package/skills/poc/assets/step4_order_note.png +0 -0
  131. package/skills/poc/interfaces/classify.md +38 -0
  132. package/skills/poc/interfaces/create-agents.md +108 -0
  133. package/skills/poc/interfaces/poc-test.md +94 -0
  134. package/skills/poc/references/flow.md +47 -0
  135. package/skills/poc/references/kb-directory.md +22 -0
  136. package/skills/poc/references/validation.md +17 -0
  137. package/skills/poc/scripts/classify.py +22 -0
  138. package/skills/poc/scripts/create_agents.py +818 -0
  139. package/skills/poc/scripts/gen_excel.py +234 -0
  140. package/skills/poc/scripts/gen_poc_manual.py +249 -0
  141. package/skills/poc/scripts/gen_report.py +208 -0
  142. package/skills/poc/scripts/inspect_chatflow.py +209 -0
  143. package/skills/poc/scripts/plan.py +178 -0
  144. package/skills/poc/scripts/poc_test.py +495 -0
  145. package/skills/poc/scripts/render.py +713 -0
  146. package/skills/poc/scripts/render_summary.py +41 -0
  147. package/skills/runtime-inspector/SKILL.md +51 -0
  148. package/skills/runtime-inspector/references/frontend-rendering.md +126 -0
  149. package/skills/runtime-inspector/references/model.md +59 -0
  150. package/skills/runtime-inspector/references/sandbox.md +131 -0
  151. package/skills/runtime-inspector/scripts/model_inspect.py +68 -0
  152. package/skills/runtime-inspector/scripts/prepare.py +98 -0
  153. package/skills/runtime-inspector/scripts/process.py +84 -0
  154. package/skills/runtime-inspector/scripts/read_state.py +32 -0
  155. package/skills/runtime-inspector/scripts/sandbox_inspect.py +463 -0
  156. package/skills/runtime-inspector/scripts/test_claude_proxy.py +49 -0
  157. package/skills/runtime-inspector/scripts/test_openai_proxy.py +41 -0
  158. package/skills/runtime-inspector/scripts/verify_persistence.py +101 -0
  159. package/skills/runtime-inspector/scripts/write_test.py +41 -0
  160. package/skills/setup/SKILL.md +17 -0
  161. package/skills/shulex-support/SKILL.md +39 -0
  162. package/skills/shulex-support/references/badcase-analysis/overview.md +34 -0
  163. package/skills/shulex-support/references/badcase-analysis/text/conversation-record.md +105 -0
  164. package/skills/shulex-support/references/badcase-analysis/text/core-flow.md +89 -0
  165. package/skills/shulex-support/references/badcase-analysis/text/log-query-rules.md +36 -0
  166. package/skills/shulex-support/references/badcase-analysis/text/m1-entry.md +53 -0
  167. package/skills/shulex-support/references/badcase-analysis/text/m10-chatflow-service.md +259 -0
  168. package/skills/shulex-support/references/badcase-analysis/text/m11-algorithm-service.md +148 -0
  169. package/skills/shulex-support/references/badcase-analysis/text/m12-quality-inspection.md +57 -0
  170. package/skills/shulex-support/references/badcase-analysis/text/m2-intent.md +120 -0
  171. package/skills/shulex-support/references/badcase-analysis/text/m3-strategy.md +126 -0
  172. package/skills/shulex-support/references/badcase-analysis/text/m4-aifield.md +121 -0
  173. package/skills/shulex-support/references/badcase-analysis/text/m5-agent.md +208 -0
  174. package/skills/shulex-support/references/badcase-analysis/text/m6-tool.md +115 -0
  175. package/skills/shulex-support/references/badcase-analysis/text/m7-reply.md +72 -0
  176. package/skills/shulex-support/references/badcase-analysis/text/m8-error.md +92 -0
  177. package/skills/shulex-support/references/badcase-analysis/text/m9-knowledge-search.md +147 -0
  178. package/skills/shulex-support/references/badcase-analysis/text/sls-query.md +283 -0
  179. package/skills/shulex-support/references/badcase-analysis/voice/core-flow.md +77 -0
  180. package/skills/shulex-support/references/badcase-analysis/voice/dependencies.md +19 -0
  181. package/skills/shulex-support/references/badcase-analysis/voice/platform-api.md +23 -0
  182. package/skills/shulex-support/references/badcase-analysis/voice/sls-playbook.md +31 -0
  183. package/skills/shulex-support/references/product-docs.md +70 -0
  184. package/skills/shulex-support/references/tickets.md +184 -0
  185. package/skills/shulex-support/scripts/support.py +427 -0
  186. package/skills/solvea-desk/SKILL.md +39 -0
  187. package/skills/solvea-desk/references/report/fields.md +82 -0
  188. package/skills/solvea-desk/references/report/report-design.md +200 -0
  189. package/skills/solvea-desk/references/report/report-template.html +44 -0
  190. package/skills/solvea-desk/references/report/report.css +517 -0
  191. package/skills/solvea-desk/references/report/workflow.md +464 -0
  192. package/skills/solvea-desk/references/tickets/query.md +103 -0
  193. package/skills/solvea-desk/references/tickets/tags.md +83 -0
  194. package/skills/solvea-desk/scripts/report/fetch_sources.py +580 -0
  195. package/skills/solvea-desk/scripts/report/render_report.py +763 -0
  196. package/skills/solvea-desk/scripts/tickets/desk_api.py +73 -0
  197. package/skills/solvea-desk/scripts/tickets/fetch_tickets.py +403 -0
  198. package/skills/solvea-desk/scripts/tickets/ticket_tags.py +305 -0
  199. package/skills/solvea-kit/SKILL.md +84 -0
  200. package/skills/solvea-kit/references/agent.md +145 -0
  201. package/skills/solvea-kit/references/chat.md +244 -0
  202. package/skills/solvea-kit/references/intent.md +69 -0
  203. package/skills/solvea-kit/references/plugin.md +53 -0
  204. package/skills/solvea-kit/references/quality-inspection.md +75 -0
  205. package/skills/solvea-kit/references/strategy.md +62 -0
  206. package/skills/solvea-kit/references/tagging.md +231 -0
  207. package/skills/solvea-kit/references/variables.md +53 -0
  208. package/skills/solvea-kit/scripts/generate_label_import_file.py +151 -0
  209. package/skills/solvea-kit/scripts/intent_classify.py +843 -0
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """Shared authenticated HTTP adapter for Shulex Desk / Tars APIs."""
3
+
4
+ import os
5
+ import sys
6
+ sys.path.insert(0, os.path.join(os.environ["SOLVEA_LIB_DIR"], "skill-runtime"))
7
+ from bootstrap import SOLVEA_ENV # noqa: E402
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+
15
+ DEFAULT_APPS_STAGING_URL = "https://apps-staging.voc.ai"
16
+ DEFAULT_APPS_PROD_URL = "https://apps.voc.ai"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DeskApiConfig:
21
+ base_url: str
22
+ token: str
23
+ cookie_name: str
24
+
25
+
26
+ def resolve_desk_api_config() -> DeskApiConfig:
27
+ """Resolve the authenticated Desk proxy for the current runtime environment."""
28
+ env = os.environ.get("SOLVEA_ENV", SOLVEA_ENV) or "prod"
29
+ if env == "prod":
30
+ return DeskApiConfig(
31
+ base_url=os.environ.get("SOLVEA_PROD_URL", DEFAULT_APPS_PROD_URL).rstrip("/"),
32
+ token=os.environ.get("SOLVEA_PROD_TOKEN", "") or os.environ.get("SOLVEA_TOKEN", ""),
33
+ cookie_name="_token",
34
+ )
35
+ return DeskApiConfig(
36
+ base_url=os.environ.get("SOLVEA_BASE_URL", DEFAULT_APPS_STAGING_URL).rstrip("/"),
37
+ token=os.environ.get("SOLVEA_TOKEN", ""),
38
+ cookie_name="_token_staging",
39
+ )
40
+
41
+
42
+ def request_json(
43
+ client: httpx.Client,
44
+ config: DeskApiConfig,
45
+ method: str,
46
+ path: str,
47
+ *,
48
+ params: dict[str, Any] | None = None,
49
+ json_body: dict[str, Any] | None = None,
50
+ ) -> dict[str, Any]:
51
+ """Send one Desk request and validate its transport and business envelope."""
52
+ headers = {
53
+ "Accept": "application/json, text/plain, */*",
54
+ "Origin": config.base_url,
55
+ "Referer": f"{config.base_url}/botdesk",
56
+ }
57
+ if config.token:
58
+ headers["Cookie"] = f"{config.cookie_name}={config.token}"
59
+
60
+ response = client.request(
61
+ method,
62
+ f"{config.base_url}{path}",
63
+ params=params,
64
+ json=json_body,
65
+ headers=headers,
66
+ )
67
+ response.raise_for_status()
68
+ payload = response.json()
69
+ if not isinstance(payload, dict):
70
+ raise ValueError("Desk API must return a JSON object")
71
+ if payload.get("code") is not None and str(payload["code"]) != "200":
72
+ raise RuntimeError(str(payload.get("message") or "Desk API request failed"))
73
+ return payload
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch the current Desk ticket list into a local XLSX snapshot."""
3
+
4
+ import os
5
+ import sys
6
+ sys.path.insert(0, os.path.join(os.environ["SOLVEA_LIB_DIR"], "skill-runtime"))
7
+ from bootstrap import ( # noqa: E402
8
+ OUTPUTS_DIR,
9
+ SOLVEA_ENV,
10
+ SOLVEA_PROJECT_ID,
11
+ SOLVEA_TENANT_ID,
12
+ STATE_DIR,
13
+ TMP_DIR,
14
+ timestamp,
15
+ )
16
+
17
+ import argparse
18
+ import json
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Any
22
+ from urllib.parse import urlsplit
23
+
24
+ import httpx
25
+ import pandas as pd
26
+
27
+ from desk_api import DeskApiConfig, request_json, resolve_desk_api_config
28
+
29
+
30
+ SOURCE_TYPE = "ticket_list_snapshot"
31
+ SOURCE_CONTRACT = "current_user_view_filtered_paginated_api"
32
+ TICKET_LIST_PATH = "/api_v2/tars/api/ticket/list"
33
+ DEFAULT_PAGE_SIZE = 100
34
+ MAX_PAGE_SIZE = 100
35
+ DEFAULT_MAX_PAGES = 120
36
+ DEFAULT_MAX_ROWS = 200000
37
+ BACKEND_FILTER_FIELDS = {
38
+ "ticketNumber",
39
+ "ticketStatusList",
40
+ "channelList",
41
+ "platformList",
42
+ "channelIdList",
43
+ "supportEmailIdList",
44
+ "agentIdList",
45
+ "tagList",
46
+ "replyUserList",
47
+ "customerEmail",
48
+ "customerName",
49
+ "customerEmailFull",
50
+ "customerPhone",
51
+ "createTimeBegin",
52
+ "createTimeEnd",
53
+ "keyword",
54
+ "marketList",
55
+ "sendFailure",
56
+ "orderId",
57
+ "categoryList",
58
+ "brandList",
59
+ "humanHandling",
60
+ "myTicket",
61
+ "snoozed",
62
+ "scheduled",
63
+ "notSpam",
64
+ "notNotification",
65
+ "botIdList",
66
+ "noTag",
67
+ "ticketNumberList",
68
+ "supportPhoneList",
69
+ "unread",
70
+ "personaId",
71
+ "campaignName",
72
+ "lastVoiceAnswerType",
73
+ "lastVoicePhoneType",
74
+ }
75
+
76
+
77
+ def _now_iso() -> str:
78
+ return datetime.now().astimezone().isoformat(timespec="seconds")
79
+
80
+
81
+ def resolve_source_config() -> DeskApiConfig:
82
+ """Resolve the Desk ticket-list endpoint for the current environment."""
83
+ config = resolve_desk_api_config()
84
+ explicit_endpoint = os.environ.get("DESK_TICKET_SOURCE_URL", "").strip()
85
+ if not explicit_endpoint:
86
+ return config
87
+ parsed = urlsplit(explicit_endpoint)
88
+ if not parsed.scheme or not parsed.netloc:
89
+ raise ValueError("DESK_TICKET_SOURCE_URL must be an absolute URL")
90
+ return DeskApiConfig(
91
+ base_url=f"{parsed.scheme}://{parsed.netloc}",
92
+ token=config.token,
93
+ cookie_name=config.cookie_name,
94
+ )
95
+
96
+
97
+ def _ticket_list_path() -> str:
98
+ explicit_endpoint = os.environ.get("DESK_TICKET_SOURCE_URL", "").strip()
99
+ if not explicit_endpoint:
100
+ return TICKET_LIST_PATH
101
+ parsed = urlsplit(explicit_endpoint)
102
+ return parsed.path or TICKET_LIST_PATH
103
+
104
+
105
+ def validate_page_size(page_size: str | int) -> int:
106
+ try:
107
+ value = int(page_size)
108
+ except (TypeError, ValueError) as exc:
109
+ raise argparse.ArgumentTypeError("page-size must be an integer") from exc
110
+ if value < 1 or value > MAX_PAGE_SIZE:
111
+ raise argparse.ArgumentTypeError(f"page-size must be between 1 and {MAX_PAGE_SIZE}")
112
+ return value
113
+
114
+
115
+ def parse_backend_filters(raw_json: str = "", filters_file: str = "") -> dict[str, Any]:
116
+ """Parse the filter-only subset of TicketListPageDTO."""
117
+ if raw_json and filters_file:
118
+ raise ValueError("use only one of --filters-json and --filters-file")
119
+ if filters_file:
120
+ raw_json = Path(filters_file).read_text(encoding="utf-8")
121
+ if not raw_json:
122
+ return {}
123
+ payload = json.loads(raw_json)
124
+ if not isinstance(payload, dict):
125
+ raise ValueError("ticket filters must be a JSON object")
126
+ unknown = sorted(set(payload) - BACKEND_FILTER_FIELDS)
127
+ if unknown:
128
+ raise ValueError(f"unsupported ticket filter fields: {', '.join(unknown)}")
129
+ return payload
130
+
131
+
132
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
133
+ path.parent.mkdir(parents=True, exist_ok=True)
134
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
135
+
136
+
137
+ def _normalize_cell(value: Any) -> Any:
138
+ if isinstance(value, (dict, list)):
139
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
140
+ return value
141
+
142
+
143
+ def _records(payload: dict[str, Any]) -> list[dict[str, Any]]:
144
+ code = payload.get("code")
145
+ if code is not None and str(code) != "200":
146
+ raise RuntimeError(payload.get("message") or "Desk ticket-list API failed")
147
+ body = payload.get("data", payload)
148
+ if not isinstance(body, dict):
149
+ return []
150
+ values = body.get("list") or body.get("records") or body.get("items") or []
151
+ return [item for item in values if isinstance(item, dict)] if isinstance(values, list) else []
152
+
153
+
154
+ def _page_body(payload: dict[str, Any]) -> dict[str, Any]:
155
+ body = payload.get("data", payload)
156
+ return body if isinstance(body, dict) else {}
157
+
158
+
159
+ def _has_more(payload: dict[str, Any], page: int, page_size: int, row_count: int) -> bool:
160
+ body = _page_body(payload)
161
+ for key in ("hasMore", "has_more"):
162
+ if key in body:
163
+ return bool(body[key])
164
+ total = body.get("total")
165
+ if isinstance(total, int):
166
+ return page * page_size < total
167
+ return row_count >= page_size
168
+
169
+
170
+ def _request_page(
171
+ client: httpx.Client,
172
+ config: DeskApiConfig,
173
+ *,
174
+ page: int,
175
+ page_size: int,
176
+ view_id: str,
177
+ sort: str,
178
+ order: str,
179
+ backend_filters: dict[str, Any] | None = None,
180
+ ) -> dict[str, Any]:
181
+ request_payload = {
182
+ "page": page,
183
+ "pageSize": page_size,
184
+ "viewId": view_id,
185
+ "sort": sort,
186
+ "order": order,
187
+ }
188
+ request_payload.update(backend_filters or {})
189
+ return request_json(
190
+ client,
191
+ config,
192
+ "POST",
193
+ _ticket_list_path(),
194
+ json_body=request_payload,
195
+ )
196
+
197
+
198
+ def fetch_pages(
199
+ config: DeskApiConfig,
200
+ *,
201
+ page_size: int,
202
+ max_pages: int,
203
+ max_rows: int,
204
+ view_id: str,
205
+ sort: str,
206
+ order: str,
207
+ backend_filters: dict[str, Any] | None = None,
208
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], bool, str]:
209
+ page_size = validate_page_size(page_size)
210
+ pages: list[dict[str, Any]] = []
211
+ records: list[dict[str, Any]] = []
212
+ truncated = False
213
+ completion_reason = "source_exhausted"
214
+ with httpx.Client(timeout=60.0) as client:
215
+ for page_no in range(1, max_pages + 1):
216
+ payload = _request_page(
217
+ client,
218
+ config,
219
+ page=page_no,
220
+ page_size=page_size,
221
+ view_id=view_id,
222
+ sort=sort,
223
+ order=order,
224
+ backend_filters=backend_filters,
225
+ )
226
+ page_records = _records(payload)
227
+ pages.append(payload)
228
+ records.extend(page_records)
229
+ if len(records) >= max_rows:
230
+ exceeded_limit = len(records) > max_rows
231
+ records = records[:max_rows]
232
+ has_more = _has_more(payload, page_no, page_size, len(page_records))
233
+ truncated = exceeded_limit or has_more
234
+ completion_reason = "max_rows" if truncated else "source_exhausted"
235
+ break
236
+ if not _has_more(payload, page_no, page_size, len(page_records)):
237
+ break
238
+ else:
239
+ truncated = True
240
+ completion_reason = "max_pages"
241
+ return pages, records, truncated, completion_reason
242
+
243
+
244
+ def write_snapshot(
245
+ task_id: str,
246
+ pages: list[dict[str, Any]],
247
+ records: list[dict[str, Any]],
248
+ *,
249
+ config: DeskApiConfig,
250
+ page_size: int,
251
+ view_id: str,
252
+ sort: str,
253
+ order: str,
254
+ truncated: bool,
255
+ backend_filters: dict[str, Any] | None = None,
256
+ completion_reason: str = "source_exhausted",
257
+ ) -> dict[str, Any]:
258
+ state_task_dir = STATE_DIR / task_id
259
+ output_task_dir = OUTPUTS_DIR / task_id
260
+ tmp_task_dir = TMP_DIR / task_id
261
+ state_task_dir.mkdir(parents=True, exist_ok=True)
262
+ tmp_task_dir.mkdir(parents=True, exist_ok=True)
263
+
264
+ for index, page in enumerate(pages, start=1):
265
+ _write_json(tmp_task_dir / f"raw_page_{index:03d}.json", page)
266
+
267
+ rows = [{key: _normalize_cell(value) for key, value in row.items()} for row in records]
268
+ dataframe = pd.DataFrame(rows) if rows else pd.DataFrame([{"_empty": ""}])
269
+ datasource = state_task_dir / "datasource.xlsx"
270
+ dataframe.to_excel(datasource, sheet_name="data", index=False, engine="openpyxl")
271
+
272
+ manifest = {
273
+ "status": "ready",
274
+ "skill": "solvea-desk",
275
+ "source_type": SOURCE_TYPE,
276
+ "source_contract": SOURCE_CONTRACT,
277
+ "task_id": task_id,
278
+ "env": SOLVEA_ENV,
279
+ "tenant_id": SOLVEA_TENANT_ID,
280
+ "project_id": SOLVEA_PROJECT_ID,
281
+ "endpoint": f"{config.base_url}{_ticket_list_path()}",
282
+ "data_scope": "current_user",
283
+ "snapshot_semantics": "current_state",
284
+ "view_id": view_id,
285
+ "sort": sort,
286
+ "order": order,
287
+ "backend_filters": backend_filters or {},
288
+ "completion_reason": completion_reason,
289
+ "page_size": page_size,
290
+ "page_count": len(pages),
291
+ "row_counts": {"data": len(records)},
292
+ "truncated": truncated,
293
+ "files": {
294
+ "state": {
295
+ "datasource": "datasource.xlsx",
296
+ "manifest": "manifest.json",
297
+ "analysis": "analysis.py",
298
+ "report_data": "report_data.json",
299
+ "calculation_notes": "calculation_notes.json",
300
+ },
301
+ "outputs": {
302
+ "report_pattern": "<report_slug>.html",
303
+ "detail_pattern": "<report_slug>_detail.xlsx",
304
+ },
305
+ },
306
+ "datasource": str(datasource),
307
+ "tmp_raw_pages": str(tmp_task_dir),
308
+ "generated_at": _now_iso(),
309
+ }
310
+ _write_json(state_task_dir / "manifest.json", manifest)
311
+ _write_json(
312
+ STATE_DIR / "latest_task.json",
313
+ {
314
+ "skill": "solvea-desk",
315
+ "status": "ready",
316
+ "source_type": SOURCE_TYPE,
317
+ "task_id": task_id,
318
+ "task_dir": str(state_task_dir),
319
+ "state_task_dir": str(state_task_dir),
320
+ "output_task_dir": str(output_task_dir),
321
+ "tmp_task_dir": str(tmp_task_dir),
322
+ "view_id": view_id,
323
+ "backend_filters": backend_filters or {},
324
+ "updated_at": _now_iso(),
325
+ },
326
+ )
327
+ return {
328
+ "ok": True,
329
+ "source_type": SOURCE_TYPE,
330
+ "task_id": task_id,
331
+ "task_dir": str(state_task_dir),
332
+ "state_task_dir": str(state_task_dir),
333
+ "output_task_dir": str(output_task_dir),
334
+ "tmp_task_dir": str(tmp_task_dir),
335
+ "datasource": str(datasource),
336
+ "manifest": str(state_task_dir / "manifest.json"),
337
+ "row_counts": {"data": len(records)},
338
+ "truncated": truncated,
339
+ }
340
+
341
+
342
+ def main() -> int:
343
+ parser = argparse.ArgumentParser(description="Fetch current Desk ticket details into datasource.xlsx")
344
+ parser.add_argument("--task-id", default="", help="Optional task id; defaults to timestamp")
345
+ parser.add_argument("--view-id", default="1", help="Desk ticket view id")
346
+ parser.add_argument("--page-size", type=validate_page_size, default=DEFAULT_PAGE_SIZE)
347
+ parser.add_argument("--max-pages", type=int, default=DEFAULT_MAX_PAGES)
348
+ parser.add_argument("--max-rows", type=int, default=DEFAULT_MAX_ROWS)
349
+ parser.add_argument("--sort", default="last_update_time")
350
+ parser.add_argument("--order", choices=["ASC", "DESC"], default="DESC")
351
+ parser.add_argument(
352
+ "--filters-json",
353
+ default="",
354
+ help="JSON object containing supported TicketListPageDTO filter fields",
355
+ )
356
+ parser.add_argument("--filters-file", default="", help="Path to a JSON filter object")
357
+ args = parser.parse_args()
358
+
359
+ try:
360
+ backend_filters = parse_backend_filters(args.filters_json, args.filters_file)
361
+ except (ValueError, json.JSONDecodeError, OSError) as exc:
362
+ parser.error(str(exc))
363
+
364
+ config = resolve_source_config()
365
+ if not config.token:
366
+ print(json.dumps({
367
+ "ok": False,
368
+ "source_type": SOURCE_TYPE,
369
+ "error": "desk_ticket_runtime_not_configured",
370
+ "message": "Check SOLVEA_ENV and the corresponding Solvea token environment variable.",
371
+ }, ensure_ascii=False, indent=2))
372
+ return 2
373
+
374
+ task_id = args.task_id.strip() or timestamp()
375
+ pages, records, truncated, completion_reason = fetch_pages(
376
+ config,
377
+ page_size=args.page_size,
378
+ max_pages=args.max_pages,
379
+ max_rows=args.max_rows,
380
+ view_id=args.view_id,
381
+ sort=args.sort,
382
+ order=args.order,
383
+ backend_filters=backend_filters,
384
+ )
385
+ result = write_snapshot(
386
+ task_id,
387
+ pages,
388
+ records,
389
+ config=config,
390
+ page_size=args.page_size,
391
+ view_id=args.view_id,
392
+ sort=args.sort,
393
+ order=args.order,
394
+ truncated=truncated,
395
+ backend_filters=backend_filters,
396
+ completion_reason=completion_reason,
397
+ )
398
+ print(json.dumps(result, ensure_ascii=False, indent=2))
399
+ return 3 if truncated else 0
400
+
401
+
402
+ if __name__ == "__main__":
403
+ raise SystemExit(main())