Skill: raccoon-data-analysis-skill (version 0.1.1)

--- name: raccoon-data-analysis description: 当用户需要使用小浣熊(Raccoon)进行数据分析、代码解释器会话管理、文件上传下载、数据可视化、办公解释器交互时使用此技能。触发词包括"小浣熊数据分析"、"Raccoon数据分析"、"代码解释器"、"办公解释器"、"数据分析会话"。 --- # 小浣熊数据分析 SKILL 你是小浣熊(Raccoon)办公解释器的操作助手。你的职责是通过调用小浣熊远程 API 或者本技能相关脚本来完成用户的数据分析需求。 ## !! 最高优先级行为规则 !! **你必须严格遵守以下规则，不得违反：** 1. **禁止本地分析数据。** 不要用 Read 工具读取用户的数据文件内容，不要用 Python/openpyxl/pandas/matplotlib 等在本地分析数据或画图。你不是本地数据分析工具。 2. **所有数据分析工作必须交给小浣熊远程 API 完成。** 你的角色是：把用户的文件上传到小浣熊 → 把用户的需求发给小浣熊 → 把小浣熊返回的结果展示给用户。 3. **Skill 加载后立即开始工作。** 不要说"技能无法启动"、"似乎无法连接"之类的话。直接检查环境变量，然后执行 API 调用。 4. **使用本 SKILL 提供的 Python 脚本调用 API。** 不要自己写 curl 命令或自己从零写 Python 代码调 API。直接运行 `scripts/` 下的现成脚本。 ## Skill 加载后的标准动作 当本 SKILL 被加载后，按以下步骤执行（不要跳过任何一步）： **步骤 1** — 检查环境变量： ```bash echo "RACCOON_API_HOST=${RACCOON_API_HOST:-未设置}" && echo "RACCOON_API_TOKEN=${RACCOON_API_TOKEN:+已设置(${#RACCOON_API_TOKEN}字符)}" ``` **步骤 2** — 如果环境变量缺失，向用户索要： ``` 请设置以下环境变量后重试： export RACCOON_API_HOST="https://code.xiaohuanxiong.com" export RACCOON_API_TOKEN="your-api-token" ``` **步骤 3** — 环境变量就绪后，根据用户需求直接执行对应的工作流。 ## 确定脚本路径 本 SKILL 的脚本位于 SKILL.md 同级的 `scripts/` 目录下。加载后先确定绝对路径： ```bash # SKILL_DIR 是 SKILL.md 所在目录，需要根据实际安装位置确定 # 常见位置示例： ls .claude/skills/raccoon-data-analysis-skill/scripts/ 2>/dev/null find ~ -path "*/raccoon-data-analysis-skill/scripts/main.py" -maxdepth 6 2>/dev/null | head -1 ``` 确定路径后，后续命令中使用绝对路径。例如： ```bash SKILL_DIR=[REDACTED_SECRET] python3 "$SKILL_DIR/scripts/main.py" analyze --file data.xlsx --prompt "分析数据" ``` ## 工作流 ### 流程一：用户提供了文件要分析（最常见场景） 当用户说"分析这个文件"、"画个图"、"帮我看看这个 Excel"时： ```bash python3 "$SKILL_DIR/scripts/main.py" analyze \ --file "/absolute/path/to/用户的文件.xlsx" \ --prompt "用户的具体分析需求" \ --output ./output ``` **注意：`--file` 参数必须使用用户文件的绝对路径。** 这一条命令会自动完成全部流程：创建会话 → 上传文件到小浣熊 → 发起对话 → 流式接收结果 → 下载生成物到本地。 ### 流程二：纯对话（无文件） 当用户只是要求计算、编程、生成数据时： ```bash python3 "$SKILL_DIR/scripts/main.py" analyze \ --prompt "用Python计算1到100的素数之和" ``` ### 流程三：需要精细控制（多轮对话、分步操作） ```bash python3 -c " import sys sys.path.insert(0, '$SKILL_DIR/scripts') from main import RaccoonClient client = RaccoonClient() # 创建会话 session = client.create_session('我的分析') sid = session['id'] print(f'会话ID: {sid}') # 上传文件 file_id = client.upload_temp_file('/absolute/path/to/file.xlsx') print(f'文件ID: {file_id}') # 第一轮对话 result = client.chat(sid, '分析数据趋势', upload_file_ids=[file_id]) # 第二轮对话（追问） result2 = client.chat(sid, '请用饼图展示占比') # 下载生成物 downloaded = client.download_artifacts(sid, output_dir='./output') for p in downloaded: print(f'已下载: {p}') " ``` ### 流程四：查询已有会话 ```bash python3 -c " import sys sys.path.insert(0, '$SKILL_DIR/scripts') from main import RaccoonClient client = RaccoonClient() for s in client.list_sessions()[:10]: print(f\"{s['id']} | {s.get('title', '无标题')}\") " ``` ## 完整的示例对话 ### 示例：用户上传 Excel 让小浣熊画雷达图 **用户**: @雷达图测试数据.xlsx 请绘制雷达图，展示学生心理状态各维度数据 **助手的正确行为**（你必须这样做）： 1. 不要读取 xlsx 文件内容 2. 不要本地安装 matplotlib 3. 直接调用小浣熊 API： ```bash # 先确认环境 echo "RACCOON_API_HOST=${RACCOON_API_HOST:-未设置}" && echo "RACCOON_API_TOKEN=${RACCOON_API_TOKEN:+已设置}" # 调用小浣熊分析（文件上传到远程，远程执行代码画图） python3 "$SKILL_DIR/scripts/main.py" analyze \ --file "/absolute/path/to/雷达图测试数据.xlsx" \ --prompt "这张表格为某县学生心理状态测评的各维度数据，请绘制雷达图，展示各维度数值，包括某县水平、平均值、标准差" \ --output ./output \ --show-code ``` 4. 脚本会自动完成所有步骤并下载生成的雷达图到 `./output/` 5. 将下载的图片路径告知用户，或用 `open` 命令打开 **助手的错误行为**（你绝对不能这样做）： - ~~用 Read 工具读取 xlsx~~ - ~~用 `python3 -c "import openpyxl..."` 本地解析 Excel~~ - ~~用 `pip3 install matplotlib` 然后本地画图~~ - ~~说"技能无法启动"或"无法连接到办公解释器"~~ ## 生成物展示 分析完成后，生成物（图片/文件）会下载到 `--output` 目录。展示给用户： ```bash # macOS 打开图片 open ./output/chart.png # 或列出所有生成物 ls -la ./output/ ``` ## 错误处理 ### 业务错误码 | 错误码 | 含义 | 处理建议 | |--------|------|---------| | 100012 | 会话ID不存在 | 重新创建会话 | | 100015 | 会话沙盒资源不足 | 删除旧会话后重试 | | 100016/100017 | 文件数量/大小超限 | 减少文件 | | 100023 | 文件不存在 | 确认文件路径 | | 200103 | 请求速率超限 | 等待后重试（脚本自动重试） | | 200506 | 当日问题超限 | 次日再试 | | 300001 | 模型不存在 | 检查 model 参数 | ### 沙盒运行时错误 | 错误 | 处理 | |------|------| | `context canceled` | 等待 5-10s 后重试，反复出现则删除会话重建 | | 执行超时 | 简化任务拆分对话 | | `MemoryError` | 减小数据量 | | SSE 中途断开 | 通过 messages 接口查询已有结果 | 脚本已内置自动重试（3次，间隔 5/10/20s）。 ### 认证错误 | HTTP 状态 | 处理 | |-----------|------| | 401 | Token 无效或过期，向用户索要新 Token | | 429 | 速率超限，等待后重试 | ## SSE 流式响应说明（仅供理解，脚本已自动处理） 办公解释器返回的 SSE 数据 stage 类型： - `generate` — 文本回复 - `code` — 生成的代码 - `execute`/`execution` — 代码执行结果 - `image` — 图片 - `ocr` — OCR 识别 数据分析接口的 finish_reason： - `stop` — 正常结束 - `text`/`code` — 需要继续请求 - `length`/`sensitive`/`context` — 异常结束 ## 关键注意事项 - **不要本地分析数据，所有分析交给小浣熊远程 API** - 所有接口需 `Authorization: Bearer $RACCOON_API_TOKEN` - 对话接口只支持 SSE 流式返回，脚本已处理 - 临时文件上传后 7 天过期 - `s3_url` 预签名 URL 约 30 分钟过期 ## 参考文档 - `references/API_REFERENCE.md` — 完整 API 参考 - `references/CHEATSHEET.md` — 速查表

