AI 工具

用 Jasper API 创建 Brand Voice 并运行营销任务

官方流程是:用 API Key 调 POST /v1/tones,再在 POST /v1/tasks/{id}/run 里传 toneId 与 contextItems。 这篇提供 mock 服务 + jasperclient.py,在无密钥时仍能看懂请求形状。

BINBHO · 图文教程

官方流程是:用 API Key 调 POST /v1/tones,再在 POST /v1/tasks/{id}/run 里传 toneId 与 contextItems。 这篇提供 mock 服务 + jasper_client.py,在无密钥时仍能看懂请求形状。

先看清成品

  • mock 上创建 tone 并跑 task,输出带品牌名的草稿。
  • 错误密钥返回 401。
  • 2 项 pytest 通过。

合规提醒:Jasper API 需工作区管理员/开发者权限开通;密钥勿进前端或公开仓库。生成营销文案须遵守广告与行业法规,禁止虚假疗效、保证收益等表述。品牌声音与知识库应使用你有权使用的资料。价格与套餐以官网为准,本文不编造。

运行说明:无 Jasper 正式密钥。A/C 为本地校验;B 用本机 mock(X-API-Key)跑通创建 tone + run task。2026-10-23(Asia/Shanghai)pytest 全部通过。对接正式环境时把 --base 设为 https://api.jasper.ai 。

做完之后你会得到什么?

成果 说明
mock_jasper.py 本地假服务
jasper_client.py 客户端
test_97b.py 2 项测试

需要准备哪些环境?

项目 说明
Python 3.10+
密钥 正式环境:JASPER_API_KEY;教程 mock:jasper-test-key

关键事实

事实 来源
鉴权头:X-API-Key: <token>,基址 https://api.jasper.ai 官方 Authentication
Agent task run 可传 toneId、contextItems、knowledgeIds 等 官方 Run task / Using Agents
task id 来自你工作区已发布的 Agent;mock 使用占位 agt_mock_blog 官方 + 教程说明

文件结构

97-jasper-B-src/
├── mock_jasper.py
├── jasper_client.py
└── test_97b.py

第 1 步:Mock

#!/usr/bin/env python3
"""Minimal mock of Jasper tones + task run for offline tutorials."""
from __future__ import annotations

import json
from http.server import BaseHTTPRequestHandler, HTTPServer

API_KEY = "jasper-test-key"
TONES: dict[str, dict] = {}
_next = 1


class H(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def _auth(self) -> bool:
        return self.headers.get("X-API-Key") == API_KEY

    def _json(self, code: int, obj: dict):
        body = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_POST(self):
        if not self._auth():
            self._json(401, {"error": "Unauthorized"})
            return
        n = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(n) or b"{}")
        if self.path == "/v1/tones":
            global _next
            tid = f"ton_mock_{_next}"
            _next += 1
            TONES[tid] = {"id": tid, "name": body.get("name"), "value": body.get("value")}
            self._json(201, TONES[tid])
            return
        if self.path.startswith("/v1/tasks/") and self.path.endswith("/run"):
            tone = body.get("toneId")
            voice = TONES.get(tone, {}).get("name", "default")
            ctx = body.get("contextItems") or []
            topic = ""
            for c in ctx:
                topic = c.get("value") or c.get("answer") or topic
            self._json(
                200,
                {
                    "output": f"[{voice}] 关于「{topic}」的营销草稿:强调价值主张,结尾加明确 CTA。",
                    "toneId": tone,
                },
            )
            return
        self.send_error(404)

    def do_GET(self):
        if not self._auth():
            self._json(401, {"error": "Unauthorized"})
            return
        if self.path.startswith("/v1/tones"):
            self._json(200, {"data": list(TONES.values())})
            return
        self.send_error(404)


def main():
    import argparse

    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=8970)
    args = ap.parse_args()
    HTTPServer(("127.0.0.1", args.port), H).serve_forever()


if __name__ == "__main__":
    main()

第 2 步:客户端

#!/usr/bin/env python3
"""Thin Jasper API client: create tone + run a task (official X-API-Key)."""
from __future__ import annotations

