Driving 交底 Desk over HTTP
Everything the page does, you can do from a script. One endpoint does the work; the rest is
authentication and polling. The app takes project material — a design note, a
README, a scheme description, an extract of code comments — and, where you have one, a
技术交底书 draft over that same material, plus a task field naming which
of four lanes to run. It returns a single JSON envelope, in Simplified Chinese, that a Chinese patent
attorney can work from.
https://api.skillsafe.ai/v1/app-api
The only headers on any call are Authorization: Bearer <token> and
Content-Type: application/json — plus Idempotency-Key on
/run and /run-stream. There is no slug header. The app
slug is named in exactly one place: the JSON body of POST /guest, as
{"slug":"jiaodi-desk"}. Get a token from
the token page without opening a developer console.
The response envelope
Every endpoint returns the same wrapper. Success carries data; failure carries
error. Nothing returns a bare value, so a client can branch on the presence of
error alone.
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "VALIDATION_ERROR", "message": "...", "details": { ... }}}
Error codes
| Code | HTTP | What it means | What to do |
|---|---|---|---|
| UNAUTHORIZED | 401 | Missing, malformed or expired token. | Mint a new one. Guest tokens expire; personal tokens outlive them. |
| FORBIDDEN | 403 | The token is valid but not for this app. | Mint the token against this app: POST /guest with {"slug":"jiaodi-desk"}. |
| VALIDATION_ERROR | 400 | The input did not match the app's shape. | Read error.details; it names the offending field. |
| PAYMENT_REQUIRED | 402 | Balance below the run's minimum. | Call /estimate first and compare against /me. |
| RATE_LIMITED | 429 | Too many requests. | Back off and retry; never tight-loop. |
| JOB_FAILED | 200 | The job reached a terminal failed state. | Returned inside a job payload, not as an HTTP error. Check status. |
The task field comes first
交底 Desk is a four-lane app. Every run must name its lane in task,
because all four lanes share one system prompt and one model and are routed by that field alone. The
contract is to produce that lane only and never a blend of two. If task is
missing or is not one of the four ids, the model picks the closest lane for the input it was given,
produces that lane's contract in full, and says so by setting lane to what it chose plus
lane_inferred to true — a fallback for a malformed request, not a
feature to rely on. Send the lane explicitly.
task | Lane | What it does | Required input | artifact.kind |
|---|---|---|---|---|
| mine | 专利点挖掘 | Mines the patentable points out of the project material. Every point has to land on a 技术问题 → 技术手段 → 有益效果 triple, carries a novelty_grade and lists what is still missing before anyone drafts. Two to six points, ranked. | material | markdown |
| draft | 交底书成文 | Writes the whole 技术交底书 against the desensitised template: 发明名称, 技术领域, 背景技术 (with the drawbacks of current practice), 发明内容 (问题 / 技术方案 / 有益效果), 附图说明, 具体实施方式, 术语表. Figures come back as mermaid source, never images. | material | disclosure |
| novelty | 查新检索方案 | Drafts the novelty search for a human to execute: distinguishing features, IPC candidates, paste-ready query strings for CNIPA, Google Patents and Espacenet, per-feature knockout criteria and an execution protocol. The model never searches and never returns a patent number. With kept search_results (see "Executing the NPL search") it also returns a per-feature comparison table over those records. | material or disclosure | queries |
| check | 交底书自检 | The pre-finalisation self-check: 逻辑闭环 (§8.4 — problem, means and effect must interlock) and 公式参数一致性 (§8.5 — every symbol defined, units and ranges consistent), plus section completeness, a desensitisation recheck, figure consistency and whether the embodiment is actually reproducible. | disclosure | markdown, or none when nothing needed rewriting |
material is required by mine and draft;
disclosure is required by check; novelty needs
at least one of the two and prefers disclosure when both arrive. Send a
lane without its required input and the run comes back posture: "blocked", saying what is
missing rather than inventing it — and it still costs a run, so check client-side first. The
natural order through the app is mine → draft →
check, with novelty sensible on either side of draft, and every
response names what it thinks comes next in next_lane.
Input fields
| Field | Type | Required | Notes |
|---|---|---|---|
| task | string | yes | One of the four lane ids above. |
| patent_type_hint | string | no | auto (the default), invention 发明, utility 实用新型 or design 外观设计. On auto the model decides and justifies the choice in assumptions, or returns patent_type: "undetermined" and asks in open_questions. A type you name explicitly is never silently swapped; if the model disagrees it raises a finding. |
| material | string | mine, draft | The project material itself — design document, README, scheme description, extracted code comments. Required by mine and draft; one of material / disclosure is required by novelty; optional on check, where it is used to check that what the draft asserts has a source. |
| disclosure | string | check | The 技术交底书 draft, verbatim. Required by check. On draft it turns the run into a revision: what holds is kept, what is broken is fixed, and the style is not rewritten wholesale. On novelty it is the preferred basis for the search. |
| point | string | no | draft only: the patent point you chose, usually a body.points[].name from an earlier mine run. Omit it and the model mines the material itself, writes the strongest point it finds, and records body.point_used.source: "mined". |
| context | string | no | Free-text notes: what you believe is new, what the competing product does, what has already been published or shipped, who the reader is. The novelty lane leans on it when scoping the field of search. |
| search_results | array | no | novelty / check only: provenance-stamped web-retrieval records the user kept after running POST /v1/app-api/search (providers web.wikipedia, web.duckduckgo — declared by this app's release). Each record: {record_id, title, abstract, url, provider, retrieved_at}. The model may cite only these record_ids; on novelty it returns a per-feature comparison table over them (body.comparison), on check it raises findings where the draft's novelty claims collide with a record. These are non-patent-literature probes — a miss is not novelty, and the patent-database part of 查新 remains a human-executed plan. See "Executing the NPL search" below. |
| prescan | object | no | {flags, resources} — deterministic facts from the in-browser scanner that the model must reconcile. The web page always sends it; API callers may omit it entirely. See below. |
| clip_note | string | no | Send only when the input was too long to transmit whole; it names what was cut. The model then works around the gap, does not speculate about the removed part and does not comment on what it was not shown. |
| retry_note | string | no | Sent only on an automatic re-ask after a malformed reply. It quotes the parse error and restates the contract; the model answers the same lane on the same input and returns only the JSON object. Reuse the idempotency key with the attempt number appended so a retry cannot double-bill. |
The request body is the input object itself. POST /run,
POST /run-stream and POST /estimate all take
{"task": ..., "material": ...} at the top level. An {"input": {...}} wrapper
is accepted by the platform and returns 200, which is the trap: the model never
sees task or material and answers a lane you did not ask for, on material it
was never given. Never wrap the body.
The output envelope
All four lanes return exactly the same outer object. Only body is per-lane. The reply is
one JSON object and nothing else — no prose before it and no code fence around
it — and it arrives as a string in data.output.output, so you parse it yourself.
Every value except the JSON keys is Simplified Chinese; the English keyword fields of the
novelty lane are the one exception.
{
"lane": "mine | draft | novelty | check",
"lane_inferred": false,
"invention": "发明主题短语,不超过 30 字",
"title": "本次运行的标题,不超过 40 字",
"patent_type": "invention | utility | design | undetermined",
"posture": "ready | needs-work | blocked",
"verdict": "一句话结论",
"summary": "3-6 句话,可直接粘进工作记录",
"assumptions": ["模型替你做的假设,逐条"],
"open_questions": ["材料没写、必须回头问人的问题"],
"findings": [
{"id": "JD-001", "title": "一句祈使句标题",
"severity": "critical | high | medium | low",
"area": "mining | novelty | structure | closure | formula | desensitize | clarity | search | drafting | formalities",
"section": "涉及的章节名,可空", "term": "涉及的词或符号,可空",
"evidence": "材料原文摘录,不超过 200 字",
"why": "后果,不超过 600 字",
"fix": "怎么改",
"fix_text": "改后的文字,或空串"}
],
"coverage_check": [
{"flag_id": "JD-M02", "status": "confirmed | set-aside | superseded",
"finding_id": "JD-001", "note": "set-aside 时说明为什么不算问题"}
],
"artifact": {"kind": "none | disclosure | queries | markdown",
"filename": "技术交底书-充电桩排队调度.md",
"content": "整份产出,kind 为 none 时是空串"},
"next_lane": {"lane": "check", "reason": "一句话"},
"body": { }
}
findingsids runJD-001upward —JD-001,JD-002, sequential, no gaps, most severe first. Acoverage_checkentry points at one of them byfinding_id.bodyis the only per-lane part. Everything above it has the same keys and the same vocabulary in all four lanes, so one parser handles every response and switches onlaneonly to readbody.postureuses the same three values everywhere.readymeans this lane's output can go straight to the next step;needs-workthat it is usable but the listed problems should be dealt with first;blockedthat a required input is absent, in which case the model says what is missing instead of producing the artifact anyway.- Every array is present even when empty — an empty array, never
nulland never a missing key. You can index without guarding. severityis calibrated to Chinese practice:critical= the 交底书 cannot be handed over or the patent point does not stand up;high= the attorney or examiner sends it back;medium= it measurably lowers the quality;low= polish.next_lane.laneis one of the four lane ids, or""when nothing sensible follows — which is what a cleancheckrun returns, being the end of the flow.
The per-lane body
Four shapes, one per lane. mine:
{
"points": [{
"name": "点名,不超过 20 字",
"patent_type": "invention | utility | design",
"problem": "要解决的技术问题",
"solution": "技术手段,写清区别于常规做法之处",
"effect": "有益效果,有数字就用数字",
"novelty_grade": "high | medium | low",
"prior_art_risk": "疑似撞车的常规做法方向,不写专利号",
"evidence": "支撑此点的材料原文摘录",
"missing_info": ["写交底书前还缺的参数、对比数据、边界条件"]
}],
"ranking_note": "排序理由,1-3 句",
"recommended_point": "第一名的 name"
}
draft:
{
"point_used": {"name": "被写成交底书的专利点", "source": "user | mined"},
"sections": [{"id": "s1", "name": "发明名称",
"status": "drafted | thin | missing-input", "note": ""}],
"desensitized": [{"hint": "被替换内容的类别提示,不复述原文", "placeholder": "本申请方"}],
"figures": [{"fig_no": 1, "caption": "图1:系统架构示意",
"mermaid": "flowchart TD; A[调度单元] --> B[充电桩]"}]
}
novelty:
{
"features": [{"feature": "区别技术特征,不是泛泛的领域词",
"cn_keywords": ["中文关键词组"],
"en_keywords": ["english keyword group"],
"synonyms": ["同义扩展,含行业黑话"]}],
"ipc_candidates": [{"code": "G06Q 50/06", "why": "依据"}],
"queries": [{"database": "CNIPA | Google Patents | Espacenet | incoPat",
"query": "可直接粘贴的检索式,用该库真实语法",
"purpose": "这条式子想命中或排除什么"}],
"knockout": [{"feature": "特征", "criterion": "看到什么样的现有文献即判该特征不新"}],
"protocol": ["执行顺序建议,4-8 条"],
"comparison": [{"record_id": "srch_…(仅当输入携带 search_results;逐字来自送入列表)",
"verdicts": [{"feature": "…", "verdict": "hit | miss | unclear", "note": "…"}],
"overall": "closest | relevant | background | off-topic",
"note": "该记录对初判的影响"}],
"comparison_note": "整体结论;无 search_results 时 comparison 为 [] 且本字段为空串"
}
check:
{
"closure": {"problem_ok": true, "solution_ok": true, "effect_ok": false,
"gaps": ["效果3无对应技术手段"]},
"formula_check": [{
"formula": "公式原文",
"defined": ["T_d", "P_max"],
"symbols_undefined": ["η"],
"unit_issue": "…或空串",
"range_issue": "…或空串",
"note": "…"
}],
"section_reviews": [{"section": "背景技术",
"verdict": "ok | thin | missing | broken",
"issues": ["…"]}],
"rewrites": 2
}
formula_check is an empty array when the 交底书 contains no formulas, and
summary then says the §8.5 pass was skipped rather than leaving you to infer it.
rewrites is a count, and it must agree with the number of findings carrying a
non-empty fix_text.
The prescan contract
The browser app runs a deterministic scanner over material and disclosure
before every run — free, no model call — and passes what it found in
prescan.flags, each with a stable id. The model must return exactly one
coverage_check entry per flag id sent, and none for ids that were not sent. That
is what lets the free scanner hold the paid run accountable: anything unaccounted for is a defect you
can detect programmatically.
sent = {f["id"] for f in payload.get("prescan", {}).get("flags", [])}
covered = {c["flag_id"] for c in result["coverage_check"]}
assert sent == covered, f"unreconciled: {sent - covered}; invented: {covered - sent}"
# resources are context, not findings: they must NOT appear in coverage_check
res = {r["id"] for r in payload.get("prescan", {}).get("resources", [])}
assert not (covered & res), f"resource reconciled as a finding: {covered & res}"
status is one of three values. confirmed — the scanner was right and
finding_id names the JD-* finding that handles it. set-aside
— the scanner is technically correct but it is not a problem in this lane on this material, and
note says why. superseded — a deeper finding swallows it, and
finding_id names that one. A flag is never silently dropped.
The flag shape
Each entry in prescan.flags is
{id, label, severity, detail, occurrences}.
| Key | Type | What it holds |
|---|---|---|
| id | string | The stable rule id. This is the value that must come back as a coverage_check.flag_id. The page's own ids look like JD-M02 — the letter is the lane the rule belongs to — but the id is opaque to the API: the model reconciles whatever ids you send. |
| label | string | The rule's one-line description in Chinese, e.g. 效果段落没有任何量化数字. |
| severity | string | critical, high, medium or low. Flags arrive sorted by severity in that order. |
| detail | string | Free text when the rule has a countable fact to add, e.g. 公司名疑似出现 3 处. Empty string otherwise. |
| occurrences | number | How many times the rule fired across the whole input. Flags are deduplicated by rule id before they are sent, so one rule firing in six places is one flag with occurrences: 6 — and one coverage_check entry still covers the group, but the contract forbids the model implying a single site when the count says otherwise. |
Where the model and the scanner disagree on a countable fact — how many times a term
appears, whether a symbol is defined anywhere, whether a section heading exists —
the scanner is right, because it is deterministic and the model is not. Where they
disagree about whether the flagged text is actually a problem, the model may say so; that is what
set-aside is for.
prescan.resources is context, not findings
prescan.resources is a separate array of {id, label} entries stating what the
scanner counted, not what it objects to: how many characters of material, which template sections carry
a heading, how many figures are referenced, how many formulas and distinct symbols were seen, whether
any quantified effect appears at all. They need no coverage_check entry,
they are not problems, and the model is instructed not to manufacture a finding merely to mention one.
"resources": [
{"id": "JR-MATERIAL-CHARS", "label": "材料 1 840 字"},
{"id": "JR-SECTIONS", "label": "已有标题的章节:技术领域、背景技术、发明内容"},
{"id": "JR-FIGURES", "label": "正文引用的图号:图1、图2"},
{"id": "JR-FORMULAS", "label": "2 个公式,7 个不同符号"},
{"id": "JR-QUANTIFIED", "label": "出现量化效果:480 kW、390 kW、19%"}
]
Flags are filtered to the lane being run
Every rule declares which lanes it belongs to, and the page sends only the rules for the lane you are
about to run. The same material therefore produces a different flag set in each lane
— a real defect that is out of lane is simply not sent, so the model is never asked to reconcile
something it is not being paid to look at. A formula-consistency rule reaches check and
nowhere else; a "no quantified effect" rule reaches mine and draft;
a desensitisation rule reaches every lane that emits a file. Compute prescan per lane; do
not cache one lane's flags and resend them on another.
Renaming a flag id breaks the reconciliation contract in both directions: the model has no entry to return and your assertion has nothing to match. Treat your flag ids as an interface.
You may omit prescan entirely. It is an optional field and API callers
usually have no scanner to feed it from. The lane still runs; it simply has fewer deterministic facts
to ground itself in, coverage_check comes back as an empty array, and nothing checks the
model's counting for you. Every worked example below except the first omits it.
1. A tiny client
A few lines of setup that every later step reuses: the base URL, the bearer token, and a JSON post
that raises on the error branch of the envelope. Two headers, no more —
Authorization and Content-Type. The slug constant is here only because
step 2 needs it in a request body; it never becomes a header. Replace the
"YOUR_TOKEN" placeholder by reading the token from wherever your program keeps secrets
rather than committing it.
# Every call in this document uses these three values.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="jiaodi-desk" # used once, in the POST /guest body
TOKEN="YOUR_TOKEN" # from https://jiaodi-desk.skillsafe.ai/tokens.html
post() { # post <path> <json>
curl -sS -X POST "$BASE$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}
import json
import urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "jiaodi-desk" # used once, in the POST /guest body
TOKEN = "YOUR_TOKEN" # from https://jiaodi-desk.skillsafe.ai/tokens.html
def call(path, payload=None, method="POST", idempotency_key=None):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
req = urllib.request.Request(BASE + path, data=body, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
if idempotency_key:
req.add_header("Idempotency-Key", idempotency_key)
with urllib.request.urlopen(req) as resp:
envelope = json.loads(resp.read())
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "jiaodi-desk"; // used once, in the POST /guest body
const TOKEN = "YOUR_TOKEN"; // from https://jiaodi-desk.skillsafe.ai/tokens.html
async function call(path, payload, method = "POST", idempotencyKey) {
const headers = {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
};
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
const res = await fetch(BASE + path, {
method,
headers,
body: payload === undefined ? undefined : JSON.stringify(payload)
});
const envelope = await res.json();
if (!envelope.ok) {
throw new Error(`${envelope.error.code}: ${envelope.error.message}`);
}
return envelope.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "jiaodi-desk" // used once, in the POST /guest body
token = "YOUR_TOKEN" // from https://jiaodi-desk.skillsafe.ai/tokens.html
)
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(method, path string, payload any, idemKey string) (json.RawMessage, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
if idemKey != "" {
req.Header.Set("Idempotency-Key", idemKey)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class JiaodiDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "jiaodi-desk"; // used once, in the POST /guest body
static final String TOKEN = "YOUR_TOKEN"; // from https://jiaodi-desk.skillsafe.ai/tokens.html
static final HttpClient CLIENT = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, java.nio.charset.StandardCharsets.UTF_8))
.build();
HttpResponse<String> res = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) {
throw new RuntimeException("HTTP " + res.statusCode() + ": " + res.body());
}
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "jiaodi-desk" # used once, in the POST /guest body
TOKEN = "YOUR_TOKEN" # from https://jiaodi-desk.skillsafe.ai/tokens.html
def call(path, payload = nil, method = :post, idempotency_key: nil)
uri = URI(BASE + path)
req = method == :get ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idempotency_key if idempotency_key
req.body = JSON.dump(payload) unless payload.nil?
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope['error']['code']}: #{envelope['error']['message']}" unless envelope["ok"]
envelope["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "jiaodi-desk"; // used once, in the POST /guest body
const TOKEN = "YOUR_TOKEN"; // from https://jiaodi-desk.skillsafe.ai/tokens.html
function call(string $path, ?array $payload = null, ?string $idemKey = null): array {
$headers = [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
];
if ($idemKey !== null) {
$headers[] = "Idempotency-Key: " . $idemKey;
}
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => $payload !== null,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload === null ? "" : json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$envelope = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($envelope["ok"])) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public static class JiaodiDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "jiaodi-desk"; // used once, in the POST /guest body
const string Token = "YOUR_TOKEN"; // from https://jiaodi-desk.skillsafe.ai/tokens.html
static readonly HttpClient Client = new HttpClient();
public static async Task<JsonElement> CallAsync(
string path, object payload = null, string idemKey = null, HttpMethod method = null)
{
var req = new HttpRequestMessage(method ?? HttpMethod.Post, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (idemKey is not null) req.Headers.Add("Idempotency-Key", idemKey);
if (payload is not null)
{
req.Content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
}
var res = await Client.SendAsync(req);
var envelope = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!envelope.GetProperty("ok").GetBoolean())
{
var err = envelope.GetProperty("error");
throw new Exception($"{err.GetProperty("code")}: {err.GetProperty("message")}");
}
return envelope.GetProperty("data");
}
}
2. Get a token
A guest token is minted on demand and is enough for /me and
/estimate. Running a lane is metered, so it needs a
personal token — sign in at the token page and copy
it from there. This is the one and only call that names the app:
{"slug": "jiaodi-desk"} in the JSON body. Every POST /guest mints a
new guest identity, so reuse one token across a session rather than minting per request.
curl -sS -X POST "$BASE/guest" \
-H "Content-Type: application/json" \
-d "{\"slug\":\"$SLUG\"}" | tee guest.json
# {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
TOKEN=$(python3 -c "import json;print(json.load(open('guest.json'))['data']['token'])")
import json, urllib.request
body = json.dumps({"slug": SLUG}).encode()
req = urllib.request.Request(BASE + "/guest", data=body, method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as resp:
guest = json.loads(resp.read())["data"]
TOKEN = guest["token"] # reuse this for the whole session
print(guest["subject_type"]) # "guest"
const res = await fetch(`${BASE}/guest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: SLUG })
});
const guest = (await res.json()).data;
const token = guest.token; // reuse for the whole session
console.log(guest.subject_type); // "guest"
guestBody := bytes.NewReader([]byte(`{"slug":"jiaodi-desk"}`))
req, _ := http.NewRequest("POST", base+"/guest", guestBody)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
SubjectType string `json:"subject_type"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
fmt.Println(env.Data.SubjectType) // "guest"
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"jiaodi-desk\"}"))
.build();
HttpResponse<String> res = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
// {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
uri = URI(BASE + "/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.dump({ "slug" => SLUG })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
guest = JSON.parse(res.body)["data"]
token = guest["token"] # reuse for the whole session
puts guest["subject_type"] # "guest"
<?php
$ch = curl_init(BASE . "/guest");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => SLUG]),
]);
$guest = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
$token = $guest["token"]; // reuse for the whole session
echo $guest["subject_type"]; // "guest"
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/guest");
req.Content = new StringContent("{\"slug\":\"jiaodi-desk\"}", Encoding.UTF8, "application/json");
var res = await Client.SendAsync(req);
var guest = JsonDocument.Parse(await res.Content.ReadAsStringAsync())
.RootElement.GetProperty("data");
var token = guest.GetProperty("token").GetString(); // reuse for the session
Console.WriteLine(guest.GetProperty("subject_type")); // "guest"
3. Check who you are and what you can spend
GET /me is free and tells you the subject type and the credit balance. Compare that
balance against the estimate in the next step before you run anything — a 402 after submitting a
whole 交底书 is a failure of the client, not of the user.
curl -sS "$BASE/me" \
-H "Authorization: Bearer $TOKEN"
# {"ok":true,"data":{"subject_type":"user","credits":48210, ...}}
me = call("/me", method="GET")
print(me["subject_type"], me["credits"])
const me = await call("/me", undefined, "GET");
console.log(me.subject_type, me.credits);
data, err := call("GET", "/me", nil, "")
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
json.Unmarshal(data, &me)
fmt.Println(me.SubjectType, me.Credits)
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/me"))
.header("Authorization", "Bearer " + TOKEN)
.GET()
.build();
System.out.println(CLIENT.send(req, HttpResponse.BodyHandlers.ofString()).body());
me = call("/me", nil, :get)
puts "#{me['subject_type']} #{me['credits']}"
<?php
$ch = curl_init(BASE . "/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN],
]);
$me = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
echo $me["subject_type"] . " " . $me["credits"];
var me = await CallAsync("/me", method: HttpMethod.Get);
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
4. Price the run before you make it
POST /estimate is free, creates no job and charges nothing. It takes the same body the run
will take — the input object itself, not wrapped in an input
key — and returns the model, the markup and the two numbers that matter.
hold_credits is the amount reserved against your balance, priced at the full output
cap; the actual charge is usually far lower, so present it as reserved and never as the price.
min_credits is the floor you must be able to cover for the run to be accepted at all.
The hold differs per lane. The four lanes have different output caps — a
draft run that has to emit a whole 技术交底书, or a check run that rewrites
several paragraphs, reserves considerably more than a mine run. Estimate the lane you are
about to run, and re-estimate whenever task changes.
MATERIAL="园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车,实测午高峰峰值负载从 480 kW 降到 390 kW。"
post /estimate "{
\"task\": \"mine\",
\"patent_type_hint\": \"auto\",
\"material\": \"$MATERIAL\"
}"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":4380,"min_credits":420}}
MATERIAL = (
"园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。"
"改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车,"
"实测午高峰峰值负载从 480 kW 降到 390 kW,没有车超过承诺离场时间。"
)
payload = {
"task": "mine",
"patent_type_hint": "auto",
"material": MATERIAL,
}
est = call("/estimate", payload)
print(est["model"], est["hold_credits"], est["min_credits"])
if me["credits"] < est["min_credits"]:
raise SystemExit(f"short by {est['min_credits'] - me['credits']} credits")
const MATERIAL =
"园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。" +
"改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车," +
"实测午高峰峰值负载从 480 kW 降到 390 kW,没有车超过承诺离场时间。";
const payload = {
task: "mine",
patent_type_hint: "auto",
material: MATERIAL
};
const est = await call("/estimate", payload);
console.log(est.model, est.hold_credits, est.min_credits);
if (me.credits < est.min_credits) {
throw new Error(`short by ${est.min_credits - me.credits} credits`);
}
const material = "园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。" +
"改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车," +
"实测午高峰峰值负载从 480 kW 降到 390 kW。"
payload := map[string]any{
"task": "mine",
"patent_type_hint": "auto",
"material": material,
}
data, err := call("POST", "/estimate", payload, "")
if err != nil {
panic(err)
}
var est struct {
Model string `json:"model"`
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
}
json.Unmarshal(data, &est)
fmt.Println(est.Model, est.HoldCredits, est.MinCredits)
String material = "园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。"
+ "改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车,"
+ "实测午高峰峰值负载从 480 kW 降到 390 kW。";
// The body is the input object itself - there is no "input" wrapper.
String payload = """
{"task": "mine", "patent_type_hint": "auto", "material": %s}
""".formatted(JsonUtil.quote(material));
String estimate = call("/estimate", payload);
System.out.println(estimate);
// {"ok":true,"data":{"model":"gpt-5.6-terra","hold_credits":4380, ...}}
MATERIAL = "园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。" \
"改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车," \
"实测午高峰峰值负载从 480 kW 降到 390 kW。"
payload = {
"task" => "mine",
"patent_type_hint" => "auto",
"material" => MATERIAL
}
est = call("/estimate", payload)
puts "#{est['model']} #{est['hold_credits']} #{est['min_credits']}"
abort "short by #{est['min_credits'] - me['credits']}" if me["credits"] < est["min_credits"]
<?php
$material = "园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。"
. "改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车,"
. "实测午高峰峰值负载从 480 kW 降到 390 kW。";
$payload = [
"task" => "mine",
"patent_type_hint" => "auto",
"material" => $material,
];
$est = call("/estimate", $payload);
echo "{$est['model']} {$est['hold_credits']} {$est['min_credits']}\n";
if ($me["credits"] < $est["min_credits"]) {
throw new RuntimeException("short by " . ($est["min_credits"] - $me["credits"]));
}
const string material =
"园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。" +
"改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车," +
"实测午高峰峰值负载从 480 kW 降到 390 kW。";
var payload = new
{
task = "mine",
patent_type_hint = "auto",
material
};
var est = await CallAsync("/estimate", payload);
Console.WriteLine($"{est.GetProperty("model")} {est.GetProperty("hold_credits")}");
5. Run a lane and poll for the result
POST /run takes the input object as its body and returns a job_id
immediately; poll GET /jobs/{job_id} until status is terminal
(succeeded, failed or cancelled). The model's reply is a
string at data.output.output, so parse it yourself.
Always send an Idempotency-Key, and derive it from
(task, input, attempt). All three parts matter. Include task or the second
lane over the same material will collide with the first and hand you back the first lane's cached
result. Include a hash of the input so editing the material starts a new run. Include an attempt
counter so a deliberate re-ask (a retry_note re-ask, say) is a new run while a
transport-level retry of the same attempt reuses the key and cannot double-bill.
ATTEMPT=1
HASH=$(printf '%s' "$MATERIAL" | shasum -a 256 | cut -c1-16)
KEY="jiaodi-desk:mine:$HASH:$ATTEMPT"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @payload.json | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['job_id'])")
until curl -sS "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \
| tee job.json | grep -q '"status":"succeeded"'; do sleep 2; done
python3 -c "import json;print(json.load(open('job.json'))['data']['output']['output'])"
import hashlib, time
def idem_key(payload, attempt=1):
material = payload.get("material", "") + "\x00" + payload.get("disclosure", "")
digest = hashlib.sha256(material.encode("utf-8")).hexdigest()[:16]
return f"jiaodi-desk:{payload['task']}:{digest}:{attempt}"
job = call("/run", payload, idempotency_key=idem_key(payload))
job_id = job["job_id"]
while True:
status = call("/jobs/" + job_id, method="GET")
if status["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
if status["status"] != "succeeded":
raise RuntimeError("job " + status["status"])
result = json.loads(status["output"]["output"])
print(result["lane"], result["posture"], "-", result["verdict"])
for f in result["findings"]:
print(f" [{f['severity']:8}] {f['id']} {f['area']}: {f['title']}")
import { createHash } from "node:crypto";
function idemKey(payload, attempt = 1) {
const material = `${payload.material ?? ""}\u0000${payload.disclosure ?? ""}`;
const digest = createHash("sha256").update(material).digest("hex").slice(0, 16);
return `jiaodi-desk:${payload.task}:${digest}:${attempt}`;
}
const job = await call("/run", payload, "POST", idemKey(payload));
let status;
do {
await new Promise(r => setTimeout(r, 2000));
status = await call(`/jobs/${job.job_id}`, undefined, "GET");
} while (!["succeeded", "failed", "cancelled"].includes(status.status));
if (status.status !== "succeeded") throw new Error(`job ${status.status}`);
const result = JSON.parse(status.output.output);
console.log(result.lane, result.posture, "-", result.verdict);
for (const f of result.findings) {
console.log(` [${f.severity}] ${f.id} ${f.area}: ${f.title}`);
}
sum := sha256.Sum256([]byte(material))
key := fmt.Sprintf("jiaodi-desk:%s:%x:1", payload["task"], sum[:8])
data, err := call("POST", "/run", payload, key)
if err != nil {
panic(err)
}
var job struct {
JobID string `json:"job_id"`
}
json.Unmarshal(data, &job)
for {
time.Sleep(2 * time.Second)
statusData, _ := call("GET", "/jobs/"+job.JobID, nil, "")
var st struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(statusData, &st)
if st.Status == "succeeded" {
fmt.Println(st.Output.Output)
break
}
if st.Status == "failed" || st.Status == "cancelled" {
panic("job " + st.Status)
}
}
String digest = Integer.toHexString(material.hashCode());
String key = "jiaodi-desk:mine:" + digest + ":1";
HttpRequest run = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payload, java.nio.charset.StandardCharsets.UTF_8))
.build();
String jobId = JsonUtil.path(
CLIENT.send(run, HttpResponse.BodyHandlers.ofString()).body(), "data", "job_id");
String status;
do {
Thread.sleep(2000);
status = call("/jobs/" + jobId, "");
} while (!status.contains("\"status\":\"succeeded\"")
&& !status.contains("\"status\":\"failed\""));
System.out.println(status);
require "digest"
def idem_key(payload, attempt = 1)
material = "#{payload['material']}\0#{payload['disclosure']}"
"jiaodi-desk:#{payload['task']}:#{Digest::SHA256.hexdigest(material)[0, 16]}:#{attempt}"
end
job = call("/run", payload, :post, idempotency_key: idem_key(payload))
status = nil
loop do
sleep 2
status = call("/jobs/#{job['job_id']}", nil, :get)
break if %w[succeeded failed cancelled].include?(status["status"])
end
raise "job #{status['status']}" unless status["status"] == "succeeded"
result = JSON.parse(status["output"]["output"])
puts "#{result['lane']} #{result['posture']} - #{result['verdict']}"
result["findings"].each { |f| puts " [#{f['severity']}] #{f['id']} #{f['title']}" }
<?php
function idem_key(array $payload, int $attempt = 1): string {
$material = ($payload["material"] ?? "") . "\0" . ($payload["disclosure"] ?? "");
$digest = substr(hash("sha256", $material), 0, 16);
return "jiaodi-desk:{$payload['task']}:{$digest}:{$attempt}";
}
$job = call("/run", $payload, idem_key($payload));
do {
sleep(2);
$status = call("/jobs/" . $job["job_id"]);
} while (!in_array($status["status"], ["succeeded", "failed", "cancelled"], true));
if ($status["status"] !== "succeeded") {
throw new RuntimeException("job " . $status["status"]);
}
$result = json_decode($status["output"]["output"], true);
echo "{$result['lane']} {$result['posture']} - {$result['verdict']}\n";
foreach ($result["findings"] as $f) {
echo " [{$f['severity']}] {$f['id']} {$f['title']}\n";
}
using System.Security.Cryptography;
var digest = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(material)))[..16].ToLower();
var key = $"jiaodi-desk:mine:{digest}:1";
var job = await CallAsync("/run", payload, key);
var jobId = job.GetProperty("job_id").GetString();
JsonElement status;
string state;
do
{
await Task.Delay(2000);
status = await CallAsync($"/jobs/{jobId}", method: HttpMethod.Get);
state = status.GetProperty("status").GetString();
} while (state is not ("succeeded" or "failed" or "cancelled"));
if (state != "succeeded") throw new Exception($"job {state}");
var result = JsonDocument.Parse(
status.GetProperty("output").GetProperty("output").GetString()).RootElement;
Console.WriteLine(result.GetProperty("verdict"));
6. Stream it instead, for anything interactive
POST /run-stream is the same call with the same body over Server-Sent Events. Deltas
arrive as they are generated, which is what the page uses to advance its progress stages while a
交底书 is being written. The same Idempotency-Key rule applies. Accumulate the deltas and
parse the JSON once the stream closes — a half-received envelope is not valid JSON, and half a
技术方案 is worse than none.
curl -sS -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d @payload.json
# event: delta
# data: {"text":"{\"lane\":\"draft\","}
# event: done
# data: {"job_id":"job_...","charged_credits":3187,"truncated":false}
import urllib.request
req = urllib.request.Request(
BASE + "/run-stream", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"))
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", idem_key(payload))
req.add_header("Accept", "text/event-stream")
chunks = []
with urllib.request.urlopen(req) as stream:
for raw in stream:
line = raw.decode("utf-8").strip()
if not line.startswith("data:"):
continue
event = json.loads(line[5:].strip())
if "text" in event:
chunks.append(event["text"])
print(".", end="", flush=True)
result = json.loads("".join(chunks))
print("\n", result["posture"], result["verdict"])
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": idemKey(payload),
"Accept": "text/event-stream"
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", text = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const event = JSON.parse(line.slice(5).trim());
if (event.text) text += event.text;
}
}
const result = JSON.parse(text);
console.log(result.posture, result.verdict);
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var sb strings.Builder
scanner := bufio.NewScanner(res.Body)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var ev struct {
Text string `json:"text"`
}
if json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &ev) == nil && ev.Text != "" {
sb.WriteString(ev.Text)
}
}
fmt.Println(sb.String())
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(payload, java.nio.charset.StandardCharsets.UTF_8))
.build();
StringBuilder text = new StringBuilder();
CLIENT.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(line -> line.startsWith("data:"))
.forEach(line -> text.append(JsonUtil.path(line.substring(5).trim(), "text")));
System.out.println(text);
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idem_key(payload)
req["Accept"] = "text/event-stream"
req.body = JSON.dump(payload)
text = +""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
event = JSON.parse(line[5..].strip) rescue next
text << event["text"] if event["text"]
end
end
end
end
result = JSON.parse(text)
puts "#{result['posture']} #{result['verdict']}"
<?php
$text = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: " . idem_key($payload),
"Accept: text/event-stream",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$text) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "data:")) {
$event = json_decode(trim(substr($line, 5)), true);
if (isset($event["text"])) $text .= $event["text"];
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$result = json_decode($text, true);
echo "{$result['posture']} {$result['verdict']}\n";
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", key);
req.Headers.Add("Accept", "text/event-stream");
req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var res = await Client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var text = new StringBuilder();
while (await reader.ReadLineAsync() is { } line)
{
if (!line.StartsWith("data:")) continue;
var ev = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (ev.TryGetProperty("text", out var t)) text.Append(t.GetString());
}
var result = JsonDocument.Parse(text.ToString()).RootElement;
Console.WriteLine(result.GetProperty("verdict"));
7. One worked example per lane
The four requests below run over the same project material — a car-park charging-pile scheduler
that defers charging by how long each car can afford to wait — and differ in task,
in which of material / disclosure they carry, and in whether they send a
prescan. The envelope is identical across all four; only body and
artifact change. Arrays shown as ... follow the shapes documented above,
abbreviated here to one representative entry. Only the first example sends a prescan; the
other three omit it, which is why their coverage_check is empty.
task: "mine" — 专利点挖掘
What in this material could be applied for, and what is still missing before anyone drafts. Every point
lands on a 技术问题 → 技术手段 → 有益效果 triple; an idea missing one of the three goes into
open_questions instead of body.points. novelty_grade is the
model's common-sense first read on whether the industry already does this, not a search result.
Request
{
"task": "mine",
"patent_type_hint": "auto",
"material": "园区充电桩原来先到先充,午高峰变压器容量不够就整片降功率。改成按每辆车的离场时间和电池 SOC 算一个可推迟时长,容量不足时先推迟可推迟时长最大的车,实测午高峰峰值负载从 480 kW 降到 390 kW,没有车超过承诺离场时间。",
"context": "我们自己觉得新的是「可推迟时长」这个量,不是排队本身。",
"prescan": {
"resources": [
{"id": "JR-MATERIAL-CHARS", "label": "材料 121 字"},
{"id": "JR-QUANTIFIED", "label": "出现量化效果:480 kW、390 kW"}
],
"flags": [
{"id": "JD-M02", "label": "材料未给出任何参数范围或边界条件",
"severity": "high", "detail": "未见阈值、上下限、采样周期", "occurrences": 1},
{"id": "JD-M04", "label": "效果数字缺少对比条件",
"severity": "medium", "detail": "480 kW / 390 kW 未说明同工况", "occurrences": 2},
{"id": "JD-M07", "label": "出现绝对化用语",
"severity": "low", "detail": "「没有车超过」", "occurrences": 1}
]
}
}
Response data.output.output, parsed
{
"lane": "mine",
"lane_inferred": false,
"invention": "按可推迟时长排序的充电桩负载调度",
"title": "充电桩排队调度 - 专利点挖掘",
"patent_type": "invention",
"posture": "needs-work",
"verdict": "材料支撑两个可申请的点,但缺参数范围与对比条件,写交底书前必须补齐。",
"summary": "材料的核心是把「还能等多久」变成一个可计算的量,并按它排序推迟充电 ... 效果有数字但没有对比条件,属于可补的缺口,不影响专利点成立。",
"assumptions": [
"patent_type_hint 为 auto:按发明专利处理,因为方案落在具体的功率分配与设备控制流程上,不是纯规则。",
"「变压器容量」按园区配电变压器的额定容量理解,材料未明确。"
],
"open_questions": [
"可推迟时长的计算是否考虑充电功率随 SOC 下降的曲线?",
"480 kW 与 390 kW 是同一天同一车流量下的对比,还是不同日期?"
],
"findings": [
{"id": "JD-001", "title": "补齐可推迟时长的参数范围与边界条件",
"severity": "high", "area": "mining", "section": "", "term": "可推迟时长",
"evidence": "按每辆车的离场时间和电池 SOC 算一个可推迟时长",
"why": "交底书的技术方案必须可实施:没有采样周期、没有推迟时长的上限、没有 SOC 下界,本领域技术人员无法复现,第 4.2 节写不下去,实施例也无从举例。",
"fix": "补上:调度周期(如 30 s)、单车最长推迟时长、触发调度的容量占用率阈值、SOC 下限。",
"fix_text": ""},
{"id": "JD-002", "title": "效果数字补对比条件",
"severity": "medium", "area": "clarity", "section": "", "term": "390 kW",
"evidence": "实测午高峰峰值负载从 480 kW 降到 390 kW",
"why": "没有同工况说明的单点数字,代理人无法写进有益效果,审查中也难以作为技术效果被采信。",
"fix": "补充同车流量、同变压器容量下的对照,并给出统计口径(几天的午高峰均值)。",
"fix_text": ""}
],
"coverage_check": [
{"flag_id": "JD-M02", "status": "confirmed", "finding_id": "JD-001", "note": ""},
{"flag_id": "JD-M04", "status": "confirmed", "finding_id": "JD-002", "note": "两处数字同一个缺口,一并处理。"},
{"flag_id": "JD-M07", "status": "set-aside", "finding_id": "",
"note": "挖点阶段不影响专利点成立;成文时会按绝对化用语改写,届时由 draft 车道处理。"}
],
"artifact": {"kind": "markdown", "filename": "专利点清单.md",
"content": "# 专利点清单\n\n## 点1 按可推迟时长排序的充电推迟\n- 技术问题:...\n- 技术手段:...\n- 有益效果:...\n- 缺口:...\n"},
"next_lane": {"lane": "draft", "reason": "点1 的三元组已经成立,可以直接成文,缺口在成文时以【待补】标注。"},
"body": {
"points": [
{"name": "按可推迟时长排序的充电推迟",
"patent_type": "invention",
"problem": "容量不足时整片降功率,既拖慢所有车,也无法保证任何一辆车按时离场。",
"solution": "为每辆在充车辆计算可推迟时长(离场时间减去按当前 SOC 补足所需电量的时间),容量不足时按可推迟时长降序推迟充电,而不是统一降功率。",
"effect": "午高峰峰值负载 480 kW 降至 390 kW,且无车辆超过承诺离场时间。",
"novelty_grade": "medium",
"prior_art_risk": "有序充电、削峰填谷类调度是常规做法;区别在于把「还能等多久」显式量化并作为排序键,检索时应盯住这一点。",
"evidence": "改成按每辆车的离场时间和电池 SOC 算一个可推迟时长",
"missing_info": ["调度周期与推迟时长上限", "SOC 下限与充电功率曲线", "对比试验的工况说明"]},
{"name": "按承诺离场时间的容量预留",
"patent_type": "invention",
"problem": "推迟队列长了以后,后到车辆可能挤占先到车辆的必需电量。",
"solution": "在推迟决策前,为每辆车按承诺离场时间预留必需电量对应的功率份额,剩余容量才进入推迟排序。",
"effect": "材料只写了「没有车超过承诺离场时间」,未给出预留量化数据。",
"novelty_grade": "low",
"prior_art_risk": "预留式资源分配在配电与通信调度里是常见做法,单独申请可能被认定为常规技术手段。",
"evidence": "没有车超过承诺离场时间",
"missing_info": ["预留是否真实实现,还是排序的副产品", "预留失败时的降级策略"]}
],
"ranking_note": "点1 三元组完整且有实测数字,点2 目前只有一句结果性描述,缺技术手段细节,排在后面。",
"recommended_point": "按可推迟时长排序的充电推迟"
}
}
task: "draft" — 交底书成文
The whole 技术交底书, in the desensitised template: 发明名称, 技术领域, 背景技术 with the drawbacks of
current practice, 发明内容 split into 要解决的技术问题 / 技术方案 / 有益效果, 附图说明, 具体实施方式 with
at least one worked embodiment, and a 术语与缩略语表. Figures are never images: section
5 carries a 图N caption for each and body.figures[].mermaid carries the source you render
yourself. Where the material cannot support a section, the section is drafted as a skeleton marked
【待补:…】 and its status is thin or missing-input —
never padded with invented content. Send disclosure as well and the run becomes a
revision of that draft rather than a rewrite.
Request
{
"task": "draft",
"patent_type_hint": "invention",
"material": "<同一份项目材料>",
"point": "按可推迟时长排序的充电推迟",
"context": "读者是外部专利代理人。园区名和产品名都不要出现。"
}
Response data.output.output, parsed
{
"lane": "draft",
"lane_inferred": false,
"invention": "基于可推迟时长的充电负载调度方法",
"title": "充电桩排队调度 - 技术交底书初稿",
"patent_type": "invention",
"posture": "needs-work",
"verdict": "七个章节全部成文,但具体实施方式的参数区间来自材料缺口,已按【待补】标注,不得直接提交。",
"summary": "交底书按脱敏模版写完 ... 第 6 节给出一个完整实施例,调度周期与推迟上限标注为【待补】,等发明人补数。第 4.3 节的效果逐条对应第 4.2 节的技术手段。",
"assumptions": [
"专利点由用户指定(point 字段),未另行挖点。",
"外观与结构不构成本方案要点,按发明专利模版撰写。"
],
"open_questions": [
"调度周期与单车最长推迟时长的优选值是多少?",
"是否存在推迟失败(容量仍不足)时的兜底策略,需要写进实施例?"
],
"findings": [
{"id": "JD-001", "title": "具体实施方式缺参数优选值",
"severity": "high", "area": "drafting", "section": "6 具体实施方式", "term": "调度周期",
"evidence": "容量不足时先推迟可推迟时长最大的车",
"why": "实施例要让本领域技术人员可复现:没有调度周期与推迟上限的范围和优选值,第 6 节只是把第 4.2 节复述一遍,可实施性会被代理人退回。",
"fix": "补一组实测参数:调度周期范围与优选值、单车最长推迟时长、容量占用率触发阈值。",
"fix_text": "所述调度单元每隔 T_s 执行一次排序,T_s 取 10 s 至 120 s,优选 30 s;【待补:实测优选值】"},
{"id": "JD-002", "title": "删除绝对化表述",
"severity": "medium", "area": "clarity", "section": "4.3 有益效果", "term": "没有车超过",
"evidence": "没有车超过承诺离场时间",
"why": "绝对化表述在交底书里会被当作无支撑的断言,实测样本量有限时更应写成条件化表述。",
"fix": "改成有统计口径的表述。",
"fix_text": "在上述实测条件下,各充电任务均在承诺离场时间前完成充电目标。"}
],
"coverage_check": [],
"artifact": {
"kind": "disclosure",
"filename": "技术交底书-充电负载调度.md",
"content": "# 技术交底书\n\n## 1 发明名称\n基于可推迟时长的充电负载调度方法及系统\n\n## 2 技术领域\n本申请涉及电动车辆充电控制技术领域 ...\n\n## 3 背景技术\n现有做法为先到先充 ... 其缺点在于:(1)容量不足时统一降功率 ...\n\n## 4 发明内容\n### 4.1 要解决的技术问题\n...\n### 4.2 技术方案\n步骤 S1 ...\n### 4.3 有益效果\n...\n\n## 5 附图说明\n图1:系统架构示意图\n图2:单调度周期的处理流程图\n\n## 6 具体实施方式\n实施例一 ...【待补:调度周期优选值】\n\n## 7 术语与缩略语表\nSOC:荷电状态 ...\n"
},
"next_lane": {"lane": "check", "reason": "初稿已成文,应先跑一次自检核对逻辑闭环与公式符号,再交代理人。"},
"body": {
"point_used": {"name": "按可推迟时长排序的充电推迟", "source": "user"},
"sections": [
{"id": "s1", "name": "发明名称", "status": "drafted", "note": ""},
{"id": "s3", "name": "背景技术", "status": "drafted", "note": "已按要求写出现有做法的三条缺点。"},
{"id": "s6", "name": "具体实施方式", "status": "thin",
"note": "参数区间来自材料缺口,已标注【待补】,见 JD-001。"},
{"id": "s7", "name": "术语与缩略语表", "status": "drafted", "note": ""}
],
"desensitized": [
{"hint": "园区名称类", "placeholder": "本申请方所述场地"},
{"hint": "内部系统代号类", "placeholder": "所述调度单元"}
],
"figures": [
{"fig_no": 1, "caption": "图1:系统架构示意图,示出调度单元、充电桩与配电变压器的连接关系",
"mermaid": "flowchart LR; A[调度单元] --> B[充电桩组]; B --> C[配电变压器]; A --> D[车辆信息接口]"},
{"fig_no": 2, "caption": "图2:单调度周期的处理流程图",
"mermaid": "flowchart TD; S1[采集 SOC 与离场时间] --> S2[计算可推迟时长] --> S3{容量是否不足} --> S4[按可推迟时长降序推迟]"}
]
}
}
task: "novelty" — 查新检索方案
The search plan, for a person to run. Three to six distinguishing features — not field-level
words — each with a Chinese keyword group, an English keyword group and synonym expansions
including the industry slang; two to five IPC candidates at main-group level; paste-ready query strings
in the real syntax of CNIPA, Google Patents and Espacenet, each with a purpose; a knockout
criterion per feature; and a four-to-eight step protocol. The model has no search access and
returns no results. No patent number, no publication number, no paper title appears anywhere
in the output unless you pasted it in yourself. Prefer sending disclosure here; the lane
falls back to material when there is no draft yet.
Request
{
"task": "novelty",
"patent_type_hint": "invention",
"disclosure": "<draft 车道产出的交底书全文>",
"context": "竞品只做整片降功率,没有按车排序。想知道「可推迟时长」这个量有没有人做过。"
}
Response data.output.output, parsed
{
"lane": "novelty",
"lane_inferred": false,
"invention": "基于可推迟时长的充电负载调度方法",
"title": "充电负载调度 - 查新检索方案",
"patent_type": "invention",
"posture": "ready",
"verdict": "分类号与关键词都能落地,方案可直接照单执行;命中风险集中在有序充电这一大类。",
"summary": "拆出四个区别技术特征,其中「可推迟时长作为排序键」是唯一需要单独检索的特征 ... 检索式按 CNIPA、Google Patents、Espacenet 三库各给一条,另附 incoPat 风格一条。执行顺序建议先分类号后关键词、先中后英。",
"assumptions": ["按中国专利实务口径起草;未指定其他法域。"],
"open_questions": ["是否有内部公开日或展会披露日,需要作为检索截止日?"],
"findings": [
{"id": "JD-001", "title": "单独检索「可推迟时长」这一排序键",
"severity": "high", "area": "search", "section": "4.2 技术方案", "term": "可推迟时长",
"evidence": "容量不足时按可推迟时长降序推迟充电",
"why": "有序充电、削峰填谷的现有文献极多,如果不把排序键单独检索,很容易被一篇「按离场时间排序」的文献整体命中。",
"fix": "先执行 Q2 与 Q3,再决定技术方案的撰写宽度。",
"fix_text": ""}
],
"coverage_check": [],
"artifact": {"kind": "queries", "filename": "查新检索方案.md",
"content": "# 查新检索方案\n\n## 一、区别技术特征\n1. 可推迟时长作为排序键 ...\n\n## 二、IPC 候选\n...\n\n## 三、检索式\n### CNIPA\n...\n\n## 四、逐特征排除判据\n...\n\n## 五、执行顺序\n...\n"},
"next_lane": {"lane": "check", "reason": "交底书已成文,检索方案交人执行的同时可以先跑一次自检。"},
"body": {
"features": [
{"feature": "以离场时间与 SOC 计算的可推迟时长作为排序键",
"cn_keywords": ["可推迟时长", "可延迟时间", "松弛时间", "排序键"],
"en_keywords": ["deferrable duration", "laxity", "slack time", "priority key"],
"synonyms": ["时间裕度", "剩余可等待时间", "charging urgency"]},
{"feature": "容量不足时按该键降序推迟而非统一降功率",
"cn_keywords": ["有序充电", "推迟充电", "降功率", "负荷分配"],
"en_keywords": ["load curtailment", "charging deferral", "power derating"],
"synonyms": ["削峰", "需求响应", "demand response"]},
{"feature": "以配电变压器容量占用率触发调度",
"cn_keywords": ["变压器容量", "容量占用率", "触发阈值"],
"en_keywords": ["transformer capacity", "utilisation threshold"],
"synonyms": ["台区容量", "配变负载率"]}
],
"ipc_candidates": [
{"code": "B60L 53/00", "why": "电动车辆充电的方法与装置,本方案的控制对象就是充电过程。"},
{"code": "H02J 7/00", "why": "蓄电池充电电路与充电控制,涉及功率分配。"},
{"code": "H02J 3/00", "why": "交流配电网络的电路装置,负载调度侧落在这里;未把握到小组,停在大组。"},
{"code": "G06Q 50/00", "why": "特定行业的数据处理系统,若按运营调度方式撰写会落入此类。"}
],
"queries": [
{"database": "CNIPA",
"query": "(TI=(充电 AND (调度 OR 有序)) OR ABST=(充电 AND (调度 OR 有序))) AND ABST=(离场时间 OR 可推迟 OR 可延迟 OR 松弛) AND IPC=(B60L53 OR H02J7)",
"purpose": "命中中文申请里把「还能等多久」显式量化的写法;用 IPC 限定挡掉纯电网调度文献。"},
{"database": "Google Patents",
"query": "(\"charging schedule\" OR \"charging deferral\") AND (\"departure time\" OR \"slack time\" OR laxity) AND (transformer OR \"capacity limit\") CPC=(B60L53/63 OR H02J3/14)",
"purpose": "英文同族与美日申请;用 transformer 挡掉纯车载 BMS 文献。"},
{"database": "Espacenet",
"query": "ftxt all \"charging schedule\" AND ftxt all \"departure time\" AND ctxt any \"deferral, curtailment, derating\" AND ipc = \"B60L53/low\"",
"purpose": "覆盖欧洲与 PCT 途径;用全文检索兜住不在标题摘要里出现排序键的文献。"},
{"database": "incoPat",
"query": "TIAB=(充电 AND 调度) AND TIAB=(离场 OR 可推迟) AND IPC=(B60L53/* OR H02J7/*) AND AD=[20150101 TO 20260101]",
"purpose": "同族合并与申请人视角复核,按申请日区间收敛。"}
],
"knockout": [
{"feature": "以离场时间与 SOC 计算的可推迟时长作为排序键",
"criterion": "若有一篇文献同时公开了「按离场时间与当前电量计算一个可等待时长」并「按该时长排序决定谁先被推迟」,则该特征不新;仅公开按离场时间先后排序(不计算时长)不构成公开。"},
{"feature": "容量不足时按该键降序推迟而非统一降功率",
"criterion": "若有一篇文献公开了按车辆优先级逐台推迟充电,且推迟决策由容量约束触发,则该特征不新;统一降功率或轮流充电不构成公开。"},
{"feature": "以配电变压器容量占用率触发调度",
"criterion": "常规技术手段,单独不足以支撑新颖性;仅在与前两个特征组合时评价。"}
],
"protocol": [
"第一步:先跑分类号(B60L 53 与 H02J 7 的交集),只读标题与首项权利要求,逐条记 keep/discard 与一句理由。",
"第二步:CNIPA 中文关键词检索(Q1),中文优先,因为本方案的同类做法在国内申请里最密集。",
"第三步:Google Patents 英文检索(Q2),补同族与国外申请。",
"第四步:Espacenet 全文检索(Q3),只用于兜住标题摘要不含排序键的文献。",
"第五步:初筛只读标题、摘要、首项权利要求与附图;进入细读的文献才读实施例。",
"第六步:止损条件——某条检索式连续 50 条命中全部 discard,即停止该式,改用同义扩展重写。",
"第七步:把每条检索式的执行日期、库、命中数、保留篇数与理由记进检索日志,保证他人可复现。"
]
}
}
Executing the NPL search (POST /v1/app-api/search)
This release declares two outbound-search providers — web.wikipedia (MediaWiki
full-text search, English) and web.duckduckgo (Instant Answer abstracts). The platform
performs the search server-side and stamps every record; signed-in users only (guests get 403),
metered at 2,000 nanos per call + 100 per record, 24h result cache, 30 req/min.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/search" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"provider": "web.wikipedia", "query": "bloom filter deduplication sliding window", "limit": 12}'
# -> {"data": {"provider", "retrieved_at", "query_echo", "total_estimate",
# "records": [{"record_id": "srch_…", "title", "abstract", "url"}], "cursor"}}
The loop the web app runs, reproducible over the API: (1) take body.features[].en_keywords
from a novelty run and search per feature; (2) curate — keep only records worth
comparing; (3) re-run task: "novelty" with the kept records as search_results
(add provider and retrieved_at from the search response to each record);
(4) read body.comparison and verify citations: cited
record_ids ⊆ sent, each sent record compared exactly once, every feature covered.
A check run accepts the same search_results and will flag draft claims that
collide with a record. Wikipedia is English-only in v1 — use the EN keyword column;
DuckDuckGo answers "what is X" queries, not open-ended ones.
task: "check" — 交底书自检
The lane this app exists for, and the one the source skill calls
disclosure_self_check. Two passes are mandatory. 逻辑闭环 (§8.4):
every 技术问题 must have a 技术手段 answering it, every 有益效果 must have a 技术手段 supporting it, and
every 技术手段 must serve some 问题 — each broken link becomes a
body.closure.gaps entry and a finding with area: "closure".
公式参数一致性 (§8.5): every symbol defined, the same symbol meaning the same
thing throughout, units consistent, parameter ranges not contradicting the embodiment's numbers, and no
two formulas contradicting each other. On top of those: per-section verdicts, a desensitisation
recheck, figure-reference consistency and whether the embodiment is reproducible.
disclosure is required; without it the lane returns
posture: "blocked" rather than guessing at a draft.
Request
{
"task": "check",
"patent_type_hint": "invention",
"disclosure": "4.2 技术方案:所述调度单元按可推迟时长 T_d 降序推迟充电,T_d = (t_leave - t_now) - E_need / (η * P_max)。4.3 有益效果:峰值负载下降 19%,用户等待时间不变。",
"material": "<同一份项目材料>"
}
Response data.output.output, parsed
{
"lane": "check",
"lane_inferred": false,
"invention": "基于可推迟时长的充电负载调度方法",
"title": "充电负载调度 - 交底书自检",
"patent_type": "invention",
"posture": "needs-work",
"verdict": "公式里 η 未定义且单位不闭合,第 4.3 节「等待时间不变」在技术方案里没有对应手段,两处均须改后再交。",
"summary": "按 8.4 逻辑闭环与 8.5 公式参数一致性逐条核对 ... 技术问题与技术手段咬合,效果一侧断了一条链。公式共 1 条,1 个符号未定义,1 处单位不一致。背景技术与附图说明未在提交的片段中出现,按 missing 记录。两处严重问题已给出改写文字。",
"assumptions": ["按提交的交底书片段核对;未出现的章节按 missing 记录,不推测其内容。"],
"open_questions": ["η 是充电效率还是功率折损系数?两种解释会改变 T_d 的量纲。"],
"findings": [
{"id": "JD-001", "title": "为「等待时间不变」补技术手段或删除该效果",
"severity": "critical", "area": "closure", "section": "4.3 有益效果", "term": "用户等待时间不变",
"evidence": "4.3 有益效果:峰值负载下降 19%,用户等待时间不变。",
"why": "8.4 逻辑闭环要求每个有益效果都有技术手段支撑。技术方案只写了按 T_d 降序推迟,没有任何保证等待时间不增的手段,效果与手段之间断链,代理人会直接退回,审查中也构成无支撑的效果陈述。",
"fix": "要么在 4.2 补上以承诺离场时间为硬约束的预留步骤,要么把效果改写为「各充电任务在承诺离场时间前完成」这一可由 T_d 定义直接推出的表述。",
"fix_text": "4.3 有益效果:(1)峰值负载在同工况下由 480 kW 降至 390 kW;(2)由于 T_d 以承诺离场时间为上界,被推迟的充电任务仍在承诺离场时间前完成。"},
{"id": "JD-002", "title": "定义 η 并统一时间量纲",
"severity": "high", "area": "formula", "section": "4.2 技术方案", "term": "η",
"evidence": "T_d = (t_leave - t_now) - E_need / (η * P_max)",
"why": "8.5 公式参数一致性:η 全文未定义;且 E_need 若以 kWh 计、P_max 以 kW 计,则商为小时,而 T_d 与 (t_leave - t_now) 在实施例中按秒比较,量纲不闭合,按此式实施会得到 3600 倍误差。",
"fix": "在术语表定义 η 并给取值范围,同时在公式中显式写出换算系数或统一为秒。",
"fix_text": "T_d = (t_leave - t_now) - 3600 * E_need / (η * P_max),其中 t_leave、t_now 与 T_d 单位为秒,E_need 单位为 kWh,P_max 单位为 kW,η 为充电链路效率,取 0.85 至 0.98,优选 0.92。"}
],
"coverage_check": [],
"artifact": {"kind": "markdown", "filename": "自检修订.md",
"content": "# 自检修订\n\n## 4.2 技术方案(改后)\nT_d = (t_leave - t_now) - 3600 * E_need / (η * P_max) ...\n\n## 4.3 有益效果(改后)\n...\n"},
"next_lane": {"lane": "check", "reason": "两处改写落到交底书后值得再跑一次自检,确认闭环补齐且无新的符号问题。"},
"body": {
"closure": {
"problem_ok": true,
"solution_ok": true,
"effect_ok": false,
"gaps": ["效果「用户等待时间不变」在 4.2 技术方案中没有对应技术手段。"]
},
"formula_check": [
{"formula": "T_d = (t_leave - t_now) - E_need / (η * P_max)",
"defined": ["T_d", "t_leave", "t_now", "E_need", "P_max"],
"symbols_undefined": ["η"],
"unit_issue": "E_need/P_max 得到小时,而 T_d 与时间差按秒使用,缺 3600 换算系数。",
"range_issue": "",
"note": "P_max 是否随 SOC 变化未说明;若为变量,本式只在恒功率段成立,应在实施例中限定适用区间。"}
],
"section_reviews": [
{"section": "背景技术", "verdict": "missing",
"issues": ["提交的片段中不存在该章节;发明专利模版要求写出现有做法及其缺点。"]},
{"section": "4.2 技术方案", "verdict": "thin",
"issues": ["只给出排序规则与公式,缺少调度周期、触发阈值与推迟上限,本领域技术人员难以复现。"]},
{"section": "4.3 有益效果", "verdict": "broken",
"issues": ["效果之一无技术手段支撑(见 JD-001)。", "19% 未给出对比工况。"]},
{"section": "5 附图说明", "verdict": "missing",
"issues": ["片段中无附图说明;4.2 的步骤适合配一张流程图。"]}
],
"rewrites": 2
}
}
The pipeline
The three lanes chain on two strings, and both are ordinary fields of the response rather than anything
you have to reformat. mine returns body.recommended_point, which is one of the
body.points[].name values: that string is exactly what draft takes as
point. draft returns artifact.kind: "disclosure" whose
content is the whole 技术交底书 as Markdown — headings, 【待补】 markers and all:
that string is exactly what check takes as disclosure.
So you feed each string straight back in. Three calls over one piece of project
material take you from raw notes to a self-checked draft without a human copy-pasting between steps.
check then returns its rewrites in findings[].fix_text and collected in its
own artifact, so a fourth call can re-check the corrected draft — which is what its
next_lane asks for while anything critical remains.
Give every call its own idempotency key. Same material, different task: if
task is not in the key, the draft run collides with the mine run
and hands back the mining lane's cached result.
# 1. Mine the points. ("$MATERIAL" is the project material from step 4.)
post /run "{\"task\":\"mine\",\"patent_type_hint\":\"auto\",\"material\":\"$MATERIAL\"}" > mine.json
# ... poll /jobs/{id} until succeeded into job.json, then take the recommended point:
python3 -c "import json;print(json.loads(json.load(open('job.json'))['data']['output']['output'])['body']['recommended_point'])" > point.txt
printf '%s' "$MATERIAL" > material.txt
# 2. Draft the 交底书 around that point.
python3 -c "import json;print(json.dumps({
'task': 'draft',
'patent_type_hint': 'invention',
'material': open('material.txt').read(),
'point': open('point.txt').read().strip()}, ensure_ascii=False))" > draft-payload.json
curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: jiaodi-desk:draft:$HASH:1" \
-d @draft-payload.json
# ... poll again, then the artifact content IS the disclosure:
python3 -c "import json;print(json.loads(json.load(open('job.json'))['data']['output']['output'])['artifact']['content'])" > 交底书.md
# 3. Self-check that very file.
python3 -c "import json;print(json.dumps({
'task': 'check',
'disclosure': open('交底书.md').read()}, ensure_ascii=False))" > check-payload.json
curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: jiaodi-desk:check:$HASH:1" \
-d @check-payload.json
# 1. Mine the points.
mine_payload = {"task": "mine", "patent_type_hint": "auto", "material": MATERIAL}
mine = run_and_wait(mine_payload, idem_key(mine_payload))
point = mine["body"]["recommended_point"]
print("chosen point:", point, "-", mine["body"]["ranking_note"])
# 2. Draft the 交底书 around it.
draft_payload = {
"task": "draft",
"patent_type_hint": "invention",
"material": MATERIAL,
"point": point,
}
draft = run_and_wait(draft_payload, idem_key(draft_payload))
assert draft["artifact"]["kind"] == "disclosure", draft["artifact"]["kind"]
disclosure = draft["artifact"]["content"] # the whole 技术交底书, Markdown
# 3. Self-check the very same string. No reformatting.
check_payload = {"task": "check", "disclosure": disclosure, "material": MATERIAL}
check = run_and_wait(check_payload, idem_key(check_payload))
print(check["posture"], "-", check["verdict"])
print("closure gaps:", check["body"]["closure"]["gaps"])
for f in check["body"]["formula_check"]:
print(" undefined:", f["symbols_undefined"], "| unit:", f["unit_issue"] or "ok")
open("技术交底书.md", "w").write(disclosure)
// 1. Mine the points.
const minePayload = { task: "mine", patent_type_hint: "auto", material: MATERIAL };
const mine = await runAndWait(minePayload, idemKey(minePayload));
const point = mine.body.recommended_point;
console.log("chosen point:", point);
// 2. Draft the 交底书 around it.
const draftPayload = {
task: "draft",
patent_type_hint: "invention",
material: MATERIAL,
point
};
const draft = await runAndWait(draftPayload, idemKey(draftPayload));
if (draft.artifact.kind !== "disclosure") throw new Error(draft.artifact.kind);
const disclosure = draft.artifact.content; // the whole 技术交底书, Markdown
// 3. Self-check the very same string.
const checkPayload = { task: "check", disclosure, material: MATERIAL };
const check = await runAndWait(checkPayload, idemKey(checkPayload));
console.log(check.posture, "-", check.verdict);
console.log("closure gaps:", check.body.closure.gaps);
writeFileSync("技术交底书.md", disclosure);
// 1. Mine the points.
minePayload := map[string]any{
"task": "mine", "patent_type_hint": "auto", "material": material,
}
mine := runAndWait(minePayload, keyFor(minePayload, 1))
point := mine.Body.RecommendedPoint
// 2. Draft the 交底书 around it.
draftPayload := map[string]any{
"task": "draft",
"patent_type_hint": "invention",
"material": material,
"point": point,
}
draft := runAndWait(draftPayload, keyFor(draftPayload, 1))
if draft.Artifact.Kind != "disclosure" {
panic("unexpected artifact kind: " + draft.Artifact.Kind)
}
disclosure := draft.Artifact.Content // the whole 技术交底书, Markdown
// 3. Self-check the very same string.
checkPayload := map[string]any{
"task": "check", "disclosure": disclosure, "material": material,
}
check := runAndWait(checkPayload, keyFor(checkPayload, 1))
fmt.Println(check.Posture, "-", check.Verdict)
fmt.Println(check.Body.Closure.Gaps)
// 1. Mine the points.
String minePayload = """
{"task": "mine", "patent_type_hint": "auto", "material": %s}
""".formatted(JsonUtil.quote(material));
String mine = runAndWait(minePayload, "jiaodi-desk:mine:" + digest + ":1");
String point = JsonUtil.path(mine, "body", "recommended_point");
// 2. Draft the 交底书 around it.
String draftPayload = """
{"task": "draft", "patent_type_hint": "invention", "material": %s, "point": %s}
""".formatted(JsonUtil.quote(material), JsonUtil.quote(point));
String draft = runAndWait(draftPayload, "jiaodi-desk:draft:" + digest + ":1");
String disclosure = JsonUtil.path(draft, "artifact", "content");
// 3. Self-check the very same string.
String checkPayload = """
{"task": "check", "disclosure": %s}
""".formatted(JsonUtil.quote(disclosure));
String check = runAndWait(checkPayload, "jiaodi-desk:check:" + digest + ":1");
System.out.println(JsonUtil.path(check, "verdict"));
Files.writeString(Path.of("技术交底书.md"), disclosure);
# 1. Mine the points.
mine_payload = { "task" => "mine", "patent_type_hint" => "auto", "material" => MATERIAL }
mine = run_and_wait(mine_payload, idem_key(mine_payload))
point = mine["body"]["recommended_point"]
puts "chosen point: #{point}"
# 2. Draft the 交底书 around it.
draft_payload = {
"task" => "draft",
"patent_type_hint" => "invention",
"material" => MATERIAL,
"point" => point
}
draft = run_and_wait(draft_payload, idem_key(draft_payload))
raise draft["artifact"]["kind"] unless draft["artifact"]["kind"] == "disclosure"
disclosure = draft["artifact"]["content"] # the whole 技术交底书, Markdown
# 3. Self-check the very same string.
check_payload = { "task" => "check", "disclosure" => disclosure, "material" => MATERIAL }
check = run_and_wait(check_payload, idem_key(check_payload))
puts "#{check['posture']} - #{check['verdict']}"
puts "closure gaps: #{check['body']['closure']['gaps'].join('; ')}"
File.write("技术交底书.md", disclosure)
<?php
// 1. Mine the points.
$minePayload = ["task" => "mine", "patent_type_hint" => "auto", "material" => $material];
$mine = run_and_wait($minePayload, idem_key($minePayload));
$point = $mine["body"]["recommended_point"];
// 2. Draft the 交底书 around it.
$draftPayload = [
"task" => "draft",
"patent_type_hint" => "invention",
"material" => $material,
"point" => $point,
];
$draft = run_and_wait($draftPayload, idem_key($draftPayload));
if ($draft["artifact"]["kind"] !== "disclosure") {
throw new RuntimeException($draft["artifact"]["kind"]);
}
$disclosure = $draft["artifact"]["content"]; // the whole 技术交底书, Markdown
// 3. Self-check the very same string.
$checkPayload = ["task" => "check", "disclosure" => $disclosure, "material" => $material];
$check = run_and_wait($checkPayload, idem_key($checkPayload));
echo "{$check['posture']} - {$check['verdict']}\n";
file_put_contents("技术交底书.md", $disclosure);
// 1. Mine the points.
var minePayload = new { task = "mine", patent_type_hint = "auto", material };
var mine = await RunAndWaitAsync(minePayload, KeyFor("mine", material, ""));
var point = mine.GetProperty("body").GetProperty("recommended_point").GetString();
// 2. Draft the 交底书 around it.
var draftPayload = new
{
task = "draft",
patent_type_hint = "invention",
material,
point
};
var draft = await RunAndWaitAsync(draftPayload, KeyFor("draft", material, point));
var artifact = draft.GetProperty("artifact");
if (artifact.GetProperty("kind").GetString() != "disclosure") throw new Exception("kind");
var disclosure = artifact.GetProperty("content").GetString();
// 3. Self-check the very same string.
var checkPayload = new { task = "check", disclosure, material };
var check = await RunAndWaitAsync(checkPayload, KeyFor("check", material, disclosure));
Console.WriteLine(check.GetProperty("verdict"));
await File.WriteAllTextAsync("技术交底书.md", disclosure);
run_and_wait is step 5 wrapped in a function: post to /run with the
idempotency key, poll GET /jobs/{job_id} until terminal, and return
json.loads(status["output"]["output"]).
Notes that will save you a support round trip
- The output is one JSON object, and you should still strip a stray code fence.
The contract says no prose and no fence; a client that trims a leading
```jsonand a trailing```, then takes the substring from the first{to the last}before parsing, costs three lines and removes a whole class of failure. Parse first, then checklane. - The run body is not wrapped in an
inputkey.POST /run,POST /run-streamandPOST /estimateall take the input object itself:{"task": ..., "material": ...}at the top level. A wrapper is accepted and returns200, but the model never seestaskormaterial— you get a confident answer to a question you did not ask, and no error to explain it. - There is no slug header — not
X-App-Slug, notX-Slug, not anything. The only headers on any endpoint areAuthorization,Content-Typeand, on the two run endpoints,Idempotency-Key. The slug is named once, in thePOST /guestbody. A bogus custom header is ignored rather than rejected, so sending one looks like it works and then explains nothing when something else breaks. - A lane without its required input returns
posture: "blocked", with the reason inverdict, what is needed inopen_questionsand empty body arrays — and it still costs a run.mineanddraftneedmaterial;checkneedsdisclosure;noveltyneeds one of the two. Check the field client-side first. - The model has no search access and will not return prior art it was not handed.
By design, and enforced in the prompt: no 专利号, no 公开号, no paper title, not even as an
illustration. The
noveltylane produces the patent search to run — execute its query strings in CNIPA, Google Patents or Espacenet yourself. The one sanctioned source issearch_results: records retrieved server-side viaPOST /v1/app-api/search(providersweb.wikipedia,web.duckduckgo, declared by this release), each stamped with arecord_idthe model must cite verbatim. Verify its output the way the web app does: every citedrecord_idmust be in the list you sent, every sent record must appear exactly once inbody.comparison. Non-patent literature only — a miss is not novelty. - Figures are mermaid source, never images.
draftwrites a 图N caption per figure in 第 5 节 and puts aflowchartorsequenceDiagrambody inbody.figures[].mermaid. Render it your side; the 图号 in the mermaid list and the ones referenced in the artifact text are contracted to agree, and that is worth asserting. (The web app renders the same source in-browser and can embed the figures as PNGs in its .docx export — that is a client behaviour; the API contract stays mermaid source.) - Desensitisation is part of the contract, not a courtesy. The output carries no
company name, product trademark, internal code name, person, email or intranet address; what it
replaced is registered in
body.desensitizedas a category hint plus the placeholder used, never as the original text. It is still worth not pasting what you would not want restated: a 交底书 is unpublished by definition. - 【待补:…】 markers are a feature. When the material cannot support a
section,
draftwrites the skeleton and marks the gap rather than inventing parameters, and records the section asthinormissing-inputinbody.sections. A draft with no markers and thin material is more suspicious than one with five. - Two lanes over one material are two runs. Include
taskin the idempotency key or the second lane returns the first lane's cached result. - Check
lane_inferred. If it istrue, yourtaskfield did not arrive or was not recognised and the model chose a lane for you. Treat the response as suspect rather than as an answer to the question you asked. - Reconcile
coverage_checkagainst the flags you sent, both ways: a missing entry is an unreconciled finding, and an entry for an id you did not send is an invented one. If you send noprescanat all, assert the array is empty. rewritesmust agree with the findings. Thechecklane'sbody.rewritescount has to match the number of findings carrying a non-emptyfix_text, and a section cannot beokinbody.section_reviewswhile a finding names it. The page checks both, and so should you.- Chinese practice only. Everything is written to the 专利法 / 实施细则 / 审查指南 house style. Name another office and the model says it is applying Chinese practice, notes the one difference that matters most, and does not pretend to know that jurisdiction's practice.