path: scripts/main.py
content: #!/usr/bin/env python3
"""
小浣熊数据分析 - 统一命令行工具

用法:
    python3 main.py analyze --file data.xlsx --prompt "分析数据"
    python3 main.py analyze --prompt "计算斐波那契数列"
    python3 main.py auth-check
    python3 main.py example [1|2|3]
    python3 main.py sessions --list
"""

import argparse
import json
import os
import platform
import subprocess
import sys
import time
import uuid
import warnings

warnings.filterwarnings('ignore', message='urllib3 v2 only supports OpenSSL 1.1.1+')

import requests


# ================================================================
# 核心客户端类
# ================================================================

class RaccoonClient:
    """小浣熊 OpenAPI 客户端，内置 SSE 流式解析、重试和生成物下载"""

    MAX_RETRIES = 3
    RETRY_DELAYS = [5, 10, 20]
    STREAM_TIMEOUT = 300
    REQUEST_TIMEOUT = 30

    def __init__(self, host=None, token=None):
        self.host = host or os.environ.get("RACCOON_API_HOST", "")
        self.token = token or os.environ.get("RACCOON_API_TOKEN", "")
        if not self.host:
            print("错误: 未设置 RACCOON_API_HOST 环境变量")
            print("请运行: export RACCOON_API_HOST='https://code.xiaohuanxiong.com'")
            sys.exit(1)
        if not self.[REDACTED_SECRET]"错误: 未设置 RACCOON_API_TOKEN 环境变量")
            print("请运行: export RACCOON_API_TOKEN='your-api-token'")
            sys.exit(1)

    @property
    def _headers(self):
        return {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.token}",
        }

    @property
    def _upload_headers(self):
        return {"Authorization": f"Bearer {self.token}"}

    def _request(self, method, path, json_data=None, params=None, timeout=None):
        url = f"{self.host}{path}"
        timeout = timeout or self.REQUEST_TIMEOUT
        resp = requests.request(
            method, url, headers=self._headers,
            json=json_data, params=params, timeout=timeout,
        )
        if resp.status_code != 200:
            raise APIError(resp.status_code, resp.text)
        body = resp.json()
        if body.get("code") != 0:
            raise APIError(body.get("code"), body.get("message", "unknown error"))
        return body.get("data", {})

    def create_session(self, name="数据分析会话"):
        return self._request("POST", "/api/open/office/v2/sessions", {"name": name})

    def get_session(self, session_id):
        return self._request("GET", f"/api/open/office/v2/sessions/{session_id}")

    def list_sessions(self, sort="-updated_at", omit_empty=True):
        data = self._request("GET", "/api/open/office/v2/sessions", params={
            "sort": sort, "omit_empty_title": omit_empty,
        })
        return data.get("sessions", [])

    def delete_session(self, session_id):
        return self._request("DELETE", f"/api/open/office/v2/sessions/{session_id}")

    def upload_temp_file(self, file_path):
        """上传临时文件，返回 file_id (int)"""
        batch_id = str(uuid.uuid4())
        url = f"{self.host}/api/open/office/v2/sessions/default_session/{batch_id}/files"
        filename = os.path.basename(file_path)
        with open(file_path, "rb") as f:
            resp = requests.post(
                url, headers=self._upload_headers,
                files={"file": (filename, f)}, timeout=60,
            )
        if resp.status_code != 200:
            raise APIError(resp.status_code, resp.text)
        body = resp.json()
        file_list = body.get("data", {}).get("file_list", [])
        if not file_list:
            raise APIError(0, "上传文件失败: 返回空 file_list")
        return file_list[0]["id"]

    def list_files(self):
        return self._request("GET", "/api/open/office/v2/sessions/files").get("file_list", [])

    def get_file_info(self, session_id, file_path):
        return self._request("GET", f"/api/open/office/v2/sessions/{session_id}/file_info",
                             params={"file_path": file_path})

    def delete_file(self, file_id):
        return self._request("DELETE", f"/api/open/office/v2/sessions/default_session/{file_id}")

    def chat(self, session_id, content, upload_file_ids=None, files=None,
             verbose=True, deep_think=False, temperature=1, retries=None):
        """发起对话并完整解析 SSE 响应，返回结构化结果"""
        retries = retries if retries is not None else self.MAX_RETRIES
        data = {
            "content": content,
            "verbose": verbose,
            "enable_web_search": False,
            "deep_think": deep_think,
            "temperature": temperature,
            "message_uuid": str(uuid.uuid4()),
            "edit": 0,
        }
        if upload_file_ids:
            data["upload_file_id"] = upload_file_ids
        if files:
            data["files"] = files

        last_error = None
        for attempt in range(retries + 1):
            try:
                return self._stream_chat(session_id, data)
            except (RetryableError, requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
                last_error = e
                if attempt < retries:
                    delay = self.RETRY_DELAYS[min(attempt, len(self.RETRY_DELAYS) - 1)]
                    print(f"[重试 {attempt+1}/{retries}] {e} - 等待 {delay}s...", file=sys.stderr)
                    time.sleep(delay)
        raise last_error

    def _stream_chat(self, session_id, data):
        url = f"{self.host}/api/open/office/v2/sessions/{session_id}/chat/conversations"
        resp = requests.post(
            url, headers=self._headers, json=data,
            stream=True, timeout=self.STREAM_TIMEOUT,
        )
        if resp.status_code == 429:
            raise RetryableError(f"HTTP 429: 请求速率超限")
        if resp.status_code == 504:
            raise RetryableError(f"HTTP 504: 推理代理超时")
        if resp.status_code != 200:
            raise APIError(resp.status_code, resp.text[:300])

        result = ChatResult()
        current_stage = ""

        for line in resp.iter_lines():
            if not line:
                continue
            line_str = line.decode("utf-8") if isinstance(line, bytes) else line
            if not line_str.startswith("data:"):
                continue
            json_str = line_str[5:].strip()
            if json_str == "[DONE]":
                result.done = True
                break
            try:
                obj = json.loads(json_str)
            except json.JSONDecodeError:
                continue

            status_code = obj.get("status", {}).get("code", 0)
            if status_code != 0:
                msg = obj.get("status", {}).get("message", "")
                raise APIError(status_code, msg)

            stage = obj.get("stage", "")
            delta = obj.get("data", {}).get("delta", "")
            finish_reason = obj.get("data", {}).get("finish_reason", "")
            session_id_resp = obj.get("data", {}).get("session_id", "")
            turn_id = obj.get("data", {}).get("turn_id", "")

            if session_id_resp:
                result.session_id = session_id_resp
            if turn_id:
                result.turn_id = turn_id
            if finish_reason:
                result.finish_reason = finish_reason

            if not delta:
                continue

            current_stage = stage or current_stage

            if current_stage == "generate":
                result.text += delta
                print(delta, end="", flush=True)
            elif current_stage == "code":
                result.code_buffer += delta
            elif current_stage in ("execute", "execution"):
                if "context canceled" in delta:
                    raise RetryableError(f"沙盒执行错误: context canceled")
                result.execution_buffer += delta
            elif current_stage == "image":
                result.image_buffer += delta
            elif current_stage == "ocr":
                result.ocr_buffer += delta

            if stage and stage != current_stage:
                result.flush_buffer(current_stage)
                current_stage = stage

        result.flush_buffer(current_stage)
        if result.text:
            print()

        if not result.done:
            raise RetryableError("SSE 流中途断开（未收到 [DONE]）")

        return result

    def data_analysis(self, content, model="SenseChat-Code-DataAnalysis-Excel",
                      temperature=0.5, retries=None):
        """无状态数据分析，返回 ChatResult"""
        retries = retries if retries is not None else self.MAX_RETRIES
        data = {
            "model": model,
            "stream": True,
            "temperature": temperature,
            "top_p": 0.9,
            "repetition_penalty": 1.02,
            "stop": ["
", "
"],
            "messages": [{"role": "user", "type": "text", "content": content}],
        }

        last_error = None
        for attempt in range(retries + 1):
            try:
                return self._stream_data_analysis(data)
            except (RetryableError, requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
                last_error = e
                if attempt < retries:
                    delay = self.RETRY_DELAYS[min(attempt, len(self.RETRY_DELAYS) - 1)]
                    print(f"[重试 {attempt+1}/{retries}] {e} - 等待 {delay}s...", file=sys.stderr)
                    time.sleep(delay)
        raise last_error

    def _stream_data_analysis(self, data):
        url = f"{self.host}/api/open/llm/v1/data-analysis/chat-completions"
        resp = requests.post(
            url, headers=self._headers, json=data,
            stream=True, timeout=self.STREAM_TIMEOUT,
        )
        if resp.status_code == 429:
            raise RetryableError(f"HTTP 429: 请求速率超限")
        if resp.status_code == 504:
            raise RetryableError(f"HTTP 504: 推理代理超时")
        if resp.status_code != 200:
            raise APIError(resp.status_code, resp.text[:300])

        result = ChatResult()

        for line in resp.iter_lines():
            if not line:
                continue
            line_str = line.decode("utf-8") if isinstance(line, bytes) else line
            if not line_str.startswith("data:"):
                continue
            json_str = line_str[5:].strip()
            if json_str == "[DONE]":
                result.done = True
                break
            try:
                obj = json.loads(json_str)
            except json.JSONDecodeError:
                continue

            choices = obj.get("data", {}).get("choices", [])
            for choice in choices:
                delta = choice.get("delta", "")
                finish_reason = choice.get("finish_reason", "")
                msg_type = choice.get("type", "")

                if finish_reason:
                    result.finish_reason = finish_reason
                if delta:
                    if msg_type == "code":
                        result.code_buffer += delta
                    else:
                        result.text += delta
                        print(delta, end="", flush=True)

            usage = obj.get("data", {}).get("usage", {})
            if usage.get("total_tokens"):
                result.usage = usage

        result.flush_buffer("code")
        if result.text:
            print()

        if not result.done:
            raise RetryableError("SSE 流中途断开（未收到 [DONE]）")

        return result

    def get_artifacts(self, session_id):
        data = self._request("GET", f"/api/open/office/v2/sessions/{session_id}/artifacts")
        return data.get("artifacts", [])

    def download_artifacts(self, session_id, output_dir="./output"):
        """下载所有生成物到本地，返回文件路径列表"""
        artifacts = self.get_artifacts(session_id)
        if not artifacts:
            print("无生成物")
            return []

        os.makedirs(output_dir, exist_ok=True)
        downloaded = []
        for art in artifacts:
            s3_url = art.get("s3_url", "")
            filename = art.get("filename", f"artifact_{art.get('timestamp', 'unknown')}")
            if not s3_url:
                continue
            local_path = os.path.join(output_dir, filename)
            try:
                resp = requests.get(s3_url, timeout=60)
                if resp.status_code == 200:
                    with open(local_path, "wb") as f:
                        f.write(resp.content)
                    downloaded.append(local_path)
                    print(f"  已下载: {local_path} ({art.get('type', 'unknown')})")
                else:
                    print(f"  下载失败 [{resp.status_code}]: {filename}", file=sys.stderr)
            except Exception as e:
                print(f"  下载异常: {filename} - {e}", file=sys.stderr)
        return downloaded

    def get_suggestions(self, session_id):
        try:
            data = self._request("GET", f"/api/open/office/v2/sessions/{session_id}/chat/suggestions")
            return data.get("suggestions", [])
        except APIError as e:
            print(f"  获取建议失败 [{e.code}]: {e.message}", file=sys.stderr)
            return []

    def get_messages(self, session_id, limit=20, offset=0):
        data = self._request(
            "GET", f"/api/open/office/v2/sessions/{session_id}/messages",
            params={"paging.limit": limit, "paging.offset": offset, "verbose": True},
        )
        return data.get("messages", [])


class ChatResult:
    """对话结果容器，按 stage 分类存储"""

    def __init__(self):
        self.text = ""
        self.code = []
        self.execution = []
        self.images = []
        self.ocr = []
        self.finish_reason = ""
        self.session_id = ""
        self.turn_id = ""
        self.usage = {}
        self.done = False
        self.code_buffer = ""
        self.execution_buffer = ""
        self.image_buffer = ""
        self.ocr_buffer = ""

    def flush_buffer(self, stage):
        if stage == "code" and self.code_buffer:
            self.code.append(self.code_buffer)
            self.code_buffer = ""
        elif stage in ("execute", "execution") and self.execution_buffer:
            self.execution.append(self.execution_buffer)
            self.execution_buffer = ""
        elif stage == "image" and self.image_buffer:
            self.images.append(self.image_buffer)
            self.image_buffer = ""
        elif stage == "ocr" and self.ocr_buffer:
            self.ocr.append(self.ocr_buffer)
            self.ocr_buffer = ""

    def summary(self):
        parts = []
        if self.text:
            parts.append(f"文本回复: {len(self.text)} 字符")
        if self.code:
            parts.append(f"代码段: {len(self.code)} 个")
        if self.execution:
            parts.append(f"执行结果: {len(self.execution)} 个")
        if self.images:
            parts.append(f"图片: {len(self.images)} 个")
        if self.finish_reason:
            parts.append(f"结束原因: {self.finish_reason}")
        return " | ".join(parts) if parts else "无内容"

    def __repr__(self):
        return f"<ChatResult {self.summary()}>"


class APIError(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message
        super().__init__(f"API错误 [{code}]: {message}")


class RetryableError(Exception):
    pass


# ================================================================
# 工具函数
# ================================================================

def check_environment():
    """检查环境变量配置"""
    host = os.environ.get("RACCOON_API_HOST", "")
    [REDACTED_SECRET]"RACCOON_API_TOKEN", "")

    if not host:
        print("错误: 未设置 RACCOON_API_HOST 环境变量", file=sys.stderr)
        print("请运行: export RACCOON_API_HOST='https://code.xiaohuanxiong.com'", file=sys.stderr)
        return False
    if not [REDACTED_SECRET]"错误: 未设置 RACCOON_API_TOKEN 环境变量", file=sys.stderr)
        print("请运行: export RACCOON_API_TOKEN='your-api-token'", file=sys.stderr)
        return False

    print(f"✓ RACCOON_API_HOST: {host}")
    print(f"✓ RACCOON_API_TOKEN: 已设置({len(token)}字符)")
    return True


def open_file(path):
    """跨平台打开文件"""
    system = platform.system()
    try:
        if system == "Darwin":
            subprocess.run(["open", path], check=False)
        elif system == "Linux":
            subprocess.run(["xdg-open", path], check=False)
    except Exception:
        pass


# ================================================================
# 子命令：分析文件/对话
# ================================================================

def cmd_analyze(args):
    """文件分析或纯对话功能"""
    if not check_environment():
        sys.exit(1)

    if args.file and not os.path.isfile(args.file):
        print(f"错误: 文件不存在: {args.file}", file=sys.stderr)
        sys.exit(1)

    try:
        client = RaccoonClient()

        session_name = args.session_name or f"分析: {os.path.basename(args.file) if args.file else args.prompt[:20]}"
        print(f"[1/5] 创建会话: {session_name}")
        session = client.create_session(session_name)
        session_id = session["id"]
        print(f"      会话ID: {session_id}")

        upload_file_ids = []
        if args.file:
            print(f"[2/5] 上传文件: {args.file}")
            file_id = client.upload_temp_file(args.file)
            upload_file_ids = [file_id]
            print(f"      文件ID: {file_id}")
        else:
            print("[2/5] 跳过文件上传（纯对话模式）")

        print(f"[3/5] 发起对话: {args.prompt}")
        print("-" * 60)
        result = client.chat(
            session_id, args.prompt,
            upload_file_ids=upload_file_ids if upload_file_ids else None,
        )

        print("-" * 60)
        print(f"[对话完成] {result.summary()}")

        if args.show_code and result.code:
            print("\n--- 执行的代码 ---")
            for i, code in enumerate(result.code, 1):
                print(f"\n# 代码段 {i}:")
                print(code)

        if args.show_execution and result.execution:
            print("\n--- 执行结果 ---")
            for i, exe in enumerate(result.execution, 1):
                print(f"\n# 结果 {i}:")
                print(exe)

        if not args.no_download:
            print(f"\n[4/5] 获取生成物...")
            downloaded = client.download_artifacts(session_id, output_dir=args.output)
            if downloaded:
                print(f"      共下载 {len(downloaded)} 个文件到 {args.output}/")
                if not args.no_open:
                    images = [p for p in downloaded if p.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.gif'))]
                    if images:
                        open_file(images[0])
            else:
                print("      无生成物")
        else:
            print("\n[4/5] 跳过生成物下载")

        print("\n[5/5] 获取追问建议...")
        suggestions = client.get_suggestions(session_id)
        if suggestions:
            for i, s in enumerate(suggestions, 1):
                print(f"      建议{i}: {s}")
        else:
            print("      无追问建议")

        print(f"\n完成! 会话ID: {session_id}")

    except RetryableError as e:
        print(f"\n对话失败（已重试）: {e}", file=sys.stderr)
        sys.exit(1)
    except APIError as e:
        print(f"\nAPI 错误: {e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"\n未预期错误: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc()
        sys.exit(1)


# ================================================================
# 子命令：认证检查
# ================================================================

def cmd_auth_check(args):
    """验证认证配置"""
    host = os.environ.get("RACCOON_API_HOST", "")
    [REDACTED_SECRET]"RACCOON_API_TOKEN", "")

    if not host:
        print("错误: 未设置 RACCOON_API_HOST 环境变量")
        print("请运行: export RACCOON_API_HOST='https://code.xiaohuanxiong.com'")
        sys.exit(1)
    if not [REDACTED_SECRET]"错误: 未设置 RACCOON_API_TOKEN 环境变量")
        print("请运行: export RACCOON_API_TOKEN='your-api-token'")
        sys.exit(1)

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    url = f"{host}/api/open/office/v2/sessions"
    try:
        resp = requests.get(url, headers=headers, params={"paging.limit": 1}, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            if data.get("code") == 0:
                print("认证验证成功!")
                print(f"  API Host: {host}")
                print(f"  [REDACTED_SECRET]")
                sessions = data.get("data", {}).get("sessions", [])
                print(f"  现有会话数: {data.get('data', {}).get('paging', {}).get('total', len(sessions))}")
                return True
        if resp.status_code == 401:
            print("认证失败: Token 无效或已过期")
        else:
            print(f"请求失败: HTTP {resp.status_code}")
            print(f"响应: {resp.text[:200]}")
    except requests.exceptions.ConnectionError:
        print(f"连接失败: 无法连接到 {host}")
    except Exception as e:
        print(f"验证异常: {e}")

    return False


# ================================================================
# 子命令：会话管理
# ================================================================

def cmd_sessions(args):
    """会话管理"""
    if not check_environment():
        sys.exit(1)

    try:
        client = RaccoonClient()

        if args.list:
            sessions = client.list_sessions()
            print(f"会话总数: {len(sessions)}")
            for s in sessions[:10]:
                print(f"  ID: {s['id']}, 标题: {s.get('title', '无标题')}, 更新: {s.get('updated_at', '')}")

        elif args.delete:
            print(f"删除会话: {args.delete}")
            client.delete_session(args.delete)
            print("删除成功")

    except APIError as e:
        print(f"API 错误: {e}", file=sys.stderr)
        sys.exit(1)


# ================================================================
# 子命令：使用示例
# ================================================================

def stream_request_example(host, headers, method, path, data=None, files=None):
    """发送流式请求并打印增量输出（示例用）"""
    url = f"{host}{path}"
    if method == "POST" and files:
        h = {k: v for k, v in headers.items() if k != "Content-Type"}
        resp = requests.post(url, headers=h, files=files, stream=True, timeout=120)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=data, stream=True, timeout=120)
    else:
        resp = requests.get(url, headers=headers, params=data, timeout=30)
        return resp.json().get("data") if resp.status_code == 200 else None

    if resp.status_code != 200:
        print(f"HTTP {resp.status_code}: {resp.text[:200]}")
        return None

    result_text = ""
    for line in resp.iter_lines():
        if not line:
            continue
        line_str = line.decode("utf-8") if isinstance(line, bytes) else line
        if not line_str.startswith("data:"):
            continue
        json_str = line_str[5:].strip()
        if json_str == "[DONE]":
            break
        try:
            obj = json.loads(json_str)
            stage = obj.get("stage", "")
            delta = obj.get("data", {}).get("delta", "")
            if delta and stage in ("generate", "image"):
                print(delta, end="", flush=True)
                result_text += delta
        except json.JSONDecodeError:
            pass
    print()
    return result_text


def example_data_analysis():
    """无状态数据分析示例"""
    print("=" * 60)
    print("示例1: 无状态数据分析")
    print("=" * 60)

    host = os.environ.get("RACCOON_API_HOST", "")
    [REDACTED_SECRET]"RACCOON_API_TOKEN", "")
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    data = {
        "model": "SenseChat-Code-DataAnalysis-Excel",
        "stream": True,
        "temperature": 0.5,
        "top_p": 0.9,
        "repetition_penalty": 1.02,
        "stop": ["
", "
"],
        "messages": [
            {
                "role": "user",
                "type": "text",
                "content": "用Python生成一个包含10个随机数的列表，计算均值和标准差",
            }
        ],
    }

    stream_request_example(host, headers, "POST", "/api/open/llm/v1/data-analysis/chat-completions", data)


def example_office_interpreter():
    """办公解释器完整流程示例"""
    print("=" * 60)
    print("示例2: 办公解释器完整流程")
    print("=" * 60)

    host = os.environ.get("RACCOON_API_HOST", "")
    [REDACTED_SECRET]"RACCOON_API_TOKEN", "")
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    print("\n[步骤1] 创建会话...")
    resp = requests.post(
        f"{host}/api/open/office/v2/sessions",
        headers=headers,
        json={"name": "Python示例会话"},
        timeout=10,
    )
    session_data = resp.json().get("data", {})
    session_id = session_data.get("id")
    if not session_id:
        print("创建会话失败")
        return
    print(f"  会话ID: {session_id}")

    print("\n[步骤2] 上传临时文件...")
    batch_id = str(uuid.uuid4())
    csv_content = "name,age,score\nAlice,25,88\nBob,30,92\nCarol,28,85\nDave,35,95\nEve,22,78"
    upload_headers = {k: v for k, v in headers.items() if k != "Content-Type"}
    resp = requests.post(
        f"{host}/api/open/office/v2/sessions/default_session/{batch_id}/files",
        headers=upload_headers,
        files={"file": ("sample_data.csv", csv_content.encode(), "text/csv")},
        timeout=30,
    )
    upload_result = resp.json().get("data", {})
    file_list = upload_result.get("file_list", [])
    if not file_list:
        print("上传文件失败")
        return
    file_id = file_list[0]["id"]
    print(f"  文件ID: {file_id}, 文件名: {file_list[0]['file_name']}")

    print("\n[步骤3] 发起对话分析...")
    chat_data = {
        "content": "帮我分析这份数据，画一个成绩分布柱状图",
        "verbose": True,
        "enable_web_search": False,
        "deep_think": False,
        "message_uuid": str(uuid.uuid4()),
        "upload_file_id": [file_id],
    }
    stream_request_example(
        host, headers, "POST",
        f"/api/open/office/v2/sessions/{session_id}/chat/conversations",
        chat_data,
    )

    print(f"\n会话ID: {session_id}")


def example_session_management():
    """会话管理示例"""
    print("=" * 60)
    print("示例3: 会话管理")
    print("=" * 60)

    host = os.environ.get("RACCOON_API_HOST", "")
    [REDACTED_SECRET]"RACCOON_API_TOKEN", "")
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    print("\n[列出会话]")
    resp = requests.get(
        f"{host}/api/open/office/v2/sessions",
        headers=headers,
        params={"sort": "-updated_at", "omit_empty_title": True},
        timeout=10,
    )
    data = resp.json().get("data", {})
    sessions = data.get("sessions", [])
    total = data.get("paging", {}).get("total", len(sessions))
    print(f"  会话总数: {total}")
    for s in sessions[:5]:
        print(f"  ID: {s['id']}, 标题: {s.get('title', '无标题')}, 更新: {s.get('updated_at', '')}")


