openapi: 3.1.0 info: title: 鲸察 HTTP 接口(dsh-jingcha) version: 0.5.0 description: | 给右下角挂件用的本机接口,也可以被你自己的脚本调用。 准入规则(每个端点都一样): - 只接受来自回环地址的连接; - Host 必须是 127.0.0.1 / localhost / ::1(挡 DNS rebinding); - 拒绝带 Origin 或 Sec-Fetch-Site: cross-site|same-site 的请求; - 变更类端点只接受 POST + content-type: application/json(挡 img 一发即杀与跨站表单); - 若配置了 apiToken,所有请求都要带 x-jingcha-token 头。 错误一律是 JSON:{"ok": false, "error": "<原因>"},常见状态码 403(准入)/ 405(方法)/ 415(content-type)。 license: name: MIT identifier: MIT servers: - url: http://127.0.0.1:3080 description: DSH web GUI 默认端口(本机回环) tags: - name: status description: 读取实时判定与在途调用 - name: control description: 强制停止(会改变运行状态) - name: settings description: 挂件设置的读写 paths: /api/jingcha/status: get: tags: [status] summary: 实时快照(与 status.json 同源的紧凑载荷) parameters: - name: session in: query required: false schema: { type: string, maxLength: 200 } description: "只看这个会话的判定与在途调用(缺省或空串 = 全局)。响应里的 scope.sessionId 回显生效值;sessions 汇总始终全局;带过滤时 reasons 会标 scope=session|host。" responses: "200": description: 当前判定 + 运行计量 + 在途调用 + 最近告警 content: application/json: schema: { $ref: "#/components/schemas/StatusResponse" } "403": { $ref: "#/components/responses/Blocked" } /api/jingcha/kill: post: tags: [control] summary: 强制停止一个调用(或按范围停) description: | 传 callId 停单个调用;不传 callId 则按 scope 停范围(默认 stalled)。 嵌套子调用(父 id 加 :ptc: 序号)同样可以停;若只有父调用在跑,会明确拒绝(reason=nested-parent-only)而不是误杀父调用。 requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/KillRequest" } responses: "200": description: 停止结果(未命中时 ok=false 并给 reason) content: application/json: schema: { $ref: "#/components/schemas/KillResponse" } "403": { $ref: "#/components/responses/Blocked" } "405": { $ref: "#/components/responses/UsePost" } "415": { $ref: "#/components/responses/NeedJson" } /api/jingcha/stop: post: tags: [control] summary: 停止所有正在跑的轮次(等价界面上的停止按钮) requestBody: required: false content: application/json: schema: type: object properties: reason: { type: string, maxLength: 200, description: 记进事件流的简短原因 } responses: "200": description: 被取消的 agent 列表 content: application/json: schema: type: object properties: ok: { type: boolean, const: true } agents: { type: array, items: { type: string } } "403": { $ref: "#/components/responses/Blocked" } "405": { $ref: "#/components/responses/UsePost" } "415": { $ref: "#/components/responses/NeedJson" } /api/jingcha/settings: get: tags: [settings] summary: 读挂件设置(含默认值) responses: "200": description: 当前设置 content: application/json: schema: { $ref: "#/components/schemas/SettingsResponse" } "403": { $ref: "#/components/responses/Blocked" } post: tags: [settings] summary: 写挂件设置(字段白名单 + 数值夹紧后落盘) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/Settings" } responses: "200": description: 夹紧后的设置 content: application/json: schema: { $ref: "#/components/schemas/SettingsResponse" } "403": { $ref: "#/components/responses/Blocked" } "405": { $ref: "#/components/responses/UsePost" } "415": { $ref: "#/components/responses/NeedJson" } components: securitySchemes: apiToken: type: apiKey in: header name: x-jingcha-token description: 仅当配置里设置了 apiToken 时才需要 responses: Blocked: description: 准入被拒(非回环 / Host 不合法 / 跨站来源 / token 不对) content: application/json: schema: { $ref: "#/components/schemas/ApiError" } UsePost: description: 变更类端点不接受 GET(405) content: application/json: schema: { $ref: "#/components/schemas/ApiError" } NeedJson: description: content-type 必须是 application/json(415) content: application/json: schema: { $ref: "#/components/schemas/ApiError" } schemas: ApiError: type: object properties: ok: { type: boolean, const: false } error: { type: string } Reason: type: object properties: kind: { type: string, examples: [tool-stall, memory-leak, error-storm] } severity: { type: string, enum: [info, warn, high, critical] } text: { type: string, description: 可直接展示给用户的中文说明 } tool: { type: string } callId: { type: string } elapsedMs: { type: integer } growth: { type: number, description: memory-leak 专用:窗口内增幅比例 } window: { type: integer, description: memory-leak 专用:连续递增的心跳周期数 } scope: { type: string, enum: [session, host], description: "仅当带 ?session= 过滤时出现:session = 该理由只属于这个会话;host = 宿主级理由(事件循环等)不受过滤影响" } Inflight: type: object properties: callId: { type: string } tool: { type: string } elapsedMs: { type: integer } preview: { type: string, description: 参数摘要(已截断、已脱敏) } nested: { type: boolean } phase: { type: string, enum: [pre, run], description: pre 表示还没派发(等待裁决或审批) } silentMs: { type: integer, description: 距上次产出信号多久 } Finding: type: object properties: at: { type: string, format: date-time } kind: { type: string } severity: { type: string, enum: [info, warn, high, critical] } text: { type: string } StatusResponse: type: object description: 挂件每 2 秒拉一次的紧凑载荷(完整快照见 status.json;本接口不返回 ok / schema / pid) properties: generatedAt: { type: string, format: date-time } verdict: type: object properties: state: { type: string, enum: [ok, busy, degraded, erroring, stalled] } label: { type: string } since: { type: string, format: date-time } reasons: { type: array, items: { $ref: "#/components/schemas/Reason" } } scope: type: object description: "本次响应的会话过滤口径(?session= 生效时为该 id,否则 null)" properties: sessionId: { type: [string, "null"], description: 生效的会话 id;null = 全局 } runtime: type: object properties: eventLoopLagMs: { type: integer } maxLagMs: { type: integer } rssBytes: { type: integer } work: type: object properties: inflight: { type: array, items: { $ref: "#/components/schemas/Inflight" } } runningAgents: { type: integer } counters: type: object additionalProperties: { type: integer } sessions: type: array description: "会话汇总(**始终全局**,不随 ?session= 收窄——挂件要靠它列出会话)" items: { $ref: "#/components/schemas/SessionSummary" } findings: { type: array, items: { $ref: "#/components/schemas/Finding" } } rules: type: [array, "null"] description: 规则表快照(等价 monitor.listRules());规则系统不可用时为 null items: { $ref: "#/components/schemas/RuleInfo" } thresholds: type: object description: 当前生效的判定阈值(毫秒) additionalProperties: { type: integer } dataDir: { type: string, description: 数据目录(status.json / events.jsonl 所在) } tool: { type: [string, "null"], description: 注册的查询工具名;toolEnabled=false 时为 null(0 token) } capabilities: type: object description: 本实例支持的接口能力(给挂件做特性探测) properties: kill: { type: boolean } stopTurns: { type: boolean } autoKillAfterMs: { type: integer } sessionFilter: { type: boolean, description: "支持 ?session= 会话过滤" } ruleRegistry: { type: boolean, description: 支持第三方规则注册(registerRule / unregisterRule / setRuleEnabled / listRules) } SessionSummary: type: object description: 会话汇总(work.sessions 的一项;按在途数 / agent 数排序) properties: sessionId: { type: string } known: { type: boolean, description: false 表示调用没带会话 id(记为 (unknown)) } inflight: { type: integer, description: 该会话在途工具调用数 } runningAgents: { type: integer } oldestMs: { type: integer, description: 该会话最久在途调用的耗时 } oldestCallId: { type: [string, "null"] } idleMs: { type: integer, description: 距该会话最近一次产出信号多久 } RuleInfo: type: object description: 判定规则表的一项(等价 monitor.listRules()) properties: id: { type: string, examples: [memory-leak, error-storm] } title: { type: string } source: { type: string, description: "builtin(内置 8 条)或 custom(第三方注册)" } enabled: { type: boolean, description: 是否参与判定(false = 被 disabledRules 关掉) } escalate: { type: [string, "null"], description: 该规则命中时的升级目标(如 stall);null = 不升级 } failures: { type: integer, description: 该规则累计抛错次数(失败隔离的记账) } KillRequest: type: object properties: callId: { type: string, description: 要停的调用 id;与 scope 二选一 } scope: { type: string, enum: [stalled, all], description: 不传 callId 时按范围停,默认 stalled } reason: { type: string, maxLength: 200, description: 记进事件流;会过滤控制字符 } KillResponse: type: object properties: ok: { type: boolean, description: 是否真的停掉了 } killed: { type: array, items: { type: string }, description: 实际被中止的 callId } reason: { type: string, description: "未命中时的原因:not-live / nested-parent-only / empty-callId / ambiguous / abort-failed" } parentId: { type: string, description: reason=nested-parent-only 时给出父调用 id } inflight: { type: integer } Settings: type: object properties: version: { type: integer } position: type: object properties: mode: { type: string, enum: [docked, free] } right: { type: integer } bottom: { type: integer } left: { type: [integer, "null"] } top: { type: [integer, "null"] } size: { type: number, minimum: 0.6, maximum: 2 } opacity: { type: number, minimum: 0.2, maximum: 1 } accent: { type: string, pattern: "^$|^#[0-9a-fA-F]{3,8}$" } labels: { type: boolean } thresholds: type: object properties: yellowSec: { type: integer, minimum: 1, maximum: 3600 } redSec: { type: integer, minimum: 2, maximum: 7200 } popups: { type: boolean } popupDurationMs: { type: integer, minimum: 1500, maximum: 60000 } panel: type: object properties: defaultOpen: { type: boolean } width: { type: integer, minimum: 260, maximum: 620 } theme: { type: string, enum: [auto, dark, light] } hidden: { type: boolean } SettingsResponse: type: object properties: ok: { type: boolean, const: true } settings: { $ref: "#/components/schemas/Settings" } defaults: { $ref: "#/components/schemas/Settings" } file: { type: string, description: 落盘文件名(不返回绝对路径) }