智叟云集系统整体架构解析:多语言 API 接入、异步监控与本地化部署
围绕品牌 AI 可见度监测,提供品牌管理、监控问题配置、多平台任务执行、回答与来源整理、历史趋势、报告以及服务商 API 接入能力。
系统既可以通过网页直接使用,也可以部署到企业自己的电脑、内网服务器或云服务器,由其他业务系统通过 API 调用。
对开发者而言,需要理解三个核心特点:

- 接口采用 HTTP 与 JSON,能够被不同编程语言调用。
- 监控采用异步执行,接口请求成功与采集任务成功需要分别判断。
- 支持本地化部署,但访问外部 AI 平台时仍需要相应网络和授权条件。
一、整体架构如何组成
系统采用前后端分离的工程结构,业务服务与采集执行按模块组织。
| 层级 | 当前技术或组成 | 主要作用 |
|---|---|---|
| 前端 | React、Vite | 品牌配置、任务操作、结果与报告展示 |
| API 服务 | Node.js、Fastify | 接收请求、校验参数、执行权限控制 |
| 身份与权限 | 登录会话、API Key、租户与客户归属 | 区分用户、服务商及可访问数据 |
| 任务执行 | Worker、定时调度、采集适配器 | 执行问题与平台组合对应的采集任务 |
| 采集方式 | Playwright 浏览器、第三方采集接口 | 获取平台回答及支持的附加内容 |
| 数据库 | SQLite | 保存品牌、问题、任务、结果及业务配置 |
| 证据存储 | 本地受控文件目录 | 保存截图等采集证据 |
| 部署入口 | Nginx、应用服务进程 | HTTPS、请求转发和服务运行管理 |
业务链路可以理解为:
浏览器客户端 企业系统 / 服务商系统
│ │
│ 登录会话 │ Bearer API Key
└────────────────┬───────────────┘
▼
身份与权限校验
▼
品牌 / 问题 / 监控任务服务
▼
后台采集执行
┌───────┴────────┐
▼ ▼
浏览器采集 第三方采集接口
└───────┬────────┘
▼
回答 / 引用 / 截图 / 诊断
▼
数据库与证据目录
▼
页面展示 / API 查询
这里的 Worker 表示执行职责。当前代码中的 Worker 和调度器由应用初始化,不能仅凭架构图就认为已经部署成独立的分布式集群。
舆情监控、扩展数据分析等新增模块,可以复用这些基础能力,但应以实际开发与验收进度说明其上线状态。
二、支持哪些编程语言调用
准确的说法是:
任何能够发送 HTTP 请求、设置请求头并解析 JSON 的语言,都可以接入智叟云集 API。
例如:
| 语言或平台 | 常见调用方式 |
|---|---|
| Python | urllib、requests、httpx |
| JavaScript / TypeScript | fetch、Axios |
| Java | HttpClient、Spring HTTP 客户端 |
| PHP | cURL、Guzzle |
| C# / .NET | HttpClient |
| Go | net/http |
| Rust | reqwest |
| C / C++ | libcurl |
| Ruby | Net::HTTP |
| Shell | curl |
| 低代码或工作流平台 | HTTP 请求节点 |
这不意味着已经为每种语言提供独立 SDK。跨语言兼容主要由统一的 HTTP 协议、请求字段、JSON 返回结构和错误规则实现。
服务商密钥应保存在服务端。 网页前端、公开代码仓库和移动端安装包不适合直接保存长期有效的服务商 Key。
三、接口地址与鉴权方式
假设部署地址为:
https://geo.example.com
服务商接口前缀为:
/api/v1
请求头:
Authorization: Bearer <API_KEY>
Accept: application/json
提交 JSON 时增加:
Content-Type: application/json
创建监控任务时,还需要:
Idempotency-Key: customer-order-20261016-001
这个标识用于识别同一次业务请求:
- 同一业务请求重试时,保留相同标识和相同请求内容。
- 同一标识被用于不同请求内容时,系统返回冲突。
- 不要每次网络重试都生成新标识,否则可能被视为新的监控任务。
源码中的 API 权限主要分为:
| 权限 | 用途 |
|---|---|
read |
查询平台、品牌、任务、报告等 |
monitor |
发起监控任务 |
provision |
创建品牌和问题 |
拥有权限还不够,目标品牌也必须处于该 Key 的授权范围内。
四、从接入到获得结果的完整流程
第一步:查询平台目录
GET /api/v1/platforms
用于读取当前部署提供的平台目录及相关信息。
不要在客户端直接把中文名称当作平台编号。例如,当前快照中千问使用的内部平台编号为 qwen,具体编号应以接口返回为准。
第二步:查询余额和调用额度
GET /api/v1/usage
主要用于了解余额、有效期、价格配置和日调用额度。
金额字段例如 balanceCents 使用"分"为单位,前端展示时再转换,不要将它直接当作"元"。
第三步:准备品牌和问题
如果品牌已经存在:
GET /api/v1/brands
GET /api/v1/brands/{brandId}/questions
如果密钥拥有 provision 权限,可以创建:
POST /api/v1/brands
Content-Type: application/json
{
"name": "示例品牌",
"aliases": ["示例品牌别名"]
}
然后添加问题:
POST /api/v1/brands/{brandId}/questions
Content-Type: application/json
{
"texts": [
"示例品牌有哪些产品和服务?"
],
"category": "品牌认知"
}
这两个创建接口在核对的版本中返回 HTTP 201。
第四步:创建监控任务
POST /api/v1/runs
Authorization: Bearer <API_KEY>
Content-Type: application/json
Idempotency-Key: customer-order-20261016-001
{
"brandId": "<BRAND_ID>",
"questionIds": ["<QUESTION_ID>"],
"platforms": ["deepseek"],
"repeatCount": 1,
"intervalSeconds": 60
}
其中:
| 字段 | 含义 |
|---|---|
brandId |
已授权的品牌编号 |
questionIds |
属于该品牌且已启用的问题编号 |
platforms |
平台编号数组 |
repeatCount |
每个问题与平台组合的采样次数 |
intervalSeconds |
重复采样间隔 |
核对的快照中,外部任务最多包含 20 个采集单元:
问题数量 × 平台数量 × 重复次数 ≤ 20
此限制属于版本配置与实现约束,不能默认所有后续版本都保持不变。