def cmd_example(args):
    """运行使用示例"""
    if not check_environment():
        sys.exit(1)

    examples = {
        "1": ("无状态数据分析", example_data_analysis),
        "2": ("办公解释器完整流程", example_office_interpreter),
        "3": ("会话管理", example_session_management),
    }

    if args.example_id and args.example_id in examples:
        examples[args.example_id][1]()
    else:
        print("小浣熊数据分析 API 示例")
        for k, (desc, _) in examples.items():
            print(f"  {k} - {desc}")
        print("\n运行全部示例...")
        for _, fn in examples.values():
            fn()
            print()


# ================================================================
# 主程序
# ================================================================

def main():
    parser = argparse.ArgumentParser(description="小浣熊数据分析 - 统一命令行工具")
    subparsers = parser.add_subparsers(dest="command", help="可用命令")

    analyze_parser = subparsers.add_parser("analyze", help="分析文件或进行对话")
    analyze_parser.add_argument("--file", "-f", help="待分析的文件路径")
    analyze_parser.add_argument("--prompt", "-p", required=True, help="分析需求描述")
    analyze_parser.add_argument("--output", "-o", default="./output", help="生成物输出目录")
    analyze_parser.add_argument("--session-name", help="会话名称")
    analyze_parser.add_argument("--no-download", action="store_true", help="不下载生成物")
    analyze_parser.add_argument("--no-open", action="store_true", help="不自动打开生成的图片")
    analyze_parser.add_argument("--show-code", action="store_true", help="显示执行的代码")
    analyze_parser.add_argument("--show-execution", action="store_true", help="显示代码执行结果")

    subparsers.add_parser("auth-check", help="检查认证配置")

    sessions_parser = subparsers.add_parser("sessions", help="会话管理")
    sessions_parser.add_argument("--list", action="store_true", help="列出会话")
    sessions_parser.add_argument("--delete", help="删除指定会话ID")

    example_parser = subparsers.add_parser("example", help="运行使用示例")
    example_parser.add_argument("example_id", nargs="?", choices=["1", "2", "3"], help="示例编号")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        return

    if args.command == "analyze":
        cmd_analyze(args)
    elif args.command == "auth-check":
        cmd_auth_check(args)
    elif args.command == "sessions":
        cmd_sessions(args)
    elif args.command == "example":
        cmd_example(args)


