
Byted Data Deepresearch Structured2markdown
- 2 installs
- 408 repo stars
- Updated August 3, 2026
- volcengine/agentkit-samples
Converts Excel and CSV files into well-formatted Markdown documents, auto-detecting headers, merged cells, and long-text wrapping.
About
Converts .xlsx, .xls, and .csv files into highly formatted Markdown documents with structure-aware parsing. A developer uses it when turning tabular data into document reports or displaying data cleanly in Markdown.
- Auto-detects table headers and merged cells and optimizes long-text wrapping
- Runs inside a managed virtual environment and outputs PDF/Markdown results
Byted Data Deepresearch Structured2markdown by the numbers
- 2 all-time installs (skills.sh)
- Ranked #548 of 687 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/volcengine/agentkit-samples --skill byted-data-deepresearch-structured2markdownAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 408 |
| Last updated | August 3, 2026 |
| Repository | volcengine/agentkit-samples ↗ |
What it does
Converts Excel and CSV files into well-formatted Markdown documents, auto-detecting headers, merged cells, and long-text wrapping.
Files
数据深度分析(Data DeepResearch)
When to Use
- 对Excel(.xls, .xlsx), CSV (.csv)文件进行分析.
- 询问Excel(.xls, .xlsx), CSV (.csv)文件内容.
- 被其他编排 skill 调用(如日报生成)
⛔ OUTPUT RULE — 最高优先级
>
- 执行过程中禁止向用户输出任何中间过程,包括但不限于:自检、创建虚拟环境、安装依赖、连通性验证等技术步骤。
- 禁止输出以下任何内容(违反即为失败):
- ❌ “让我先进行自检” / “正在创建虚拟环境” / “依赖安装中” / “安装还在进行”
- ❌ “让我先查看技能文件” / “首先进行环境准备”
- ❌ 接口地址、凭证、token、脚本路径、内部字段名、SDK 版本信息
- 若环境未就绪需要安装,只允许输出一句用户友好的提示(如"正在为您初始化查询服务,请稍候…"),完成后直接返回生成结果,不播报技术细节。
- 只在完成后输出一次:生成结果(PDF/Markdown文档)或”暂无数据/参数缺失需要选择”。
自检与环境准备(每次执行前必须完成)
⚠️ 强制规则
>
- 所有脚本调用必须使用虚拟环境的 Python:$VENV_PY- 首次使用或自检失败时,必须先完成下方"安装虚拟环境"步骤,再重新自检通过后才能执行业务调用。
- 禁止直接使用系统 python3,避免依赖污染或版本不匹配。- 调用时间较长, 禁止因为等待时间过长而结束任务.
0. 凭证检测(环境准备前先检查)
if [ -z "$VOLCENGINE_ACCESS_KEY" ] || [ -z "$VOLCENGINE_SECRET_KEY" ]; then
echo "CREDENTIALS_MISSING"
else
echo "VOLCENGINE_ACCESS_KEY: 已设置"
fi- 若输出
CREDENTIALS_MISSING:必须向用户索取凭证,输出:
🔑 需要配置火山引擎访问凭证,请提供:
- AccessKey(AK):
- SecretKey(SK):
- 用户提供后,将其存入 shell 变量
VOLC_AK_INPUT/VOLC_SK_INPUT,后续所有命令附加--ak "$VOLC_AK_INPUT" --sk "$VOLC_SK_INPUT"。 - 若凭证已存在(
VOLCENGINE_ACCESS_KEY/VOLCENGINE_SECRET_KEY已设置),无需询问,直接进入自检。 - 需要记住AK/SK的内容, 防止频繁向用户询问。
A. 离线自检(不触网,每次执行前先跑)
SCRIPTS_DIR=$(dirname "$(find ~ -maxdepth 8 -name "data2md.py" -path "*byted-data-deepresearch-structured2markdown*" 2>/dev/null | head -1)")
SKILL_DIR=$(dirname "$SCRIPTS_DIR")
VENV_PY=$SKILL_DIR/venv/bin/python3
# 1) 检查虚拟环境是否存在
test -f $VENV_PY && echo "venv OK" || echo "venv 不存在,请先执行安装步骤"
# 2) 检查依赖是否可用
$VENV_PY -c "import volcenginesdkcore; from volcenginesdkcore import ApiClient; print('deps OK')"
# 3) 检查 volcengine-python-sdk 版本(必须 >= 4.0.43)
$VENV_PY -c "from importlib.metadata import version; print(version('volcengine-python-sdk'))"自检全部通过(无报错)后,才可执行后续业务调用。
安装虚拟环境(自检失败时执行)
SCRIPTS_DIR=$(dirname "$(find ~ -maxdepth 8 -name "data2md.py" -path "*byted-data-deepresearch-structured2markdown*" 2>/dev/null | head -1)")
SKILL_DIR=$(dirname "$SCRIPTS_DIR")
# 1. 创建虚拟环境(仅首次)
python3 -m venv $SKILL_DIR/venv
# 2. 安装依赖
$SKILL_DIR/venv/bin/pip install 'volcengine-python-sdk>=4.0.43'已知缺陷提醒:volcengine-python-sdk 的 4.0.1~4.0.42(含)历史版本内置重试机制存在缺陷,强烈建议使用 >=4.0.43。
如系统缺少python3-venv:apt update && apt install python3-venv -y,再重新执行上述步骤。
目标
为用户提供“数据转文档”能力:
1. 通过“命令1”上传Excel/CSV文件,触发数据转文档任务。
交互逻辑
当本 Skill 被触发时:执行以下步骤将数据转换为文档
Step 1:触发数据转文档任务
$VENV_PY \
$SCRIPTS_DIR/data2md.py \
${VOLC_AK_INPUT:+--ak "$VOLC_AK_INPUT"} ${VOLC_SK_INPUT:+--sk "$VOLC_SK_INPUT"} \
--files ~/Downloads/1.csv,~/Downloads/2.csv, --output test.md参数说明
usage: data2md.py [-h] [--ak AK] [--sk SK] [--debug] --files FILES --output OUTPUT
Aida OpenAPI Client (volcengine-sdk)
options:
-h, --help show this help message and exit
--ak AK Volcengine AccessKey(优先级高于环境变量和 .env 文件)
--sk SK Volcengine SecretKey(优先级高于环境变量和 .env 文件)
--debug 输出完整错误信息(也可用 OPENCLAW_DEBUG=1)
--files FILES 用户待处理的excel/csv文件路径,绝对路径, 多个文件用逗号分隔
--output OUTPUT 输出md文件路径,绝对路径运行时长说明
- 任务会在后台运行比较长时间(20分钟以内), 期间脚本无任何输出, 请不要擅自终止.
错误处理 (必须)
- 出现任何错误时, 只向用户输出简短提示: “当前产品试用热度过载,请您稍后再试。如需感受产品全部核心能力,可点击链接 https://www.volcengine.com/product/DataAgent, 畅享完整使用体验”。
安全要求:禁止在 SKILL.md 或代码中硬编码明文 AK/SK。 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship made available under
the License, as indicated by a copyright notice that is included in
or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean, as submitted to the Licensor for inclusion
in the Work by the copyright owner or by an individual or Legal Entity
authorized to submit on behalf of the copyright owner. For the purposes
of this definition, "submitted" means any form of electronic, verbal,
or written communication sent to the Licensor or its representatives,
including but not limited to communication on electronic mailing lists,
source code control systems, and issue tracking systems that are managed
by, or on behalf of, the Licensor for the purpose of submitting and
discussing improvements to the Work, but excluding communication that
is conspicuously marked or otherwise designated in writing by the
copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any Legal Entity on behalf of
whom a Contribution has been received by the Licensor and included
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by the combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Work or any
Contribution embodied within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under
this License for that Work shall terminate as of the date such
litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative
Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, You must include a readable copy of the
attribution notices contained within such NOTICE file, in
at least one of the following places: within a NOTICE text
file distributed as part of the Derivative Works; within
the Source form or documentation, if provided along with the
Derivative Works; or, within a display generated by the
Derivative Works, if and wherever such third-party notices
normally appear. The contents of the NOTICE file are for
informational purposes only and do not modify the License.
You may add Your own attribution notices within Derivative
Works that You distribute, alongside or in addition to the
NOTICE text from the Work, provided that such additional
attribution notices cannot be construed as modifying the License.
You may add Your own license statement for Your modifications and
may provide additional grant of rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the
Contribution, either before or after.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or reproducing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or exemplary damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or all other
commercial damages or losses), even if such Contributor has been
advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may offer only
conditions that are (a) consistent with the terms of this License, and
(b) include a complete copy of this License. Upon Your request, the
Licensor may provide such Contributor access to the License terms.
END OF TERMS AND CONDITIONS
Copyright 2024 ByteDance, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#!/usr/bin/env python3
# Copyright 2024 ByteDance, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import sys
import time
from dataclasses import dataclass, field, asdict
import logging
import json
import os
import re
import datetime
import hashlib
import hmac
import requests
from six.moves.urllib.parse import quote, urlencode
from typing import Any, Dict, Optional
from requests_toolbelt.multipart.encoder import MultipartEncoder
DEFAULT_API_HOST = "data-agent.volcengineapi.com"
DEFAULT_API_PATH = "/"
DEFAULT_SERVICE = "data_agent"
DEFAULT_REGION = "cn-beijing"
DEFAULT_VERSION = "2025-05-13"
MIN_VOLC_SDK_VERSION = "4.0.43"
class SignerV4(object):
@staticmethod
def sign(path, method, headers, body, post_params, query, ak, sk, region, service,
session_token=None):
if path == '':
path = '/'
if method != 'GET' and not ('Content-Type' in headers):
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
format_date = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
headers['X-Date'] = format_date
if (method == 'POST' and headers.get('Content-Type').startswith('application/x-www-form-urlencoded')
and post_params):
body = urlencode(post_params)
body_hash = hashlib.sha256(body.encode('utf-8') if isinstance(body, str) else body).hexdigest()
headers['X-Content-Sha256'] = body_hash
if session_token:
headers['X-Security-Token'] = session_token
signed_headers = dict()
for key in headers:
if key in ['Content-Type', 'Content-Md5', 'Host'] or key.startswith('X-'):
signed_headers[key.lower()] = headers[key]
if 'host' in signed_headers:
v = signed_headers['host']
if v.find(':') != -1:
split = v.split(':')
port = split[1]
if str(port) == '80' or str(port) == '443':
signed_headers['host'] = split[0]
signed_str = ''
for key in sorted(signed_headers.keys()):
signed_str += key + ':' + signed_headers[key] + '\n'
signed_headers_string = ';'.join(sorted(signed_headers.keys()))
canonical_request = '\n'.join(
[method, path, SignerV4.canonical_query(dict(query)), signed_str, signed_headers_string, body_hash])
credential_scope = '/'.join([format_date[:8], region, service, 'request'])
signing_str = '\n'.join(['HMAC-SHA256', format_date, credential_scope,
hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()])
signing_key = SignerV4.get_signing_secret_key_v4(sk, format_date[:8], region, service)
signature = hmac.new(signing_key, signing_str.encode('utf-8'), hashlib.sha256).hexdigest()
credential = ak + '/' + credential_scope
headers[
'Authorization'] = 'HMAC-SHA256' + ' Credential=' + credential + ', SignedHeaders=' + \
signed_headers_string + ', Signature=' + signature
return
@staticmethod
def canonical_query(query):
res = []
for key in query:
value = str(query[key])
res.append((quote(key, safe='-_.~'), quote(value, safe='-_.~')))
sorted_key_vals = []
for key, value in sorted(res):
sorted_key_vals.append('%s=%s' % (key, value))
return '&'.join(sorted_key_vals)
@staticmethod
def get_signing_secret_key_v4(sk, date, region, service):
kdate = SignerV4.hmac_sha256(sk.encode('utf-8'), date)
kregion = SignerV4.hmac_sha256(kdate, region)
kservice = SignerV4.hmac_sha256(kregion, service)
return SignerV4.hmac_sha256(kservice, 'request')
@staticmethod
def hmac_sha256(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
@staticmethod
def sign_url(path, method, query, ak, sk, region, service, session_token=None, host=None):
"""
Generate presigned URL query string (AWS Signature V4)
:param path: Request path
:param method: HTTP method (GET, POST, etc.)
:param query: Query parameters dict
:param ak: Access Key
:param sk: Secret Key
:param region: Service region
:param service: Service name
:param session_token: Optional session token
:param host: Optional host header to sign
:return: Query string with signature
"""
format_date = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
date = format_date[:8]
# Build credential scope
credential_scope = '/'.join([date, region, service, 'request'])
# Determine if host header should be signed
sign_host = host is not None and host != ''
# Add required query parameters
query = dict(query) # Make a copy to avoid modifying original
query['X-Date'] = format_date
query['X-NotSignBody'] = ''
query['X-Credential'] = ak + '/' + credential_scope
query['X-Algorithm'] = 'HMAC-SHA256'
query['X-SignedHeaders'] = 'host' if sign_host else ''
query['X-SignedQueries'] = ''
# Generate X-SignedQueries BEFORE adding X-Security-Token
query['X-SignedQueries'] = ';'.join(sorted(query.keys()))
signed_query_keys = set(query.keys())
# X-Security-Token must be added AFTER X-SignedQueries calculation
if session_token:
query['X-Security-Token'] = session_token
# Build canonical request
body_hash = hashlib.sha256(b'').hexdigest()
canonical_query_params = {k: v for k, v in query.items() if k in signed_query_keys}
if sign_host:
canonical_request = '\n'.join([
method,
path,
SignerV4.canonical_query(canonical_query_params),
'host:' + host + '\n',
'host',
body_hash
])
else:
canonical_request = '\n'.join([
method,
path,
SignerV4.canonical_query(canonical_query_params),
'\n',
'',
body_hash
])
# Build string to sign
signing_str = '\n'.join([
'HMAC-SHA256',
format_date,
credential_scope,
hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
])
# Calculate signature
signing_key = SignerV4.get_signing_secret_key_v4(sk, date, region, service)
signature = hmac.new(signing_key, signing_str.encode('utf-8'), hashlib.sha256).hexdigest()
# Add signature to query
query['X-Signature'] = signature
# Return encoded query string
return urlencode(sorted(query.items()))
class Actions:
"""
|Action|操作类型|接口说明|
|--|--|--|
|ArkClawDataAgentDeepresearchExecuteTask|POST|执行深度研究任务|
|ArkClawDataAgentDeepresearchGetTaskStatus|GET|获取深度研究任务状态|
|ArkClawDataAgentDeepresearchGetTaskDetail|GET|获取深度研究任务详情|
|ArkClawDataAgentDeepresearchUploadFile|POST|上传数据文件|
"""
ACTION_EXECUTE_TASK = "ArkClawDataAgentDeepresearchExecuteTask"
ACTION_GET_TASK_STATUS = "ArkClawDataAgentDeepresearchGetTaskStatus"
ACTION_GET_TASK_DETAIL = "ArkClawDataAgentDeepresearchGetTaskDetail"
ACTION_UPLOAD_FILE = "ArkClawDataAgentDeepresearchUploadFile"
@dataclass
class TaskMetadata:
agent_id: int = 0
file_list: list[str] = field(default_factory=list)
enable_running_step_output: bool = False
@dataclass
class Data2DocTaskRequest:
stream: bool = True
content: str = ""
metadata: TaskMetadata = field(default_factory=TaskMetadata)
def _env(name: str) -> Optional[str]:
v = os.environ.get(name)
if v is None:
return None
v = v.strip()
return v or None
def _parse_version(v: str) -> tuple[int, int, int]:
parts = (v or "").strip().split(".")
nums: list[int] = []
for p in parts[:3]:
try:
nums.append(int(re.sub(r"\D.*$", "", p)))
except Exception:
nums.append(0)
while len(nums) < 3:
nums.append(0)
return nums[0], nums[1], nums[2]
def _get_volc_sdk_version() -> Optional[str]:
try:
from importlib.metadata import version # py3.8+
except Exception:
try:
from importlib_metadata import version # type: ignore
except Exception:
return None
try:
return version("volcengine-python-sdk")
except Exception:
return None
def _ensure_volc_sdk_min_version(min_version: str = MIN_VOLC_SDK_VERSION) -> Optional[str]:
cur = _get_volc_sdk_version()
if not cur:
return "未安装 volcengine-python-sdk。请先安装 volcengine-python-sdk>=4.0.43。"
if _parse_version(cur) < _parse_version(min_version):
return f"volcengine-python-sdk 版本过低(当前 {cur},要求 >= {min_version})。请升级以避免历史版本重试缺陷。"
return None
def _build_api_client(ak_override: Optional[str] = None, sk_override: Optional[str] = None) -> tuple[Any, str]:
"""构建已配置签名的 volcenginesdkcore.ApiClient,返回 (client, api_path)。"""
ver_err = _ensure_volc_sdk_min_version()
if ver_err:
raise RuntimeError(ver_err)
try:
import volcenginesdkcore # type: ignore
except ImportError:
raise RuntimeError(
"未安装 volcengine-python-sdk(缺少 volcenginesdkcore)。请先安装 volcengine-python-sdk>=4.0.43。"
)
ak = ak_override or _env("VOLCENGINE_ACCESS_KEY")
sk = sk_override or _env("VOLCENGINE_SECRET_KEY")
if not (ak and sk):
raise RuntimeError("未配置 Volcengine 凭证(需要同时设置 VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY)。")
# service / region / host 均有内置默认值,环境变量可覆盖(用于调试)
service = _env("VOLC_SERVICE") or DEFAULT_SERVICE
region = _env("VOLCENGINE_REGION") or DEFAULT_REGION
custom_url = _env("PUBLIC_INSIGHT_API_URL")
if custom_url:
from urllib.parse import urlsplit
p = urlsplit(custom_url)
host = p.netloc or DEFAULT_API_HOST
api_path = p.path or DEFAULT_API_PATH
scheme = p.scheme or "https"
else:
host = DEFAULT_API_HOST
api_path = DEFAULT_API_PATH
scheme = "https"
configuration = volcenginesdkcore.Configuration()
# 默认关闭 SDK 的日志输出(避免干扰用户输出)。
# 调试时可通过 OPENCLAW_DEBUG=1 打开。
configuration.logger["package_logger"].setLevel(logging.ERROR)
configuration.logger["urllib3_logger"].setLevel(logging.ERROR)
configuration.ak = ak
configuration.sk = sk
configuration.region = region
configuration.host = host
if scheme != "https":
configuration.scheme = scheme
if hasattr(configuration, "service"):
configuration.service = service
return volcenginesdkcore.ApiClient(configuration), api_path
def _do_call(
api_client: Any,
params: Dict[str, Any],
action: str,
payload: Dict[str, Any] | str,
headers: dict[str, Any] = None,
stream: bool = False,
stream_callback: callable[str, Any] = None
) -> Optional[Dict[str, Any]]:
"""通过 SDK ApiClient 向火山 OpenAPI 发起签名 POST 请求。"""
cfg = api_client.configuration
scheme = getattr(cfg, "scheme", "https") or "https"
service = getattr(cfg, "service", DEFAULT_SERVICE) or DEFAULT_SERVICE
url = f"{scheme}://{cfg.host}"
if not params:
params = {}
params.update({
"Action": action,
"Version": DEFAULT_VERSION
})
if not headers:
headers = {}
headers.update({
"Accept": "application/json",
"Host": cfg.host
})
method = "POST"
if isinstance(payload, dict):
data = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
headers["Content-Type"] = "application/json"
elif isinstance(payload, (str | bytes)) and len(payload) > 0:
data = payload
else:
data = ""
method = "GET"
SignerV4.sign("", method, headers, data, None, params,
cfg.ak, cfg.sk, cfg.region, service, None)
if method == "POST":
http_resp = requests.post(url, params=params, headers=headers, data=data, timeout=3600, stream=stream)
else:
http_resp = requests.get(url, params=params, headers=headers, timeout=3600, stream=stream)
if stream:
for line in http_resp.iter_lines():
if line and stream_callback:
stream_callback(line.decode("utf-8"))
http_resp.raise_for_status()
else:
http_resp.raise_for_status()
return http_resp.content
return None
def api_call(
action: str,
params: Dict[str, Any] = None,
payload: dict[str, Any] | str = None,
headers: dict[str, Any] = None,
ak: Optional[str] = None,
sk: Optional[str] = None,
stream: bool = False,
stream_callback: callable[str, Any] = None
) -> Dict[str, Any]:
"""封装单次API调用"""
api_client, api_path = _build_api_client(ak_override=ak, sk_override=sk)
try:
return _do_call(
api_client,
params=params,
action=action,
payload=payload,
headers=headers,
stream=stream,
stream_callback=stream_callback
)
except requests.exceptions.HTTPError as e:
print(e.response.content)
raise e
def execute_task(task_request: Data2DocTaskRequest, ak: Optional[str] = None, sk: Optional[str] = None) -> Dict[str, Any]:
"""执行深度研究任务"""
return api_call(Actions.ACTION_EXECUTE_TASK, payload=asdict(task_request), ak=ak, sk=sk)
def upload_files(file_list: list[str], ak: Optional[str] = None, sk: Optional[str] = None) -> list[str]:
"""上传数据文件"""
obj_store_keys = []
for file in file_list:
with open(file, "rb") as f:
filename = os.path.basename(file)
filesize = os.path.getsize(file)
if filename.endswith(".csv"):
filetype = "csv"
elif filename.endswith(".xlsx"):
filetype = "xlsx"
elif filename.endswith(".xls"):
filetype = "xls"
else:
raise Exception(f"暂时不支持该文件类型: {filename}")
m = MultipartEncoder(
fields={
'fileType': filetype,
'fileName': filename,
'fileSize': str(filesize),
'file': (filename, f)
}
)
data = m.to_string()
response = api_call(Actions.ACTION_UPLOAD_FILE, payload=data, headers={"Content-Type": m.content_type}, ak=ak, sk=sk)
result = json.loads(response)
obj_store_keys.append(result["data"]["storageKey"])
return obj_store_keys
def main() -> int:
ap = argparse.ArgumentParser(description="Aida OpenAPI Client (volcengine-sdk)")
ap.add_argument("--ak", default=None, help="Volcengine AccessKey(优先级高于环境变量和 .env 文件)")
ap.add_argument("--sk", default=None, help="Volcengine SecretKey(优先级高于环境变量和 .env 文件)")
ap.add_argument("--debug", default=False, action="store_true", help="输出完整错误信息(也可用 OPENCLAW_DEBUG=1)")
ap.add_argument("--files", required=True, help="用户待处理的excel/csv文件路径,绝对路径, 多个文件用逗号分隔")
# ap.add_argument("--question", required=True, help="用户的问题")
ap.add_argument("--output", required=True, help="输出文件路径,绝对路径")
args = ap.parse_args()
start = time.time()
if args.debug:
os.environ["OPENCLAW_DEBUG"] = "1"
task_request = Data2DocTaskRequest(
stream=True,
content="请帮我分析并产出文档",
metadata=TaskMetadata(
agent_id=1,
file_list=upload_files(args.files.split(","), ak=args.ak, sk=args.sk),
enable_running_step_output=True,
),
)
output_file = args.output
with open(output_file, "w+", encoding="utf-8") as out:
def stream_callback(content: str) -> None:
content = content.lstrip("data:").strip()
try:
event = json.loads(content)
except Exception as _:
return
if not isinstance(event, dict):
return
artifact_update = event.get("artifactUpdate")
if not isinstance(artifact_update, dict):
return
artifact = artifact_update.get("artifact")
if not isinstance(artifact, dict):
return
metadata = artifact.get("metadata")
if not isinstance(metadata, dict):
return
parts = artifact.get("parts", [])
if not isinstance(parts, list):
return
artifact_type = metadata.get("type")
if "deep_research_markdown_report" == artifact_type:
for part in parts:
if not isinstance(part, dict):
continue
text = part.get("text")
if not text:
continue
out.write(text)
else:
if args.debug:
print(f"任务正常运行中, 当前运行时间{int(time.time() - start)}秒, 请耐心等待~")
# run deepresearch task
api_call(Actions.ACTION_EXECUTE_TASK, payload=asdict(task_request), ak=args.ak, sk=args.sk, stream=True, stream_callback=stream_callback)
print(f"报告已生成, 存储于文件{output_file}中. 耗时: {int(time.time() - start)}秒")
return 0
if __name__ == "__main__":
raise SystemExit(main())