refactor(agent): runAgent uses Agent upstreams only; deprecate legacy settings.pi fields

pi.provider / baseUrl / model / thinkingLevel / apiKeySecretRef are now
DEPRECATED_SETTINGS_FIELDS: kept in the schema for stored JSON, dropped
from PATCH /settings and from the settings sections, never read by the
server. runAgent fails with a clear message when no upstream is set.
The agent demo gate checks the default upstream provider; e2e seeds a
fake upstream as default. OpenAPI summaries say Agent upstream.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 13:45:40 +08:00
1 parent fb7727cf66
commit 3e37ac8ae7
15 files changed
+122 -115

No files matched your search

@@ -94,24 +94,22 @@ describe.skipIf(!hasPg)("W2-A 管理 API(真实 PG)", () => {
expect((await json(r)).error.message).toMatch(/恢复水位/);
});
it("Pi apiKey 只写:存为 secret,响应/审计/日志不含明文;null 清除并删除 secret", async () => {
it("旧 pi.apiKey / provider / model 不再接受:被丢弃,不建 secret,不改设置", async () => {
const plain = "sk-live-super-secret-0001";
const r = await json(await admin("PATCH", "/settings", { pi: { apiKey: plain, model: "claude-x" } }));
const ref = r.data.pi.apiKeySecretRef;
expect(ref).toMatch(/^[0-9a-f-]{36}$/);
const before = (await json(await admin("GET", "/settings"))).data.pi;
const r = await json(
await admin("PATCH", "/settings", { pi: { apiKey: plain, provider: "openai", model: "claude-x" } }),
);
expect(r.data.pi).toMatchObject({
provider: before.provider,
model: before.model,
apiKeySecretRef: before.apiKeySecretRef,
});
expect(JSON.stringify(r)).not.toContain(plain);
expect(await h.ctx.secrets.resolve(ref)).toBe(plain);
// 再改一次:原地轮换,ref 不变
const r2 = await json(await admin("PATCH", "/settings", { pi: { apiKey: `${plain}-2` } }));
expect(r2.data.pi.apiKeySecretRef).toBe(ref);
expect(await h.ctx.secrets.resolve(ref)).toBe(`${plain}-2`);
const audits = await sql<{
payload: unknown;
}>`select payload from audit_event where action = 'settings.update'`.execute(h.db);
expect(JSON.stringify(audits.rows)).not.toContain(plain);
const r3 = await json(await admin("PATCH", "/settings", { pi: { apiKey: null } }));
expect(r3.data.pi.apiKeySecretRef).toBeNull();
await expect(h.ctx.secrets.resolve(ref)).rejects.toThrow(/不存在/);
const n = await sql<{ n: number }>`select count(*)::int as n from secret where purpose = 'llm_api_key'`.execute(
h.db,
);
expect(n.rows[0]!.n).toBe(0);
noLeak(plain);
});
});
+17 -2
View File
@@ -20,6 +20,7 @@ import { type Bootstrapped, bootstrap } from "../../src/context.ts";
import { createApp } from "../../src/http/app.ts";
import { EventBus } from "../../src/http/sse.ts";
import { createLogger } from "../../src/logger.ts";
import { setDefaultUpstream } from "../../src/modules/pi/store.ts";
import type { ServerModule } from "../../src/modules/types.ts";
import { type RunningWorker, startWorker } from "../../src/worker/index.ts";
import { withSchemaLock } from "../auth-settings/harness.ts";
@@ -215,9 +216,23 @@ export async function createE2E(name: string, opts: E2EOptions = {}): Promise<E2
});
},
};
// 通用设置:fake 模型、TMDB 走设置里的 secret、结算快一点
// 通用设置:fake 上游(脚本化假模型,设为默认)、TMDB 走设置里的 secret、结算快一点
const fakeUpstream = await db
.insertInto("pi_upstream")
.values({
name: "fake",
provider: "fake",
auth_kind: "none",
default_model: "fake-model",
thinking_level: "medium",
extra: JSON.stringify({}),
enabled: true,
status: "ok",
})
.returning("id")
.executeTakeFirstOrThrow();
await setDefaultUpstream(booted.ctx, fakeUpstream.id);
await e.settings({
pi: { provider: "fake", model: "fake-model" },
metadata: { tmdbApiKey: TMDB_KEY, tmdbBaseUrl: `${tmdb.url}/3` },
retention: { seedRetentionDays: 0 },
});
+9 -1
View File
@@ -13,7 +13,7 @@ import { ensureSetupToken } from "../../src/modules/auth-settings/auth-service.t
import { createSecretStore, createSettingsReader } from "../../src/modules/auth-settings/provided.ts";
import { createPiModule } from "../../src/modules/pi/index.ts";
import { refreshPiTokens } from "../../src/modules/pi/refresh.ts";
import { dbCredentialStore } from "../../src/modules/pi/store.ts";
import { dbCredentialStore, setDefaultUpstream } from "../../src/modules/pi/store.ts";
import type { ServerContext } from "../../src/modules/types.ts";
import {
Browser,
@@ -383,5 +383,13 @@ describe.skipIf(!hasPg)("Pi 上游(真实 PG)", () => {
const d3 = await drive(env, h.ctx.capabilities, r3, new AbortController().signal);
expect(d3).toMatchObject({ status: "failed" });
expect(d3.message).toMatch(/已停用/);
// 没有默认上游:旧 settings.pi 字段不再兜底,直接失败
await setDefaultUpstream(h.ctx, null);
const r4 = await newRun(null);
const d4 = await drive(env, h.ctx.capabilities, r4, new AbortController().signal);
expect(d4).toMatchObject({ status: "failed" });
expect(d4.message).toMatch(/未配置 Agent 上游/);
await setDefaultUpstream(h.ctx, fake.id);
});
});