五步接入:从零到第一条 200
不需要装 SDK、不需要看源码:拿 key → 存环境变量 → 发第一个请求 → 拿到永久链接 → 换成二级单元。 每一步都能原样复制运行,每一步都写清"成功长什么样"。
-
申请并领取 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里写清楚。 -
把 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。 -
发第一个请求(结果档 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 ContentWindows 上的三个已知坑(都在 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 解析,中文注释与提示会乱码)。 -
拿到 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可以让这次调用不生成快照(省一次落盘),但那样就没有永久链接了。 -
换用二级单元 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,它天生就是答案档)。
# 深答案档:同一个端点 + 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}'
常见错误怎么办
| HTTP | 错误码 | 什么时候出现 | 怎么办 |
|---|---|---|---|
401 | INVALID_API_KEY | 没带 Authorization 头 / 不是 Bearer / key 形状不对 / key 不存在 | 检查请求头是否为 `Authorization: Bearer <你的 key>`;key 明文只在签发/领取时出现一次,丢了就让管理员重新批准一次(会换新 key,不会重复扣费)。 |
402 | INSUFFICIENT_CREDITS | 预付 credits 余额不足(包月套餐额度也算在内) | 响应里带 `balance` / `required`;充值后重试,或改用更便宜的档位(结果档 < 快答案档 < 深答案档)。 |
403 | SCOPE_FORBIDDEN | 这个 key 没租你要调的商品(例如只租了 l1 却调 /v1/answer) | 响应里带 `required` / `granted` / `available`(你实际能调的商品清单);按 `available` 换端点,或让管理员给这个 key 加上对应 scope(`l1` / `l1:answer` / `l1:deep` / `l2:<单元>` / `*`)。 |
429 | RATE_LIMITED | 同一个 key 打太快(默认 5 rps,突发 5) | 响应带 `retry_after`(秒);按它退避后重试。上游 429 我们没有 `Retry-After` 可依赖,所以退避必须由调用方自己做(见 B31 实测)。 |
429 | QUOTA_EXCEEDED | 日/月配额用尽(与余额是两本账) | 响应带 `retry_after`;等到次日/次月自动恢复,或让管理员提升该 key 的档位配额。 |
503 | ANSWER_UNAVAILABLE | 答案档当前不可交付(缺写作引擎或抓取端点) | 这是**扣费之前**就返回的 503:`charged: 0`,你一分钱没花;`error.missing` 写明缺什么、`error.hint` 写明怎么开(例如 `WRITER_ENABLED=true`)。要现在就能用请改调结果档 `/v1/search`。 |
503 | DIGEST_NOT_READY | 定时单元的当期归档还没生成(missPolicy=reject,或那一期正在生成中) | 响应带 `date` / `slot` / `miss_policy`;等期次生成后重试,或在 body 里加 `"live": true` 走一次实时检索(会产生正常的检索成本)。 |
503 | UNIT_CIRCUIT_OPEN | 该单元连续失败触发熔断(冷却中),其它单元不受影响 | 响应带冷却信息;等冷却结束自动半开恢复,或先用其它单元顶上(读写同一个中间件的别的单元)。 |
不在表里的情况:完整文档的错误码全表;或先跑
GET /healthz 看服务自身状态(含各商品当前能不能交付)。