第五步:查询任务
GET /api/v1/runs/{runId}
根据任务状态继续等待,或读取结果。
第六步:获取报告
GET /api/v1/runs/{runId}/report
查询成功并不保证报告中所有样本都采集成功,应结合任务完成情况理解报告内容。
五、成功时返回什么
以下 JSON 是依据代码字段整理的精简示例,省略了部分字段,编号和金额均为示例。
1. 任务创建成功
HTTP 状态:
202 Accepted
返回示例:
{
"id": "example-run-id",
"brandId": "example-brand-id",
"status": "queued",
"reused": false,
"customerId": "example-customer-id",
"billing": {
"chargedCents": 20,
"balanceCents": 980
}
}
含义:
id:后续查询使用的任务编号。queued:任务已进入队列。reused:是否复用了此前相同幂等请求。billing:本次返回的计费相关信息,实际费用以部署配置为准。
HTTP 202 只代表请求已受理,不代表采集已经完成。 这也符合 HTTP 标准对 202 的定义。HTTP 202 标准说明
2. 查询成功且任务完成
HTTP 状态:
200 OK
返回示例:
{
"id": "example-run-id",
"status": "completed",
"completedChecks": 1,
"failedChecks": 0,
"platformRuns": [
{
"id": "example-sample-id",
"platformId": "deepseek",
"status": "completed",
"errorCode": null,
"errorMessage": null,
"answer": {
"content": "这里是平台返回的完整回答。",
"brandMentioned": true,
"mentionCount": 1,
"citations": []
}
}
]
}
客户端可以读取 platformRuns 中的逐条结果,并根据 answer 是否存在决定展示方式。
引用、截图和分享能力因平台、配置及采集结果而异,不能假定每条回答都包含所有附加内容。
六、失败时返回什么
需要区分两类失败。
第一类:请求被接口拒绝
例如余额不足:
402 Payment Required
{
"error": "余额不足,无法创建任务。",
"code": "INSUFFICIENT_BALANCE",
"requestId": "example-request-id"
}
当前服务商接口的错误结构是:
error 面向调用者的错误说明
code 程序判断使用的错误标识
requestId 定位日志使用的请求编号
注意,error 是字符串,不应按其他平台常见的 error.message 嵌套结构读取。
第二类:查询请求成功,但采集任务失败
HTTP 状态仍然可以是:
200 OK
返回内容:
{
"id": "example-run-id",
"status": "failed",
"completedChecks": 0,
"failedChecks": 1,
"platformRuns": [
{
"id": "example-sample-id",
"platformId": "doubao",
"status": "failed",
"errorCode": "CAPTCHA_REQUIRED",
"errorMessage": "平台要求人工验证,请联系管理员处理后重试。",
"retryable": false,
"requiresHuman": true,
"answer": null
}
]
}
这并不矛盾:
- HTTP 200 表示"查询任务记录成功"。
status: failed表示"该任务的采集结果失败"。
因此,客户端必须检查 HTTP 状态、任务状态、逐条样本状态。
七、HTTP 状态码与处理方式
下面列出核对版本中的主要情况。业务错误代码可能比表中的通用代码更细。
| HTTP 状态码 | 含义 | 常见代码或情况 | 调用方处理 |
|---|---|---|---|
| 200 | 查询成功 | 平台、任务、报告查询 | 继续检查任务和样本状态 |
| 201 | 创建成功 | 创建品牌、添加问题 | 保存返回的资源编号 |
| 202 | 任务已受理 | 创建监控任务 | 保存任务编号并查询 |
| 400 | 请求不合法 | INVALID_REQUEST |
修正字段、参数或幂等标识 |
| 401 | 鉴权失败 | UNAUTHORIZED |
检查 Key 是否有效、过期或撤销 |
| 402 | 余额不足 | INSUFFICIENT_BALANCE |
补充余额后再发起 |
| 403 | 无权访问或调用被关闭 | FORBIDDEN、API_KEY_DISABLED 等 |
检查权限、授权品牌及接口开关 |
| 404 | 资源或接口不存在 | NOT_FOUND |
检查地址和资源编号 |
| 409 | 请求冲突 | IDEMPOTENCY_CONFLICT 等 |
检查幂等标识与请求内容 |
| 410 | 资源已过期 | IMAGE_EXPIRED |
检查证据保留状态 |
| 429 | 限流或额度限制 | RATE_LIMIT_EXCEEDED、DAILY_LIMIT_EXCEEDED 等 |
按 Retry-After 等待 |
| 500 | 服务内部异常 | INTERNAL_ERROR |
保存 requestId,联系管理员 |
代理服务器还可能返回 502、503、504,甚至 HTML 错误页面。这类响应不一定遵循应用的 JSON 错误结构。
DNS、连接和 TLS 证书验证失败,则可能根本没有 HTTP 状态码,调用方需要捕获网络异常。
任务业务状态
status |
含义 |
|---|---|
queued |
排队中 |
running |
执行中 |
completed |
已完成 |
partial_failed |
部分采集成功、部分失败 |
failed |
任务失败 |
cancelled |
已取消 |
典型采集错误
errorCode |
说明 | 建议 |
|---|---|---|
LOGIN_REQUIRED |
平台登录不可用 | 恢复采集环境登录 |
CAPTCHA_REQUIRED |
需要人工验证 | 完成验证,避免连续自动重试 |
BROWSER_PROFILE_BUSY |
浏览器配置正在使用 | 等待或关闭冲突会话 |
PLATFORM_NETWORK_ERROR |
平台网络异常 | 检查网络后有限重试 |
COLLECTION_TIMEOUT |
采集超时 | 查看阶段和证据再决定 |
UPSTREAM_TIMEOUT |
上游等待超时 | 核对原上游任务,避免重复提交 |
UPSTREAM_BALANCE_ERROR |
上游账户余额不足 | 管理员处理,不等同于客户余额不足 |
八、Python 完整调用案例
下面把请求封装和业务流程放在两个文件中。示例使用 Python 标准库,无需额外安装 HTTP 包。Python urllib 文档
文件一:client.py
import json
import os
from urllib.request import Request, urlopen
from urllib.error import HTTPError
class ApiError(Exception):
def __init__(self, status, body):
self.status = status
self.body = body
super().__init__(f"HTTP {status}: {body}")
class ZhisouClient:
def __init__(self):
self.base_url = os.environ["ZHISOU_BASE_URL"].rstrip("/")
self.api_key = os.environ["ZHISOU_API_KEY"]
def request(self, method, path, payload=None, idem_key=None):
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
}
data = None
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(
payload, ensure_ascii=False
).encode("utf-8")
if idem_key:
headers["Idempotency-Key"] = idem_key
request = Request(
self.base_url + path,
data=data,
headers=headers,
method=method,
)
try:
with urlopen(request, timeout=30) as response:
return json.load(response)
except HTTPError as error:
raw = error.read().decode("utf-8", errors="replace")
try:
body = json.loads(raw)
except json.JSONDecodeError:
body = {"error": "服务返回了非 JSON 响应"}
raise ApiError(error.code, body) from error
文件二:monitor.py
import os
import time
from urllib.parse import quote
from client import ZhisouClient
api = ZhisouClient()
# 同一次业务重试必须沿用这个标识及相同参数。
request_key = os.environ["ZHISOU_REQUEST_KEY"]
created = api.request(
"POST",
"/api/v1/runs",
{
"brandId": os.environ["ZHISOU_BRAND_ID"],
"questionIds": [os.environ["ZHISOU_QUESTION_ID"]],
"platforms": ["deepseek"],
"repeatCount": 1,
"intervalSeconds": 60,
},
idem_key=request_key,
)
run_id = created["id"]
print("任务已受理:", run_id)
terminal = {"completed", "partial_failed", "failed", "cancelled"}
deadline = time.monotonic() + 600
while time.monotonic() < deadline:
run = api.request(
"GET",
"/api/v1/runs/" + quote(run_id, safe=""),
)
print("当前状态:", run["status"])
if run["status"] in terminal:
for sample in run.get("platformRuns", []):
answer = sample.get("answer")
if sample["status"] == "completed" and answer:
print(sample["platformId"], answer["content"])
else:
print(
sample["platformId"],
sample.get("errorCode"),
sample.get("errorMessage"),
)
break
time.sleep(10)
else:
print("本次等待结束,任务未必失败。")
print("请保留任务编号,稍后继续查询:", run_id)
示例没有自动重试失败的创建请求,避免在状态不明确时误建任务。生产客户端可以进一步加入针对 GET 请求的有限重试,以及 429 的等待处理。
客户端停止等待,不会自动取消服务端任务。
九、其他语言调用示例
下面四种语言查询同一个任务接口,使用相同的环境变量:
ZHISOU_BASE_URL 部署根地址,不包含 /api/v1
ZHISOU_API_KEY 服务商密钥
ZHISOU_RUN_ID 已有任务编号
1. JavaScript / Node.js
以下代码在服务端运行,使用支持内置 fetch 的 Node.js 环境。
// get-run.mjs
const base = process.env.ZHISOU_BASE_URL.replace(/\/$/, "");
const runId = encodeURIComponent(process.env.ZHISOU_RUN_ID);
const response = await fetch(`${base}/api/v1/runs/${runId}`, {
headers: {
Authorization: `Bearer ${process.env.ZHISOU_API_KEY}`,
Accept: "application/json"
},
signal: AbortSignal.timeout(30000)
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error(`HTTP ${response.status}:非 JSON 响应`);
}
if (!response.ok) {
throw new Error(
`HTTP ${response.status} ${data.code}: ${data.error}`
);
}
console.log("任务状态:", data.status);
console.log("样本结果:", data.platformRuns);
Node.js 的 fetch 提供标准 HTTP 请求接口,网络异常或超时需要由上层捕获处理。Node.js fetch 文档
2. Java
以下使用 Java 11 及以上的 HttpClient。为了不引入额外依赖,先展示状态码和原始 JSON;业务项目可使用自己的 JSON 库解析 status。
// GetRun.java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class GetRun {
public static void main(String[] args) throws Exception {
String base = System.getenv("ZHISOU_BASE_URL")
.replaceAll("/+$", "");
String id = URLEncoder.encode(
System.getenv("ZHISOU_RUN_ID"),
StandardCharsets.UTF_8
);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/api/v1/runs/" + id))
.timeout(Duration.ofSeconds(30))
.header("Authorization",
"Bearer " + System.getenv("ZHISOU_API_KEY"))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
);
System.out.println("HTTP " + response.statusCode());
System.out.println(response.body());
// HTTP 200 后,仍需解析 JSON 中的任务 status。
}
}
3. PHP
需要启用 cURL 扩展。
<?php
$base = rtrim(getenv("ZHISOU_BASE_URL"), "/");
$id = rawurlencode(getenv("ZHISOU_RUN_ID"));
$ch = curl_init("$base/api/v1/runs/$id");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ZHISOU_API_KEY"),
"Accept: application/json"
]
]);
$raw = curl_exec($ch);
if ($raw === false) {
$message = curl_error($ch);
curl_close($ch);
throw new RuntimeException($message);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400) {
throw new RuntimeException(
"HTTP $status: " . ($data["error"] ?? "请求失败")
);
}
echo "任务状态:" . ($data["status"] ?? "未知") . PHP_EOL;
4. C# / .NET
// Program.cs
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
var baseUrl = Environment.GetEnvironmentVariable(
"ZHISOU_BASE_URL")!.TrimEnd('/');
var runId = Uri.EscapeDataString(
Environment.GetEnvironmentVariable("ZHISOU_RUN_ID")!);
using var client = new HttpClient {
Timeout = TimeSpan.FromSeconds(30)
};
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(
"Bearer",
Environment.GetEnvironmentVariable("ZHISOU_API_KEY")
);
using var response = await client.GetAsync(
$"{baseUrl}/api/v1/runs/{runId}");
var raw = await response.Content.ReadAsStringAsync();
using var json = JsonDocument.Parse(raw);
if (!response.IsSuccessStatusCode) {
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine(raw);
return;
}
Console.WriteLine(
json.RootElement.GetProperty("status").GetString()
);
这些示例共用同一套接口契约。实际接入时,需要检查环境变量是否齐全,并统一处理网络错误、非 JSON 响应、HTTP 错误与业务失败。本次未用这些示例发起线上付费采集。
十、重点说明:支持本地化部署使用
本地化部署意味着应用服务、业务数据库和证据文件可以部署在客户自己的环境中。
可以选择:
| 部署方式 | 适用场景 |
|---|---|
| 单台电脑 | 演示、测试、小规模使用 |
| 企业内网服务器 | 团队共享访问,限制外部入口 |
| 企业自有云服务器 | 多地点访问、服务商对接 |
| 专用私有环境 | 按企业要求管理账号、网络和数据 |
客户端安装在什么语言的系统中,不影响本地 API 的调用方式。部署完成后,只需将客户端的 ZHISOU_BASE_URL 改为实际访问地址。
本地化部署不等于完全离线
需要明确区分:
- 品牌、问题、历史结果和相关配置可以保存在本地。
- 调用外部 AI 平台仍需访问该平台。
- 使用第三方采集接口时,问题和必要参数会发送给对应服务。
- 浏览器采集需要维护部署环境中的登录状态。
- 完全隔离网络下新增采集是否可用,取决于是否另有本地模型或内网采集适配;当前不能直接宣称已支持。
因此,企业部署前应确定:哪些数据留在本地、哪些请求需要外发,以及允许连接哪些目标服务。
十一、本地部署的实施步骤
以下是基于当前工程脚本的基础流程,不是"一条命令完成所有生产配置"的承诺。
第一步:准备运行环境
准备经过验证的 Node.js 版本、npm、源码及锁文件。
浏览器采集还需要安装受支持的 Chrome 或 Edge,并确保运行账号能够访问浏览器与数据目录。Linux 有界面采集还需要相应的显示环境。
前后端依赖按目标操作系统安装,不能直接把 Windows 的原生依赖复制到 Ubuntu 使用。
第二步:进入项目目录
必须进入包含 package.json 的项目根目录:
cd /opt/zhisou
npm ci
npm run build:web
npm ci 根据锁文件安装依赖;npm run build:web 构建前端资源。
第三步:配置存储与监听地址
Linux 示例:
export GEO_DATA_DIR=/srv/zhisou/data
export HOST=127.0.0.1
export PORT=3030
当前路径逻辑会在数据目录下组织:
/srv/zhisou/data/
brand-monitoring.sqlite
artifacts/
profiles/
对应:
- 数据库;
- 截图与证据;
- 采集浏览器配置及登录状态。
这些目录需要持久化,并由受限的服务账号访问。
第四步:初始化管理员
在受控环境中提供 GEO_ADMIN_USER 和 GEO_ADMIN_PASSWORD,执行:
npm run admin:init
此脚本用于首次初始化;核对版本中已经存在平台管理员时会拒绝重复初始化。密码不要写入公开文档或源码。
第五步:配置采集环境
浏览器路径由部署环境决定,可通过:
GEO_BROWSER_PATH
指定。
需要登录对应租户的采集浏览器时,在项目目录执行:
npm run tenant:login -- default deepseek
这里的 default 是示例租户编号。登录状态保存在采集环境,而不自动继承日常浏览器中的登录。
第六步:启动并配置访问入口
基础启动命令:
npm start
生产运行建议交给服务管理器,并通过 Nginx 提供 HTTPS 入口。
若后端监听 127.0.0.1:3030,局域网其他电脑无法直接访问这个监听地址,需要通过反向代理入口访问,或按部署方案配置指定内网监听地址与防火墙。
客户端使用内网地址时,也应配置受信任的证书或企业 CA,不要通过关闭证书验证来长期使用。
第七步:完成部署验收
至少检查:
- 管理员能够登录。
- 品牌、问题可以正常读取。
- API Key 权限与品牌授权有效。
- 余额、定价与调用额度正确。
- 最小监控任务可以进入终态。
- 成功与失败样本均有清晰返回。
- 截图访问需要正确授权。
- 服务重启后数据仍然存在。
- 数据备份能够在另一环境恢复。
十二、服务商接入时最重要的三个判断
先判断请求是否成功,再判断任务是否完成,最后判断每条样本是否有效。
HTTP 是否为成功状态?
├─ 否:处理鉴权、余额、限流或服务错误
└─ 是:读取任务 status
├─ queued / running:继续查询
└─ 终态:逐条检查 platformRuns
├─ completed 且有 answer:使用结果
└─ 失败或取消:记录原因并按规则处理
通过这套方式,智叟云集系统能够为不同技术栈提供统一的监控服务入口,同时让企业选择自有环境部署、内网使用或服务商系统集成。