@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.
- package/README.md +25 -22
- package/dist/cli.js +2202 -127
- package/dist/cli.js.map +1 -1
- package/package.json +19 -6
- package/capabilities.lock.json +0 -898
- 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,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())
|