OpenGEO
← 返回博客

教程 · 11 分钟阅读

从本地 Mock 到一个完整的 Job

From a local Mock to a completed Job

发布 2026-10-13 · 更新 2026-10-13 · OpenGEO

这篇教程用本地 Mock 理解 OpenGEO 的 Job 流程。你会创建一个任务,使用同一个 ID 查询进度,再读取属于该任务的观察结果。模拟回答与 Demo Brand 都是虚构数据,不表示实际监测效果。

准备环境并启动 Mock

需要 Node.js 22 或更高版本、Git 和 pnpm 11。在一个终端运行下面的命令;Mock 只监听本机 127.0.0.1,保持这个终端打开。首次安装依赖需要网络,之后的监测示例只请求本机。

bash
git clone https://github.com/jiangqiusuo/OpenGEO.git
cd OpenGEO
pnpm install
pnpm mock

另开终端执行示例

进入同一个 OpenGEO 目录,再运行已包含在仓库里的脚本。Windows PowerShell、macOS 和 Linux 都可以使用这条 Node 命令,不需要在命令中粘贴密钥。

bash
node examples/mock-to-job.mjs

默认地址是 http://127.0.0.1:8787。若端口已被占用,在启动 Mock 的终端设置 OPEN_GEO_PORT,再把对应的本机地址作为脚本的唯一参数,例如 node examples/mock-to-job.mjs http://127.0.0.1:8788。脚本拒绝外网地址和重定向,避免把教程请求发到真实服务。

一条请求发生了什么

POST /v1/monitor-runs 返回 202 和 queued 状态。响应中的 id 是之后查询与结果关联的依据,Location 指向这个 Job。创建时使用新的幂等键;同一个键与相同请求会返回同一个 Job,改动问题则会返回冲突。

本地模拟器第一次查询返回 running,第二次返回 succeeded 与 complete,然后 items 返回对应的观察结果。这是可预测的教学流程;真实服务的等待时长、部分结果和失败原因应按 API 返回值判断,不能用两次查询推断真实任务已经完成。

json
{
  "source": "local-mock",
  "job_id": "job_mock_1",
  "status_trace": ["queued", "running", "succeeded"],
  "result_state": "complete",
  "observation_count": 1
}

上面的输出摘录省略了 answer;任务编号取决于本机 Mock 已创建的任务数量。检查 source 为 local-mock、最后状态为 succeeded,以及每条观察的 job_id 与任务一致。

可复制的完整代码

下面的代码直接来自仓库里的 examples/mock-to-job.mjs,也是页面下载的同一份文件。轮询最多八次,每次请求有超时;遇到失败、取消、部分定稿或超出窗口会报错,不会一直等待。

javascript
/* global console, process, fetch, AbortSignal */
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
import { pathToFileURL, URL } from 'node:url';
import { setTimeout as delay } from 'node:timers/promises';

export async function runTutorial(base = 'http://127.0.0.1:8787') {
  const origin = new URL(base);
  if (origin.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(origin.hostname) || origin.pathname !== '/' || origin.username || origin.password || origin.search || origin.hash) {
    throw new Error('This tutorial only accepts a loopback HTTP origin.');
  }
  const request = async (path, options = {}) => {
    const response = await fetch(new URL(path, origin), { ...options, redirect: 'error', signal: AbortSignal.timeout(5000) });
    if (!response.ok) throw new Error(`Mock returned HTTP ${response.status}`);
    return response.json();
  };
  const created = await request('/v1/monitor-runs', {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'idempotency-key': randomUUID() },
    body: JSON.stringify({ capability_id: 'observe.ai_answer', prompts: ['Which tools help teams understand AI search visibility?'], interaction_mode: 'search', turnaround_class: 'best_effort' })
  });
  if (!/^job_[a-z0-9_]+$/.test(created.id)) throw new Error('Invalid Job ID');
  const trace = [created.status];
  let job = created;
  for (let attempt = 0; attempt < 8 && job.status !== 'succeeded'; attempt++) {
    await delay(100);
    job = await request(`/v1/jobs/${encodeURIComponent(created.id)}`);
    trace.push(job.status);
    if (['failed', 'cancelled', 'finalized_partial'].includes(job.status)) throw new Error(`Job ended with ${job.status}`);
  }
  if (job.status !== 'succeeded' || job.result_state !== 'complete') throw new Error('Mock did not finish within the bounded polling window');
  const items = await request(`/v1/jobs/${encodeURIComponent(job.id)}/items`);
  if (!Array.isArray(items.data) || items.data.length !== job.progress.completed_items || items.data.some((item) => item.job_id !== job.id)) throw new Error('Observations do not match the completed Job');
  return { source: 'local-mock', job_id: job.id, status_trace: trace, result_state: job.result_state, observation_count: items.data.length, answer: items.data[0]?.answer.text };
}

if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
  runTutorial(process.argv[2]).then((result) => console.log(JSON.stringify(result, null, 2))).catch((error) => { console.error(error.message); process.exitCode = 1; });
}

常见问题与下一步

连接失败时先确认 Mock 终端还在运行、端口一致。端口被占用时请换一个可用的本机端口,不要停止不认识的服务。Mock 只模拟字符串问题、search 和 best_effort;其他配置会明确拒绝,生成和发布入口尚未模拟。最多保存一百个任务,达到上限后重启 Mock 会清空虚构数据。

job_demo_* 是工作台使用的固定 Fixture,不会随轮询推进;本教程创建的是 job_mock_*。完成后在 Mock 终端按 Ctrl+C 停止服务。接入真实 API 时,再阅读开发者文档的认证、能力、部分结果和错误处理说明;不要把这份只允许本机的教程脚本直接当作生产客户端。

下载同一份示例脚本