if __name__ == "__main__":
    main()


path: references/API_REFERENCE.md
content: # 小浣熊数据分析 API 参考文档

## 认证方式

所有接口均使用 Bearer Token 认证：

```
Authorization: Bearer $RACCOON_API_TOKEN
```

## 通用响应格式

### 成功响应（非流式）

```json
{
  "code": 0,
  "message": "success",
  "data": { ... }
}
```

### 流式响应（SSE）

每行格式：`data:{JSON}`，以 `data:[DONE]` 结束。

### 错误响应

网关层：
```json
{"code": 100001, "message": "empty_params_error", "details": "..."}
```

业务层：
```json
{"error": {"code": 3, "message": "string", "details": []}}
```

---

## 一、数据分析接口

### 数据分析对话

创建数据分析模型响应，支持流式输出。

- **路径**: `POST /api/open/llm/v1/data-analysis/chat-completions`
- **Content-Type**: `application/json`

#### 请求参数

| 参数 | 类型 | 必须 | 默认值 | 说明 |
|------|------|------|--------|------|
| model | string | 是 | - | 模型ID，如 `SenseChat-Code-DataAnalysis-Excel` |
| messages | object[] | 是 | - | 对话上下文数组 |
| stream | boolean | 否 | true | 只支持 true |
| n | int | 否 | 1 | 生成回复数量 [1,4] |
| max_new_tokens | int | 否 | 自适应 | 最大生成token数 |
| temperature | float | 否 | 0.5 | 温度 [0.1,2] |
| top_p | float | 否 | 0.9 | 核采样 (0,1) |
| repetition_penalty | float | 否 | 1 | 重复惩罚 [1,2]，推荐 [1,1.2] |
| stop | string[] | 否 | `["<\|endofblock\|>","<\|endofmessage\|>"]` | 停止符 |

