跳至主要內容

指南

客戶端範例

Python 與 Node.js 範例:串流接收、續傳、上載與下載。

範例都從環境變數讀取地址和 API Key,與快速開始相同:AXON_API 是 API 地址,AXON_KEY 是你的 Key。

Python(httpx)

import json
import os
import httpx

API = os.environ["AXON_API"]
HEADERS = {"Authorization": f"Bearer {os.environ['AXON_KEY']}"}
TERMINAL = {"turn.completed", "turn.failed", "turn.cancelled"}


def sse_events(response):
    """把 SSE 回應解析成 (id, event, data) 三元組。"""
    frame = {}
    for line in response.iter_lines():
        if line == "":
            if "data" in frame:
                yield frame.get("id"), frame.get("event"), json.loads(frame["data"])
            frame = {}
        elif line.startswith(":"):
            continue                                   # 保活註解
        else:
            field, _, value = line.partition(":")
            frame[field] = value[1:] if value.startswith(" ") else value


def ask(client, session_id, content, files=()):
    """發送一條訊息(可附帶檔案 ID),輸出回覆;返回終止事件和 Agent 交付的檔案。"""
    body = {"content": content, "files": list(files)}
    client.post(f"{API}/sessions/{session_id}/messages", json=body).raise_for_status()

    delivered = []
    last_id = None
    while True:
        headers = {"Last-Event-ID": last_id} if last_id else {}
        try:
            with client.stream("GET", f"{API}/sessions/{session_id}/events/stream",
                               headers=headers, timeout=None) as response:
                response.raise_for_status()
                for last_id, event, data in sse_events(response):
                    if event == "agent.message.delta":
                        print(data["content"], end="", flush=True)
                    elif event == "agent.tool_use":
                        print(f"\n[調用工具 {data['name']}]")
                    elif event == "agent.tool_result":
                        delivered += data["files"]
                    elif event in TERMINAL:
                        print(f"\n本輪結束:{event}", data.get("reason") or data.get("usage"))
                        return data, delivered
            return None, delivered    # 流結束但沒有終止事件:以事件歷史和會話狀態為準
        except httpx.TransportError:
            continue       # 網絡中斷:帶 Last-Event-ID 重連


with httpx.Client(headers=HEADERS) as client:
    session = client.post(f"{API}/sessions", json={"model_id": "deepseek-v4-flash"}).json()
    ask(client, session["id"], "用 Python 算一下 2 的 100 次方")
    ask(client, session["id"], "再除以 3,保留兩位小數")

    # 上載檔案交給 Agent,再下載它交付的結果
    with open("sales.csv", "rb") as f:
        upload = client.post(f"{API}/files", files={"file": ("sales.csv", f)}).json()
    _, delivered = ask(client, session["id"], "按 region 彙總 amount,結果保存成 summary.csv 交付給我",
                       files=[upload["id"]])
    for file in delivered:
        content = client.get(f"{API}/files/{file['id']}/content", follow_redirects=True)
        content.raise_for_status()
        with open(file["filename"], "wb") as out:
            out.write(content.content)

Node.js

Node.js 18 及以上,原生 fetch;保存為 .mjs 執行。

const API = process.env.AXON_API;
const HEADERS = { Authorization: `Bearer ${process.env.AXON_KEY}`, "Content-Type": "application/json" };
const TERMINAL = new Set(["turn.completed", "turn.failed", "turn.cancelled"]);

async function* sseEvents(response) {
  const decoder = new TextDecoder();
  let buffer = "";
  for await (const chunk of response.body) {
    buffer += decoder.decode(chunk, { stream: true });
    let end;
    while ((end = buffer.indexOf("\n\n")) !== -1) {
      const frame = {};
      for (const line of buffer.slice(0, end).split("\n")) {
        if (line.startsWith(":")) continue;               // 保活註解
        const i = line.indexOf(":");
        frame[line.slice(0, i)] = line.slice(i + 1).replace(/^ /, "");
      }
      buffer = buffer.slice(end + 2);
      if (frame.data) yield { id: frame.id, event: frame.event, data: JSON.parse(frame.data) };
    }
  }
}

async function ask(sessionId, content, files = []) {
  await fetch(`${API}/sessions/${sessionId}/messages`, {
    method: "POST", headers: HEADERS, body: JSON.stringify({ content, files }),
  });
  const response = await fetch(`${API}/sessions/${sessionId}/events/stream`, { headers: HEADERS });
  for await (const { event, data } of sseEvents(response)) {
    if (event === "agent.message.delta") process.stdout.write(data.content);
    if (TERMINAL.has(event)) return data;
  }
}

const session = await (await fetch(`${API}/sessions`, {
  method: "POST", headers: HEADERS, body: JSON.stringify({ model_id: "deepseek-v4-flash" }),
})).json();
await ask(session.id, "列出 /workspace 裡的檔案");

上載與下載檔案(同一個 .mjs 中):

import { readFile, writeFile } from "node:fs/promises";

const AUTH = { Authorization: HEADERS.Authorization };   // multipart 由 fetch 自己設定 Content-Type

const form = new FormData();
form.append("file", new Blob([await readFile("sales.csv")]), "sales.csv");
const upload = await (await fetch(`${API}/files`, { method: "POST", headers: AUTH, body: form })).json();
// 發送時把它附上:ask(session.id, "按 region 彙總 amount", [upload.id]);Agent 交付的檔案在 agent.tool_result 的 files 中

async function download(file) {                            // file 為 files 中的一項
  const response = await fetch(`${API}/files/${file.id}/content`, { headers: AUTH });  // 自動跟隨 307
  await writeFile(file.filename, Buffer.from(await response.arrayBuffer()));
}

以上範例為了簡潔,省略了錯誤處理。生產環境中請檢查每個回應的狀態碼,並參考 Python 範例,在斷線時帶 Last-Event-ID 重連。

命令行對話

把上面的各個部分串起來,就是一個命令行對話程式:建立會話,串流顯示回覆,斷線時自動續傳,可以附帶檔案,並把 Agent 交付的檔案下載到 downloads/ 目錄。下面是把上面的函數接成命令行迴圈後的執行效果示意(輸入訊息後按 Enter 發送,/file 路徑 把檔案附到下一條訊息,/quit 退出):

會話 ses_679cd58a3c8344ea8c1f7f4603177955(deepseek-v4-flash)。/file 路徑 附帶檔案,/quit 退出。

> /file sales.csv
[已上載 sales.csv,45 位元組,隨下一條訊息發送]
> 按 region 彙總 amount,結果保存成 summary.csv 交付給我

[調用 read_file]

[調用 write_file]

[調用 deliver_file]
[已下載 downloads/summary.csv]
已按 region 彙總完成。結果:華南 150,華北 80。檔案 summary.csv 已交付。
— turn.completed,用量 {'input_tokens': 34084, 'output_tokens': 304}