import argparse
import json
import os
import urllib.error
import urllib.request


def request(base: str, key: str, method: str, path: str, payload: dict | None = None) -> dict:
    data = None if payload is None else json.dumps(payload).encode()
    req = urllib.request.Request(
        f"{base.rstrip('/')}{path}",
        data=data,
        headers={"X-API-Key": key, "Content-Type": "application/json", "Accept": "application/json"},
        method=method,
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--base", default=os.environ.get("JASPER_BASE_URL", "https://api.jasper.ai"))
    ap.add_argument("--key", default=os.environ.get("JASPER_API_KEY", ""))
    ap.add_argument("--name", default="Tutorial Voice")
    ap.add_argument("--value", default="Professional, concise, benefit-led B2B tone without hype.")
    ap.add_argument("--task-id", default="agt_mock_blog")
    ap.add_argument("--topic", default="推出 Acme Flow 的产品更新邮件")
    args = ap.parse_args(argv)
    if not args.key:
        print("设置 JASPER_API_KEY", file=__import__("sys").stderr)
        return 2
    tone = request(
        args.base,
        args.key,
        "POST",
        "/v1/tones",
        {"name": args.name, "value": args.value},
    )
    print("tone:", tone.get("id"), tone.get("name"))
    out = request(
        args.base,
        args.key,
        "POST",
        f"/v1/tasks/{args.task_id}/run",
        {
            "toneId": tone["id"],
            "contextItems": [{"id": "topic", "value": args.topic}],
        },
    )
    print(out.get("output") or out)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

第 3 步:测试

import subprocess
import sys
import threading
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent


def test_mock_create_and_run():
    port = 18970
    proc = subprocess.Popen(
        [sys.executable, str(HERE / "mock_jasper.py"), "--port", str(port)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        time.sleep(0.3)
        r = subprocess.run(
            [
                sys.executable,
                str(HERE / "jasper_client.py"),
                "--base",
                f"http://127.0.0.1:{port}",
                "--key",
                "jasper-test-key",
                "--topic",
                "春季促销邮件",
            ],
            capture_output=True,
            text=True,
            check=True,
        )
        assert "tone:" in r.stdout
        assert "春季促销" in r.stdout or "营销草稿" in r.stdout
    finally:
        proc.terminate()
        proc.wait(timeout=5)


def test_bad_key():
    port = 18971
    proc = subprocess.Popen(
        [sys.executable, str(HERE / "mock_jasper.py"), "--port", str(port)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        time.sleep(0.3)
        r = subprocess.run(
            [
                sys.executable,
                str(HERE / "jasper_client.py"),
                "--base",
                f"http://127.0.0.1:{port}",
                "--key",
                "wrong",
            ],
            capture_output=True,
            text=True,
        )
        assert r.returncode != 0
        assert "401" in r.stderr or "401" in r.stdout
    finally:
        proc.terminate()
        proc.wait(timeout=5)

第 4 步:运行

..                                                                       [100%]
2 passed in 0.72s
tone: ton_mock_1 Tutorial Voice
[Tutorial Voice] 关于「春季促销」的营销草稿:强调价值主张,结尾加明确 CTA。

怎么检查结果?

  • 输出含 tone: ton_mock_…。
  • 换正式 base/key/task-id 前先在开发者文档试通。

调试示例

正式 task id 不是随便编的。 需在 Jasper 工作区创建/发布 Agent task,再用返回的 agt_… id。mock 只演示 HTTP 形状。

常见问题

现象 原因 处理
401 密钥/角色 检查 Dev Tools token
OAuth 用户代操作 服务端脚本优先 API Key
输出不稳定 Agent 版本 可传 version 钉住

模板规则

  • 只用自己工作区密钥。
  • mock 不得暴露为公网开放代理。
  • 生成内容需人工审核。

项目资源

用途 地址
Authentication(官方) https://developers.jasper.ai/docs/authentication
Run an agent task(官方) https://developers.jasper.ai/reference/runtask
Using Agents(官方) https://developers.jasper.ai/docs/using-agents

mock 练手,正式环境换 base、key、真实 task id。