#### messages 对象

| 参数 | 类型 | 必须 | 说明 |
|------|------|------|------|
| role | string | 是 | `system` / `user` / `assistant` |
| type | string | 是 | 取决于 role：`system` → `text`；`user` → `text`/`file`/`description`；`assistant` → `text`/`code`/`execution`/`summary` |
| content | string | 是 | 消息内容 |

#### 响应参数（流式）

| 参数 | 类型 | 说明 |
|------|------|------|
| data.id | string | 消息ID |
| data.choices[].index | int | 回复序号 |
| data.choices[].role | string | 角色 |
| data.choices[].delta | string | 增量内容 |
| data.choices[].type | string | 内容类型：`text` / `code` |
| data.choices[].finish_reason | string | 结束原因（见下） |
| data.usage | object | token用量 |

#### finish_reason 说明

| 值 | 含义 | 处理方式 |
|----|------|---------|
| `text` | 触发会话停止符 | 需要继续请求 |
| `code` | 触发代码停止符 | 需要执行代码后将结果拼接回 messages 继续请求 |
| `stop` | 正常结束 | 对话结束 |
| `length` | 达到最大生成长度 | 对话结束 |
| `sensitive` | 触发敏感词 | 对话结束 |
| `context` | 触发上下文长度限制 | 对话结束 |

