五步接入:从零到第一条 200

不需要装 SDK、不需要看源码:拿 key → 存环境变量 → 发第一个请求 → 拿到永久链接 → 换成二级单元。 每一步都能原样复制运行,每一步都写清"成功长什么样"。

  1. 申请并领取 key

    不需要找人工要账号:填一张表 → 人工审批 → 打开领取链接拿明文。 审批通过后你会收到一条 /claim/<领取码> 链接,打开一次就能看到 key; 再打开只显示"已领取"。链接默认 72 小时有效,过期不用重新填表,让管理员重新批准一次即可。

    第 1 步 · 浏览器打开申请表(纯 HTML,不用 JS)
    # 第 1 步:填申请表(浏览器打开,纯 HTML 表单,不用 JS)
    https://search.ozforce.cn/apply
    第 1 步 · 或者用程序提交同一张表(curl)
    # 或者用程序提交同一张表(返回 application_id,状态 pending)
    curl -sS -X POST "https://search.ozforce.cn/v1/apply" \
      -H "Content-Type: application/json" \
      -d '{"name":"李四","email":"lisi@example.com","purpose":"给公司内部 AI 助手做联网检索,每天几百次","expected_daily_calls":200}'
    第 1 步 · 审批后打开领取链接(明文只出现这一次)
    # 第 3 步:审批通过后打开领取链接(明文只显示这一次,看到就存进密码管理器)
    https://search.ozforce.cn/claim/<领取码>

    看到 key 的第一眼就存进密码管理器。默认批下来是免费档(日 50 / 月 500 + 赠送 100 credits), 想更高额度请在申请的 purpose 里写清楚。

  2. 把 key 存进环境变量

    所有片段都从环境变量 WS_API_KEY 读 key。 这样做的好处是:命令历史、截图、录屏里都不会出现明文。

    第 2 步 · Windows PowerShell
    # Windows PowerShell:只对这个窗口有效(永久生效请用 [Environment]::SetEnvironmentVariable)
    $env:WS_API_KEY = '<你的 key>'
    第 2 步 · macOS / Linux
    # macOS / Linux:只对这个终端有效(永久生效请写进 ~/.bashrc 或 ~/.zshrc)
    export WS_API_KEY='<你的 key>'

    上面两行只对当前窗口有效。要永久生效:Windows 用 [Environment]::SetEnvironmentVariable('WS_API_KEY','…','User');macOS/Linux 写进 ~/.zshrc 或 ~/.bashrc。

  3. 发第一个请求(结果档 l1)

    这一步之后你就已经接完了:POST /v1/search 返回来源列表 + 一条永久快照链接。 成功长什么样:HTTP 200,响应里有 sources[](每条含 title / url / snippet)、 snapshot_url、usage.credits_charged、usage.balance_after。

    第 3 步 · curl(macOS / Linux / Git Bash)
    # 第一个请求:结果档 l1 —— 拿回来源列表 + 一条永久快照链接
    curl -sS -X POST "https://search.ozforce.cn/v1/search" \
      -H "Authorization: Bearer ${WS_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{"q":"杭州 三日游 攻略","count":5,"freshness":"month","snapshot":true}'
    第 3 步 · curl(Windows 命令行 cmd.exe,实测可直接粘贴)
    rem Windows 命令行(cmd.exe):在同一窗口先 set WS_API_KEY=你的key,再粘贴下一行
    curl -sS -X POST "https://search.ozforce.cn/v1/search" -H "Authorization: Bearer %WS_API_KEY%" -H "Content-Type: application/json" -d "{\"q\":\"杭州 三日游 攻略\",\"count\":5,\"freshness\":\"month\",\"snapshot\":true}"
    第 3 步 · Node(存成 first.mjs,node first.mjs)
    // 存成 first.mjs,然后:node first.mjs(Node 18+ 自带 fetch,不用装任何包)
    const BASE = 'https://search.ozforce.cn'
    const KEY = process.env.WS_API_KEY
    if (!KEY) {
      console.error('先设置环境变量 WS_API_KEY(值是你的 API key)')
      process.exit(1)
    }
    
    const res = await fetch(BASE + '/v1/search', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ q: '杭州 三日游 攻略', count: 5, freshness: 'month', snapshot: true })
    })
    
    const data = await res.json()
    if (!res.ok) {
      console.error('失败', res.status, data.error)
      process.exit(1)
    }
    
    console.log('扣费', data.usage.credits_charged, 'credits,余额', data.usage.balance_after)
    console.log('永久链接', data.snapshot_url)
    for (const s of data.sources) console.log('-', s.title, s.url)
    第 3 步 · Python(只用标准库)
    # Python 3.8+,只用标准库
    import json, os, urllib.request
    
    BASE = 'https://search.ozforce.cn'
    KEY = os.environ.get('WS_API_KEY')
    if not KEY:
        raise SystemExit('先设置环境变量 WS_API_KEY(值是你的 API key)')
    
    body = json.dumps({'q': '杭州 三日游 攻略', 'count': 5, 'freshness': 'month', 'snapshot': True}).encode('utf-8')
    req = urllib.request.Request(
        BASE + '/v1/search', data=body, method='POST',
        headers={'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json'},
    )
    with urllib.request.urlopen(req) as res:
        data = json.loads(res.read().decode('utf-8'))
    
    print('扣费', data['usage']['credits_charged'], 'credits,余额', data['usage']['balance_after'])
    print('永久链接', data['snapshot_url'])
    for s in data['sources']:
        print('-', s['title'], s['url'])
    第 3 步 · Windows PowerShell(中文 body 走 UTF-8 字节)
    # Windows PowerShell 5.1 / PowerShell 7 都能直接跑(先设好 $env:WS_API_KEY)
    $Base = 'https://search.ozforce.cn'
    $Key  = $env:WS_API_KEY
    if (-not $Key) { throw '先设置环境变量 WS_API_KEY:$env:WS_API_KEY = ''<你的 key>''' }
    
    # 中文 body 必须发 UTF-8 字节:PowerShell 5.1 直接把字符串交给 -Body,中文会按本地代码页变成问号
    $Json  = @{ q = '杭州 三日游 攻略'; count = 5; freshness = 'month'; snapshot = $true } | ConvertTo-Json -Compress
    $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Json)
    
    $R = Invoke-RestMethod -Method Post -Uri "$Base/v1/search" `
      -Headers @{ Authorization = "Bearer $Key" } `
      -ContentType 'application/json; charset=utf-8' `
      -Body $Bytes
    
    "扣费 $($R.usage.credits_charged) credits,余额 $($R.usage.balance_after)"
    "永久链接 $($R.snapshot_url)"
    $R.sources | Select-Object title, url | Format-Table -AutoSize
    
    # 读 HTML 页面(例如 /products、/v1/catalog)时务必加 -UseBasicParsing:
    #   Invoke-WebRequest "$Base/products" -UseBasicParsing | Select-Object -Expand Content

    Windows 上的三个已知坑(都在 B42 自证里实测过): ① PowerShell 里不要直接粘上面那段 POSIX curl —— PowerShell 5.1 把参数交给 curl.exe 时不转义内层双引号,body 会变成 {q:杭州} 并被判 400 INVALID_JSON;Windows 上请用 cmd.exe 版或 PowerShell 版; ② 中文 JSON body 在 PowerShell 里必须转成 UTF-8 字节再交给 -Body,否则按本地代码页发出、中文变问号; ③ 用 Invoke-WebRequest 读 HTML 时必须加 -UseBasicParsing,老版本会走 IE 引擎并误报。 另:把 PowerShell 片段存成 .ps1 文件时请存为 UTF-8 带 BOM (PowerShell 5.1 读无 BOM 的 .ps1 会按 ANSI 解析,中文注释与提示会乱码)。

  4. 拿到 snapshot_url:这是永久链接

    每次 snapshot:true(默认就是 true)的调用都会冻结一份副本,返回形如 https://search.ozforce.cn/s/<12 位 token> 的链接。这条链接里的内容从此不再变化: 20 天后打开、明年打开,和今天看到的一字不差(正文逐字节冻结,页脚合规信息实时渲染)。 同一个 token 永远不会被覆盖,所以它可以直接当"当时的证据"发给客户、写进合同附件、贴进工单。

    /s/<token>.json 是同一份内容的原始数据(长缓存、不可变),适合程序读取。

    第 4 步 · 打开快照(不需要 key)
    # 把上一步响应里的 snapshot_url 原样打开(不需要 key):
    #   https://search.ozforce.cn/s/<token>        人读的 HTML 页(离线保存也能读)
    #   https://search.ozforce.cn/s/<token>.json   同一份内容的原始数据
    curl -sS "https://search.ozforce.cn/s/<token>.json"

    snapshot: false 可以让这次调用不生成快照(省一次落盘),但那样就没有永久链接了。

  5. 换用二级单元 API(成品)

    二级单元就是"已经替你调好一级 API 并按领域整理好"的成品。 以 m1/news(即时新闻) 为例: 端点 POST /v2/m1/news/run, 返回 items[](或 tracks[],视单元而定)+ 两条链接(快照 / 实时入口)。 key 上需要对应的 scope:l2:news(或 *)。

    第 5 步 · curl(m1/news)
    # 二级单元:m1/news(结果结构更贴合该领域,价格见页面上的单元卡片)
    curl -sS -X POST "https://search.ozforce.cn/v2/m1/news/run" \
      -H "Authorization: Bearer ${WS_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{"q":"科技","count":5,"snapshot":true}'
    第 5 步 · Node(m1/news)
    // 二级单元 m1/news:返回 items[](结构按单元的 resultSchema)
    const BASE = 'https://search.ozforce.cn'
    const KEY = process.env.WS_API_KEY
    if (!KEY) { console.error('先设置环境变量 WS_API_KEY'); process.exit(1) }
    
    const res = await fetch(BASE + '/v2/m1/news/run', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ q: '科技', count: 5, snapshot: true })
    })
    const data = await res.json()
    if (!res.ok) { console.error('失败', res.status, data.error); process.exit(1) }
    
    console.log('共', data.items.length, '条,扣费', data.usage.credits_charged, 'credits')
    console.log('永久链接', data.snapshot_url)
    for (const item of data.items) console.log('-', item.title, item.url)
    第 5 步 · Python(m1/news)
    # 二级单元 m1/news
    import json, os, urllib.request
    
    BASE = 'https://search.ozforce.cn'
    KEY = os.environ.get('WS_API_KEY')
    if not KEY:
        raise SystemExit('先设置环境变量 WS_API_KEY')
    
    body = json.dumps({'q': '科技', 'count': 5, 'snapshot': True}).encode('utf-8')
    req = urllib.request.Request(
        BASE + '/v2/m1/news/run', data=body, method='POST',
        headers={'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json'},
    )
    with urllib.request.urlopen(req) as res:
        data = json.loads(res.read().decode('utf-8'))
    
    print('共', len(data['items']), '条,扣费', data['usage']['credits_charged'], 'credits')
    print('永久链接', data['snapshot_url'])

    先看有哪些单元、每个单元多少钱:GET /v1/units(公开)与商品页 /products;机读价格用 /v1/catalog。

    第 5 步 · 列出所有单元(公开,不要 key)
    # 单元目录(公开,不要 key)
    curl -sS "https://search.ozforce.cn/v1/units"

想直接要一段答案?用答案档

结果档只给链接;要"一段能直接展示、带引用的答案"就调 POST /v1/answer: 不传 depth 是快答案(读片段),depth:"deep" 是深答案(真去抓原文)。 注意:这个端点不接受 need_answer(传了会 400,它天生就是答案档)。

答案档 · curl(深答案)
# 深答案档:同一个端点 + depth:"deep"(会真去抓原文,更准也更慢)
curl -sS -X POST "https://search.ozforce.cn/v1/answer" \
  -H "Authorization: Bearer ${WS_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"q":"天空为什么是蓝色的","depth":"deep","count":8}'

常见错误怎么办

下表的状态码与错误码都取自实现(src/util/errors.mjs),不是文档自己编的。 所有错误体统一是 { "error": { "code", "message", ... }, "request_id": "..." }; 排查时把 request_id 给我们就够了。

HTTP错误码什么时候出现怎么办
401INVALID_API_KEY没带 Authorization 头 / 不是 Bearer / key 形状不对 / key 不存在检查请求头是否为 `Authorization: Bearer <你的 key>`;key 明文只在签发/领取时出现一次,丢了就让管理员重新批准一次(会换新 key,不会重复扣费)。
402INSUFFICIENT_CREDITS预付 credits 余额不足(包月套餐额度也算在内)响应里带 `balance` / `required`;充值后重试,或改用更便宜的档位(结果档 < 快答案档 < 深答案档)。
403SCOPE_FORBIDDEN这个 key 没租你要调的商品(例如只租了 l1 却调 /v1/answer)响应里带 `required` / `granted` / `available`(你实际能调的商品清单);按 `available` 换端点,或让管理员给这个 key 加上对应 scope(`l1` / `l1:answer` / `l1:deep` / `l2:<单元>` / `*`)。
429RATE_LIMITED同一个 key 打太快(默认 5 rps,突发 5)响应带 `retry_after`(秒);按它退避后重试。上游 429 我们没有 `Retry-After` 可依赖,所以退避必须由调用方自己做(见 B31 实测)。
429QUOTA_EXCEEDED日/月配额用尽(与余额是两本账)响应带 `retry_after`;等到次日/次月自动恢复,或让管理员提升该 key 的档位配额。
503ANSWER_UNAVAILABLE答案档当前不可交付(缺写作引擎或抓取端点)这是**扣费之前**就返回的 503:`charged: 0`,你一分钱没花;`error.missing` 写明缺什么、`error.hint` 写明怎么开(例如 `WRITER_ENABLED=true`)。要现在就能用请改调结果档 `/v1/search`。
503DIGEST_NOT_READY定时单元的当期归档还没生成(missPolicy=reject,或那一期正在生成中)响应带 `date` / `slot` / `miss_policy`;等期次生成后重试,或在 body 里加 `"live": true` 走一次实时检索(会产生正常的检索成本)。
503UNIT_CIRCUIT_OPEN该单元连续失败触发熔断(冷却中),其它单元不受影响响应带冷却信息;等冷却结束自动半开恢复,或先用其它单元顶上(读写同一个中间件的别的单元)。

不在表里的情况:完整文档的错误码全表;或先跑 GET /healthz 看服务自身状态(含各商品当前能不能交付)。

下一步