test(e2e): AG-3 agent quality with fake model; runbook capability list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
41e3647f0a
commit
3f0327ad97
3 files changed
+225
-5
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import type { FakeScript, FakeToolResult } from "@umt/agent";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { resolveBusiness } from "../../src/modules/auth-settings/engine-business.ts";
|
||||
import { createE2E, type E2E, pgAvailable, runEvents, runRow, seedBase, seedMovieTarget, waitFor } from "./harness.ts";
|
||||
|
||||
// E15 Agent 质量(AG-3,来自 AGENT-REAL 真实上游验证):
|
||||
// - 只读摘要能力 subscription.list / decision.list / download.inspect(按 id 或 hash);task.inspect 收到下载任务 ID
|
||||
// 时明确报错并指向 download.inspect;
|
||||
// - download.plan 的 requiresApproval 按授权范围判定(覆盖本作品的持续授权 → false;范围外 / 没有 → true);
|
||||
// - 超预算:run = budget_exceeded,任务页(引擎 run 的 business.outcome)显示超出预算而不是完成;
|
||||
// 被预算截断时中间过程文字不作为汇报;同一目标模板的超预算合并到同一条待处理事项。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 断言里按输出结构取值
|
||||
type Any = any;
|
||||
const hasPg = await pgAvailable();
|
||||
const HASH = "ab".repeat(20);
|
||||
const SURVEY = "盘点订阅、待处理与下载";
|
||||
const LOOP = (at: string) => `巡检库存(${at}):逐个作品查看版本目标`;
|
||||
|
||||
describe.skipIf(!hasPg)("E15 Agent 质量(fake 模型 + 真实能力)", () => {
|
||||
const scripts: FakeScript[] = [];
|
||||
const results = new Map<string, FakeToolResult[]>();
|
||||
let e: E2E;
|
||||
let target: Awaited<ReturnType<typeof seedMovieTarget>>;
|
||||
let downloadTaskId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
e = await createE2E("e15", { fakeScripts: scripts });
|
||||
const base = await seedBase(e);
|
||||
target = await seedMovieTarget(e, base.siteId);
|
||||
downloadTaskId = (
|
||||
await e.db
|
||||
.insertInto("download_task")
|
||||
.values({
|
||||
instance_id: base.instanceId,
|
||||
torrent_hash: HASH,
|
||||
infohash_v1: HASH,
|
||||
source_release_id: target.r1080,
|
||||
name: "Dune.Part.Two.2024.1080p.BluRay.x264-GRP",
|
||||
status: "failed",
|
||||
last_error: JSON.stringify({ message: "qB 状态 missingFiles" }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
await e.db
|
||||
.insertInto("decision")
|
||||
.values({
|
||||
kind: "download_failed",
|
||||
title: "下载异常:Dune.Part.Two",
|
||||
subject_type: "download_task",
|
||||
subject_id: downloadTaskId,
|
||||
dedupe_key: `download_failed:${downloadTaskId}`,
|
||||
options: JSON.stringify([{ key: "retry", label: "重试" }]),
|
||||
})
|
||||
.execute();
|
||||
scripts.push(
|
||||
{
|
||||
match: SURVEY,
|
||||
steps: [
|
||||
{ text: "先列订阅。", toolCalls: [{ capability: "subscription.list", input: { groupBy: "status" } }] },
|
||||
{ text: "再看待处理。", toolCalls: [{ capability: "decision.list", input: {} }] },
|
||||
{
|
||||
text: "按 hash 查下载。",
|
||||
toolCalls: [{ capability: "download.inspect", input: { hash: HASH.toUpperCase() } }],
|
||||
},
|
||||
// 常见误用:把下载任务 ID 当业务任务 ID
|
||||
{ text: "看任务时间线。", toolCalls: [{ capability: "task.inspect", input: { taskId: downloadTaskId } }] },
|
||||
],
|
||||
finalText: (r) => {
|
||||
results.set(SURVEY, r);
|
||||
return "## 盘点\n\n- 订阅 1 个\n- 待处理 1 条\n\n| 下载 | 状态 |\n|---|---|\n| Dune | failed |";
|
||||
},
|
||||
},
|
||||
{
|
||||
match: /^巡检库存/,
|
||||
steps: Array.from({ length: 6 }, () => ({
|
||||
text: "Let me check the inventory once more.",
|
||||
toolCalls: [{ capability: "inventory.query", input: { query: "沙丘2" } }],
|
||||
})),
|
||||
finalText: "不该到这里",
|
||||
},
|
||||
);
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await e?.close();
|
||||
}, 60_000);
|
||||
|
||||
const runToEnd = async (goal: string) => {
|
||||
const r = await e.api("POST", "/agent/runs", { goal });
|
||||
expect(r.status).toBe(201);
|
||||
const runId = r.body.data.id as string;
|
||||
const run = await waitFor(
|
||||
async () => {
|
||||
const x = await runRow(e, runId);
|
||||
return ["succeeded", "failed", "waiting", "budget_exceeded", "cancelled"].includes(x.status) ? x : null;
|
||||
},
|
||||
60_000,
|
||||
`run 结束:${goal}`,
|
||||
);
|
||||
return { runId, run };
|
||||
};
|
||||
|
||||
it("只读摘要能力:订阅分组、待处理含 options、下载按 hash;task.inspect 收到下载任务 ID 时指向 download.inspect", async () => {
|
||||
const { run } = await runToEnd(SURVEY);
|
||||
expect(run.status).toBe("succeeded");
|
||||
const r = results.get(SURVEY)!;
|
||||
const byCap = (id: string) => r.find((x) => x.capabilityId === id)!;
|
||||
expect(byCap("subscription.list")).toMatchObject({ isError: false });
|
||||
expect(byCap("subscription.list").json).toMatchObject({
|
||||
total: 1,
|
||||
groups: [{ key: "active", count: 1 }],
|
||||
items: [{ title: "沙丘2", mediaEntityId: target.entityId, slots: { total: 1, satisfied: 0 } }],
|
||||
});
|
||||
expect(byCap("decision.list").json).toMatchObject({
|
||||
total: 1,
|
||||
byKind: [{ kind: "download_failed", count: 1 }],
|
||||
items: [{ kind: "download_failed", subjectId: downloadTaskId, options: [{ key: "retry", label: "重试" }] }],
|
||||
});
|
||||
expect(byCap("download.inspect").json).toMatchObject({
|
||||
downloadTask: { id: downloadTaskId, torrentHash: HASH },
|
||||
release: { id: target.r1080 },
|
||||
decisions: [{ kind: "download_failed" }],
|
||||
otherMatches: [],
|
||||
});
|
||||
expect(byCap("task.inspect")).toMatchObject({ isError: true });
|
||||
expect(byCap("task.inspect").text).toContain("download.inspect");
|
||||
|
||||
// HTTP 同一路径:按 id 查;不存在的下载任务 404 类错误
|
||||
const inv = await e.api("POST", "/capabilities/download.inspect/invoke", { input: { downloadTaskId } });
|
||||
expect(inv.status).toBe(200);
|
||||
expect(JSON.stringify(inv.body)).toContain(HASH);
|
||||
const bad = await e.api("POST", "/capabilities/release.search/invoke", { input: { query: "a" } });
|
||||
expect(bad.status).toBeGreaterThanOrEqual(400);
|
||||
expect(JSON.stringify(bad.body)).toContain("检索词太短");
|
||||
}, 90_000);
|
||||
|
||||
it("download.plan 的 requiresApproval 按授权范围判定", async () => {
|
||||
const plan = async () => {
|
||||
const r = await e.api("POST", "/capabilities/download.plan/invoke", {
|
||||
input: { slotId: target.slotId, sourceReleaseId: target.r1080 },
|
||||
});
|
||||
expect(r.status).toBe(200);
|
||||
return r.body.data.output ?? r.body.data;
|
||||
};
|
||||
expect((await plan()).requiresApproval).toBe(true);
|
||||
// 范围外的持续授权不算覆盖
|
||||
const other = await e.api("POST", "/automation-grants", {
|
||||
action: "download.submit",
|
||||
scope: { mediaEntityIds: ["00000000-0000-4000-8000-000000000001"] },
|
||||
reason: "e2e 范围外授权",
|
||||
});
|
||||
expect(other.status).toBe(201);
|
||||
expect((await plan()).requiresApproval).toBe(true);
|
||||
const g = await e.api("POST", "/automation-grants", {
|
||||
action: "download.submit",
|
||||
scope: { mediaEntityIds: [target.entityId] },
|
||||
reason: "e2e 授权",
|
||||
});
|
||||
expect(g.status).toBe(201);
|
||||
expect((await plan()).requiresApproval).toBe(false);
|
||||
}, 60_000);
|
||||
|
||||
it("超预算:状态为超出预算(任务页同样),中间过程不当汇报,同一目标模板只一条待处理事项", async () => {
|
||||
await e.settings({ agent: { maxToolCalls: 3 } });
|
||||
const a = await runToEnd(LOOP("2026-09-29T01:00:00.000Z"));
|
||||
expect(a.run.status).toBe("budget_exceeded");
|
||||
expect(a.run.summary).not.toContain("Let me check");
|
||||
expect(a.run.summary).not.toContain("模型汇报");
|
||||
// 预算将尽提醒进了工具结果
|
||||
const ends = (await runEvents(e, a.runId)).filter((x) => x.type === "tool_call_end");
|
||||
expect(ends.length).toBeGreaterThan(0);
|
||||
|
||||
// 引擎 run 正常跑完,业务结果是超出预算:任务页按 business.outcome 显示「超出预算」而不是「完成」
|
||||
const task = await e.db
|
||||
.selectFrom("task")
|
||||
.select(["engine_run_id", "status", "output"])
|
||||
.where("id", "=", a.run.task_id!)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(task.status).toBe("failed");
|
||||
expect((task.output as Any).status).toBe("budget_exceeded");
|
||||
const biz = await resolveBusiness(e.db, e.ow, [task.engine_run_id!]);
|
||||
const row = { business: biz.get(task.engine_run_id!) };
|
||||
expect(row?.business).toMatchObject({ kind: "agent", outcome: "budget_exceeded" });
|
||||
|
||||
const b = await runToEnd(LOOP("2026-09-29T02:00:00.000Z"));
|
||||
expect(b.run.status).toBe("budget_exceeded");
|
||||
const ds = await e.db.selectFrom("decision").selectAll().where("dedupe_key", "like", "agent_budget:%").execute();
|
||||
expect(ds).toHaveLength(1);
|
||||
expect(ds[0]!.dedupe_key).toMatch(/^agent_budget:goal:/);
|
||||
expect(ds[0]!.agent_run_id).toBe(b.runId);
|
||||
expect((ds[0]!.evidence as Any).runIds.sort()).toEqual([a.runId, b.runId].sort());
|
||||
expect(ds[0]!.attempts).toBeGreaterThanOrEqual(1);
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -151,3 +151,4 @@ NFO / 图片投影、身份巡检与挂载根因聚合、HTTP 失败自动切浏
|
||||
- 追加(用户 2026-09-29):对比 Webtop 与浏览器镜像合一 vs 与本体镜像合一(协调者倾向前者:同会话、生命周期隔离、安全面、镜像大小);**视觉自动化闭环**:Agent 截图(多模态喂视觉模型)→ 坐标点击/输入 → 再截图,有界步数与预算,用于登录/验证码/交互式挑战;二次验证与本人设备确认不自动过;每步证据入待处理,失败转人工接管。能力:`browser.screenshot`(图片内容)、`browser.click_at`、`browser.type_at`、`browser.solve_challenge`(有界子流程)。
|
||||
- **上游模型列表一键获取(用户 2026-09-29)**:`POST /pi/upstreams/{id}/models/refresh` 按 provider 调远程模型列表接口(OpenAI 兼容 /models、Anthropic /v1/models 等),存库带时间,UI 可搜索选择 + 手填,失败回退静态表。用户已接入 OpenCode Go 订阅,Agent 能力测试统一用 DeepSeek V4.1 Flash(视觉部分需视觉模型)。
|
||||
- **AG-3 Agent 质量(来自 AGENT-REAL 真实上游验证,2026-09-29)**:补只读能力 subscription.list / decision.list / download.inspect(task.inspect 不再接收 download_task id);`download.plan` 的 requiresApproval 按授权范围判定;测试补全带 sessionId(OpenCode 需 x-opencode-session);等待期间 token/费用显示;超预算 run 在任务页标"超预算";同根因超预算不重复建待处理;汇报 Markdown 渲染;系统提示:禁止空/单字探查查询、参数类型(externalId 字符串)、子会话预算不超父剩余、中文思考与汇报、被中断时输出中文小结;重跑 AGENT-REAL 七场景。
|
||||
- 进度(2026-09-29):除真机重跑外已完成——38 个能力(新增 3 个只读摘要,已进后台调查 / 巡检白名单);`coveringGrant` 与授权器共用;预算将尽提醒、被打断时丢弃中间过程文字;等待期间用量写回;任务页按 `business.outcome` 显示超出预算;超预算待处理按根因合并;汇报 Markdown 安全子集渲染;fake 模型 e2e 见 `apps/server/test/e2e/e15-agent-quality.test.ts`。开发库没有真实上游(只有 fake),AGENT-REAL a/e/g 真机重跑留给协调者重建用户栈后做。
|
||||
+28
-5
@@ -6,7 +6,25 @@
|
||||
|
||||
## 能力
|
||||
|
||||
启动时装配 15 个能力(`catalog.lookup`、`release.search`、`release.inspect`、`identity.resolve`、`download.plan`、`download.submit`、`download.recheck`、`organize.plan`、`organize.execute`、`task.inspect`、`task.retry`、`task.cancel`、`policy.preview`、`policy.apply`、`inventory.query`)。注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。网页搜索 `web.search` / `web.fetch` 见 [websearch.md](websearch.md)。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。
|
||||
启动时装配 38 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
|
||||
|
||||
| 分组 | 能力 |
|
||||
|---|---|
|
||||
| 查询(read) | `catalog.lookup`、`release.search`、`release.inspect`、`inventory.query`、`subscription.list`、`decision.list`、`download.inspect`、`task.inspect`、`site.list`、`site.user_data` |
|
||||
| 计划(preview / propose) | `identity.resolve`、`download.plan`、`organize.plan`、`policy.preview` |
|
||||
| 执行(execute) | `download.submit`、`download.recheck`、`organize.execute`、`task.retry`、`task.cancel`、`policy.apply`、`site.health_check`、`site.refresh_login` |
|
||||
| 原站浏览器 | `browser.snapshot`、`browser.navigate`、`browser.interact`、`browser.download`、`browser.screenshot`、`browser.click_at`、`browser.type_at`、`browser.scroll`、`browser.wait`、`browser.solve_challenge`(见 [browser.md](browser.md)) |
|
||||
| 会话 | `agent.plan`、`agent.propose_options`、`agent.spawn`、`agent.await` |
|
||||
| 网页搜索 | `web.search`、`web.fetch`(见 [websearch.md](websearch.md)) |
|
||||
|
||||
AG-3 补的只读摘要(类别 read,scope `read`,都在后台调查 / 巡检白名单里):
|
||||
|
||||
- `subscription.list {status?, mediaKind?, query?, groupBy=status|mediaKind|none, limit≤200}`:订阅分组计数 + 条目(作品、模式、状态、优先级、版本目标总数 / 已满足数、上次 / 下次搜索)。查单个作品的文件与版本目标仍用 `inventory.query`。
|
||||
- `decision.list {status?, kind?, limit≤100}`:待处理事项摘要,缺省只列 open / investigating / awaiting_user,按严重度排序;每条带可选处理 `options`。只读,处理仍由用户在待处理中心选。
|
||||
- `download.inspect {downloadTaskId? | hash?}`:按下载任务 ID 或种子 hash(v1 / v2,大小写均可)读下载任务、关联计划、资源、业务任务(提交下载 / 核验入库,ID 可交给 `task.inspect` / `task.retry`)、整理记录与待处理事项;同 hash 在多个下载器实例时其余 ID 放 `otherMatches`。
|
||||
- `task.inspect` 只接受业务任务(`task` 表)ID;传入下载任务 ID 时返回 `invalid_input`:"这是下载任务 ID…用 download.inspect 查看"。
|
||||
|
||||
检索词(`catalog.lookup` / `release.search` / `inventory.query` / `subscription.list` 的 `query`)去掉首尾空白后不能为空、不能是单个英文字母 / 数字 / 符号、不能只有通配符与标点(`invalid_input`:"检索词太短…");中文单字片名允许。`catalog.lookup.externalId.externalId` 一律是字符串。
|
||||
|
||||
- **授权**(`agent/authorize.ts`):propose / approve / execute 类必须满足以下之一:
|
||||
- 单次审批 `approval`:状态为 approved,绑定计划类型 + 计划 ID + payload hash + revision,且未过期。执行成功后审批标记为 consumed。
|
||||
@@ -15,7 +33,7 @@
|
||||
模型参数里写"用户已同意"不起作用。输入里的 `approvalId` 只是一个指针,授权结果以库里的审批记录为准。控制台管理员从 Web 手动调用(actor=`user:*`)算人工授权,API Key、Agent 和后台任务都必须有授权记录。自动化总开关关闭或处于只读对账时,自动来源的 execute 一律拒绝。
|
||||
- **审计**:每次调用都写一条 `audit_event`,action 为 `capability.invoke` 或 `capability.denied`,payload 包含 outcome、授权 ID、幂等键和脱敏后的输入摘要。
|
||||
- **幂等键与操作账本**:有副作用的调用由服务端生成幂等键,格式为 `id:v版本:<agentRunId|taskId>:sha256(规范化输入)`,并记入 `capability_invocation`。状态为 succeeded 时直接返回记录的结果。状态为 running(进程在调用途中崩溃)时,用同一个键重跑处理器来对账,处理器本身是幂等的。
|
||||
- `download.plan` 只生成 **draft** 计划:不预留 Slot,不计入预算,也不会被后台巡检补提交。`download.submit` 在授权通过后先把 draft 转成 reserved(Slot revision 必须与生成计划时一致),再入队 `submitDownload`,和自动路径共用幂等键 `plan:<planId>`。
|
||||
- `download.plan` 只生成 **draft** 计划:不预留 Slot,不计入预算,也不会被后台巡检补提交。`requiresApproval` 按授权范围判定:有覆盖本计划的持续授权(action 覆盖 `download.submit`、对象 / 类型范围含目标作品、绑定策略仍发布、每日次数 / 字节预算有余量,与 authorize 同一函数 `coveringGrant`)时为 false,否则为 true;没生成计划时按 Slot 的作品判定。`download.submit` 在授权通过后先把 draft 转成 reserved(Slot revision 必须与生成计划时一致),再入队 `submitDownload`,和自动路径共用幂等键 `plan:<planId>`。
|
||||
- `organize.plan` 把冻结的整理计划存成 artifact(kind=plan),`payloadHash` 就是计划指纹。`organize.execute` 执行前会重新冻结一次,指纹不一致就返回 conflict。
|
||||
|
||||
## 会话 API
|
||||
@@ -49,10 +67,15 @@
|
||||
- 入库完成 → `succeeded`,汇报带整理记录 ID;
|
||||
- 仍在等待 → `waiting`;
|
||||
- 失败 → `failed`;
|
||||
- 超预算 → `budget_exceeded`,同时开一条待处理事项 `agent_budget:<runId>`;
|
||||
- 超预算 → `budget_exceeded`,同时开一条待处理事项。同根因只保留一条未关闭事项(`attempts`+1,`evidence.runIds` 累积,`agent_run_id` 指向最新一次):去重键为 `agent_budget:root:<被调查事项的去重键>`(后台调查)、`agent_budget:schedule:<定时任务>`(巡检)或 `agent_budget:goal:<目标模板指纹>`(去掉时间戳 / UUID / 数字后的目标 sha256 前 16 位)。业务任务行记 `failed`,`output.status=budget_exceeded`;任务页按引擎 run 的 `business.outcome`(= `agent_run.status`)显示「超出预算」,不显示「完成」;
|
||||
- 取消 → `cancelled`。
|
||||
|
||||
模型的最终文本只附在汇报后面,标注"模型汇报(未经核查)"。
|
||||
模型的最终文本只附在汇报后面,标注"模型汇报(未经核查)"。被预算或中止打断时,最后一次工具调用之前的文字只是中间过程,不作为模型汇报(`finalText` 置空,会话页显示服务端的中文汇总)。
|
||||
|
||||
**预算提醒**:工具调用只剩 ≤2 次,或 token / 费用 / 时长用到 85% 时,运行时在工具结果末尾追加「【预算提醒】…停止探查,下一条回复直接用中文写小结」,系统提示要求模型见到后直接收尾。
|
||||
**等待期间的用量**:会话挂起(审批 / 选方案 / 等子会话)时没有 `run_completed`,drive 退出前把运行时累计用量按字段取较大值写回 `agent_run.usage`,会话页和列表在等待期间照样显示已用 token / 费用。
|
||||
|
||||
模型汇报在会话页按 Markdown 安全子集渲染(标题、列表、表格、引用、代码、粗体 / 斜体 / 链接;原文 HTML 按文字显示,链接只放行 http(s) 与站内路径),流式输出中仍按原文显示。
|
||||
|
||||
worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步骤会从头重跑。已经完成的工具调用不会重放。副作用调用的结果模型会看到"结果未知",模型需要重新核对;同参数再调用一次,会直接拿到账本里的结果。
|
||||
|
||||
@@ -154,7 +177,7 @@ runAgent 按 `runAgent{upstreamId}` → `agent_run.upstream_id` → 默认上游
|
||||
| `GET /pi/providers?models=false` | 能力表:authKinds(第一个是推荐项)、`oauth.modes`(browser / device)、`termsNote`(条款风险)、`baseUrlOverridable`、`envFields`、静态模型表 |
|
||||
| `GET /pi/upstreams`、`GET /pi/upstreams/{id}` | 不含密钥,只给 `hasCredential`、`credentialMask`(前 4 后 4)、`account`、`oauthExpiresAt`、`isDefault` |
|
||||
| `POST /pi/upstreams`、`PATCH /pi/upstreams/{id}`、`DELETE …` | `apiKey` 只写(PATCH 传 null 清除);provider 不可改;改 authKind 会删掉旧凭据;删除连同 secret |
|
||||
| `POST /pi/upstreams/{id}/test {mode?: auth\|completion, model?}` | 缺省 `auth`:解析凭据(OAuth 必要时刷新)+ 校验模型在目录中,**不产生费用**;`completion` 再发一次最多 16 token 的补全 |
|
||||
| `POST /pi/upstreams/{id}/test {mode?: auth\|completion, model?}` | 缺省 `auth`:解析凭据(OAuth 必要时刷新)+ 校验模型在目录中,**不产生费用**;`completion` 再发一次最多 16 token 的补全,带一次性 `sessionId`(OpenCode Zen / Go 据此加 `x-opencode-session` 头,缺了会被拒) |
|
||||
| `GET /pi/upstreams/{id}/models` | `{source: remote\|static, fetchedAt, models[], remoteSupported, error, errorKind}`:拉过远程列表 → 远程与静态目录合并去重(同 id 远程优先,远程缺的字段用静态补,远程在前);没拉过 → 静态目录(已配置凭据时按 provider 过滤,如 Copilot) |
|
||||
| `POST /pi/upstreams/{id}/models/refresh` | 「获取模型列表」:用该上游的凭据调 provider 的模型列表接口,成功存 `pi_upstream.models` + `models_fetched_at`(迁移 0044)。失败不动已存列表,返回当前列表 + 一句话 `error`,`errorKind` = auth(401/403)/ network / unsupported / config / other。改 Base URL 或 `extra.api` 会清掉已存列表 |
|
||||
| `POST /pi/models/probe {provider, authKind, apiKey?, baseUrl?, extra?}` | 新建上游前试拉(凭据只用这一次,不落库);新建向导创建后会用已存凭据再拉一次并存库 |
|
||||
|
||||
Reference in new issue
Block a user