---

## 二、办公解释器接口

### 2.1 会话管理

#### 创建会话

- **路径**: `POST /api/open/office/v2/sessions`
- **Content-Type**: `application/json`

| 请求参数 | 类型 | 说明 |
|----------|------|------|
| name | string | 可选，会话名称，不超过70字符 |
| description | string | 可选，会话描述，不超过70字符 |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| id | string | 会话ID |
| title | string | 会话标题 |
| created_at | string | 创建时间 |
| updated_at | string | 更新时间 |
| last_chatted_at | string | 最后聊天时间 |
| language | string | 会话语种 |

#### 获取会话信息

- **路径**: `GET /api/open/office/v2/sessions/{session_id}`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| org_user_id | string | 可选，查询其他用户的会话 |

响应同创建会话。

#### 获取会话列表

- **路径**: `GET /api/open/office/v2/sessions`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| omit_empty_title | boolean | 是否忽略空标题会话 |
| query_title | string | 按标题搜索 |
| org_user_id | integer | 查询其他用户的会话 |
| sort | string | 排序：`+created_at` / `-created_at` / `+updated_at` / `-updated_at` |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| sessions | array | 会话列表 |
| paging | object | 分页信息（limit/offset/total） |

#### 删除会话

- **路径**: `DELETE /api/open/office/v2/sessions/{session_id}`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| org_user_id | string | 可选 |

响应返回被删除会话的信息。

---

### 2.2 对话交互

#### 新增用户对话

- **路径**: `POST /api/open/office/v2/sessions/{session_id}/chat/conversations`
- **Content-Type**: `application/json`
- **返回方式**: 流式 SSE

| 请求参数 | 类型 | 说明 |
|----------|------|------|
| content | string | 必选，对话内容 |
| verbose | boolean | 可选，是否返回代码执行中间信息 |
| deep_think | boolean | 可选，是否开启深度思考 |
| enable_web_search | boolean | 可选，是否开启联网搜索（默认 false） |
| temperature | number | 可选，温度 [0.1,2]，默认 1 |
| edit | integer | 可选，编辑标识（0=普通，其他=编辑消息） |
| message_uuid | string | 可选，用户消息UUID |
| files | string[] | 可选，会话内文件ID列表 |
| upload_file_id | integer[] | 可选，临时上传文件ID列表 |
| user_at_commands | object | 可选，@知识库操作 |
| user_at_commands.asset_filters[].asset_code | string | 知识库代码 |
| user_at_commands.asset_filters[].file_uuid | string | 文件UUID |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| stage | string | 阶段：`ocr`/`generate`/`code`/`execute`/`execution`/`image` |
| data.delta | string | 增量文本 |
| data.finish_reason | string | 结束原因 |
| data.session_id | string | 会话ID |
| data.turn_id | string | 对话轮次ID |
| status.code | integer | 状态码 |
| status.message | string | 状态消息 |

#### 获取追问建议

- **路径**: `GET /api/open/office/v2/sessions/{session_id}/chat/suggestions`

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| suggestions | string[] | 建议列表 |

#### 获取消息列表

- **路径**: `GET /api/open/office/v2/sessions/{session_id}/messages`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| paging.limit | integer | 每页条数 [1,100]，默认 20 |
| paging.offset | integer | 偏移量，默认 0 |
| org_user_id | integer | 可选 |
| verbose | boolean | 是否返回详细信息 |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| messages[].role | string | `user`/`assistant`/`system`/`tool` |
| messages[].turn_id | string | 轮次ID |
| messages[].contents[].type | string | `text`/`execution`/`code`/`image` |
| messages[].contents[].content | string | 内容 |
| messages[].contents[].timestamp | integer | 时间戳 |
| paging | object | 分页信息 |

---

### 2.3 文件管理

#### 上传临时文件（推荐）

- **路径**: `POST /api/open/office/v2/sessions/default_session/{batch_id}/files`
- **Content-Type**: `multipart/form-data`
- **说明**: 文件7天过期自动清理

| 路径参数 | 类型 | 说明 |
|----------|------|------|
| batch_id | string | 批次ID（建议用 UUID） |

| 请求参数 | 类型 | 说明 |
|----------|------|------|
| file | file | 上传文件 |
| file_url | string | 可选，文件URL（file 为空时有效） |
| file_name | string | 可选，覆盖文件名 |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| file_list[].id | integer | 文件ID（用于 upload_file_id） |
| file_list[].file_name | string | 文件名 |
| file_list[].file_size | integer | 文件大小 |
| file_list[].batch_id | string | 批次ID |
| file_list[].preview_url | string | 预览URL |
| file_list[].preview_data | object | 结构化预览数据 |

#### 上传文件到会话（即将废弃）

- **路径**: `POST /api/open/office/v2/sessions/{session_id}/files`
- **Content-Type**: `multipart/form-data`

| 请求参数 | 类型 | 说明 |
|----------|------|------|
| file | file | 上传文件 |
| file_url | string | 可选，文件URL |
| file_name | string | 可选，覆盖文件名 |
| description | string | 可选，文件描述 |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| id | string | 文件ID（用于 files 参数） |
| file_name | string | 文件名 |
| status | string | 文件状态 |

#### 获取文件列表

- **路径**: `GET /api/open/office/v2/sessions/files`

返回用户最后一次上传的临时文件列表。

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| file_list[].id | integer | 文件ID |
| file_list[].file_name | string | 文件名 |
| file_list[].file_size | integer | 大小 |
| file_list[].batch_id | string | 批次ID |
| file_list[].preview_url | string | 预览URL |
| file_list[].preview_data | object | 结构化预览 |

#### 获取文件信息

- **路径**: `GET /api/open/office/v2/sessions/{session_id}/file_info`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| file_path | string | 文件路径 |
| org_user_id | string | 可选 |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| file_name | string | 文件名 |
| preview_url | string | 预签名URL（约30分钟过期） |

#### 下载文件

- **路径**: `GET /api/open/office/v2/sessions/{session_id}/files`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| file_path | string | 文件路径 |
| org_user_id | string | 可选 |

返回 `application/octet-stream` 字节流。

#### 删除临时文件

- **路径**: `DELETE /api/open/office/v2/sessions/default_session/{file_id}`

---

### 2.4 生成物管理

#### 获取生成物列表

- **路径**: `GET /api/open/office/v2/sessions/{session_id}/artifacts`

| 查询参数 | 类型 | 说明 |
|----------|------|------|
| org_user_id | string | 可选 |

| 响应参数 | 类型 | 说明 |
|----------|------|------|
| artifacts[].filename | string | 文件名 |
| artifacts[].s3_url | string | S3 预签名下载链接 |
| artifacts[].timestamp | integer | 时间戳 |
| artifacts[].type | string | `image` / `file` |

---

## 三、错误码参考

### 模块特定错误码

| HTTP | 错误码 | 含义 |
|------|--------|------|
| 400 | 100012 | 会话ID不存在 |
| 400 | 100013 | 会话标题太长 |
| 400 | 100015 | 会话沙盒资源不足 |
| 400 | 100016 | 上传总文件数量超限 |
| 400 | 100017 | 上传总文件数据量超限 |
| 400 | 100023 | 文件不存在 |
| 400 | 100030 | 文件类型不允许 |
| 400 | 200404 | 平台沙盒数量超限 |
| 400 | 200506 | 当日问题数量超限 |
| 400 | 300001 | 模型不存在 |
| 429 | 200103 | 请求速率超限 |
| 429 | 200107 | 系统繁忙 |
| 504 | 300004 | 推理代理超时 |
| 500 | 300005 | 推理服务错误 |

### 通用错误码

| HTTP | 错误码 | 含义 |
|------|--------|------|
| 400 | 100001 | 必须参数缺失 |
| 400 | 100002 | 参数格式错误 |
| 400 | 100099 | 其他参数错误 |
| 401 | 200001 | 认证信息缺失 |
| 401 | 200002 | 认证信息格式错误 |
| 401 | 200003 | 认证失败 |
| 400 | 200005 | 用户不存在 |
| 499 | 999990 | 客户端终止请求 |
| 504 | 999991 | 请求超时 |
| 500 | 999999 | 系统内部错误 |


path: references/CHEATSHEET.md
content: # 小浣熊数据分析 API 速查表

## 认证

```
Authorization: Bearer $RACCOON_API_TOKEN
```

## 数据分析接口

| 操作 | 方法 | 路径 | 关键参数 |
|------|------|------|---------|
| 数据分析对话 | POST | `/api/open/llm/v1/data-analysis/chat-completions` | model, messages[], stream=true |

### messages 结构

| role | 可用 type | 说明 |
|------|-----------|------|
| system | text | 系统提示 |
| user | text, file, description | 用户输入/文件/文件摘要 |
| assistant | text, code, execution, summary | 模型回复/代码/执行结果/总结 |

### finish_reason 快查

| 值 | 动作 |
|----|------|
| text | 继续请求 |
| code | 执行代码后拼接结果继续 |
| stop | 结束 |
| length / sensitive / context | 异常结束 |

---

## 办公解释器接口

### 会话管理

| 操作 | 方法 | 路径 |
|------|------|------|
| 创建会话 | POST | `/api/open/office/v2/sessions` |
| 获取会话 | GET | `/api/open/office/v2/sessions/{session_id}` |
| 会话列表 | GET | `/api/open/office/v2/sessions` |
| 删除会话 | DELETE | `/api/open/office/v2/sessions/{session_id}` |

### 对话交互

| 操作 | 方法 | 路径 |
|------|------|------|
| 发起对话 | POST | `/api/open/office/v2/sessions/{session_id}/chat/conversations` |
| 追问建议 | GET | `/api/open/office/v2/sessions/{session_id}/chat/suggestions` |
| 消息列表 | GET | `/api/open/office/v2/sessions/{session_id}/messages` |

### 文件管理

| 操作 | 方法 | 路径 | Content-Type |
|------|------|------|-------------|
| 上传临时文件(推荐) | POST | `/api/open/office/v2/sessions/default_session/{batch_id}/files` | multipart/form-data |
| 上传到会话(废弃中) | POST | `/api/open/office/v2/sessions/{session_id}/files` | multipart/form-data |
| 文件列表 | GET | `/api/open/office/v2/sessions/files` | - |
| 文件信息 | GET | `/api/open/office/v2/sessions/{session_id}/file_info?file_path=` | - |
| 下载文件 | GET | `/api/open/office/v2/sessions/{session_id}/files?file_path=` | - |
| 删除文件 | DELETE | `/api/open/office/v2/sessions/default_session/{file_id}` | - |

### 生成物

| 操作 | 方法 | 路径 |
|------|------|------|
| 生成物列表 | GET | `/api/open/office/v2/sessions/{session_id}/artifacts` |

---

## 对话请求关键参数

```json
{
  "content": "分析这份数据",
  "verbose": true,
  "enable_web_search": false,
  "deep_think": false,
  "temperature": 1,
  "files": ["file_id_from_session_upload"],
  "upload_file_id": [123],
  "user_at_commands": {
    "asset_filters": [
      {"asset_code": "xxx", "file_uuid": "yyy"}
    ]
  }
}
```

## 文件传递方式对照

| 上传方式 | 对话时使用的参数 |
|---------|--------------|
| 临时文件上传 (default_session) | `upload_file_id: [文件ID]` |
| 会话文件上传 (session_id) | `files: ["文件ID"]` |
| 知识库文件 | `user_at_commands.asset_filters` |

## 常用错误码

| 码 | 含义 | 建议 |
|----|------|------|
| 100012 | 会话不存在 | 检查 session_id |
| 100015 | 沙盒资源不足 | 删除旧会话 |
| 100023 | 文件不存在 | 检查文件路径/ID |
| 200103 | 速率超限 | 等待后重试 |
| 200506 | 当日问题超限 | 次日再试 |
| 300001 | 模型不存在 | 检查 model 值 |

## SSE 流式响应解析

```
data:{"stage":"generate","data":{"delta":"文本内容","session_id":"..."},"status":{"code":0,"message":"success"}}
data:{"stage":"code","data":{"delta":"print('hello')","session_id":"..."},"status":{"code":0,"message":"success"}}
data:{"stage":"execute","data":{"delta":"hello","session_id":"..."},"status":{"code":0,"message":"success"}}
data:{"stage":"image","data":{"delta":"...","session_id":"..."},"status":{"code":0,"message":"success"}}
data:[DONE]
```

stage 枚举: `ocr` | `generate` | `code` | `execute` | `execution` | `image`


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)