feat(server): pi module with upstream CRUD, server-side OAuth and token refresh
- /pi/providers, /pi/upstreams CRUD, test (auth-only by default, no cost), models - subscription login via pi models.login with a suspended interaction: browser mode waits for the pasted redirect URL, device mode polls server-side; per-provider login lock, readable callback-port-in-use errors - credential store locks the pi_upstream row (select for update) so api and worker refreshes never double-write a rotated refresh token - refreshPiTokens task: refresh before expiry; failure marks expired and opens a relogin decision, closed again after a successful login - runAgent picks upstreamId > agent_run.upstream_id > default upstream > legacy settings; credentials injected by the server, never logged Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
743fe2486d
commit
fd3ffe5a8d
13 files changed
+1617
-26
No files matched your search
@@ -23,6 +23,7 @@ import {
|
||||
KnownCapabilityId,
|
||||
} from "@umt/contracts";
|
||||
import { type AgentRunRow, type DownloaderInstanceRow, sql } from "@umt/db";
|
||||
import { type RunUpstream, resolveRunUpstream } from "../pi/index.ts";
|
||||
import type { ServerContext } from "../types.ts";
|
||||
import { DEMO_SCRIPTS, parseFakeScripts } from "./fake-scripts.ts";
|
||||
import { buildGoalReport } from "./report.ts";
|
||||
@@ -113,28 +114,33 @@ export interface DriveResult {
|
||||
finalText: string;
|
||||
}
|
||||
|
||||
/** 设置 → @umt/agent 的运行参数(apiKey 现场经 secrets 解密,不落日志) */
|
||||
/**
|
||||
* 设置 → @umt/agent 的运行参数。按上游运行时(W7-A)凭据由上游的 CredentialStore 在请求时解密注入,这里不取明文;
|
||||
* 没有任何上游时回退 v0.1 旧字段(pi.provider / pi.apiKeySecretRef,apiKey 现场经 secrets 解密,不落日志)。
|
||||
*/
|
||||
async function agentSettings(
|
||||
env: AgentEnv,
|
||||
s: GlobalSettings,
|
||||
budget: AgentBudget,
|
||||
fake: boolean,
|
||||
upstream: RunUpstream | null,
|
||||
): Promise<AgentSettings> {
|
||||
const pi = s.pi;
|
||||
let apiKey = "fake-key-not-used";
|
||||
if (!fake) {
|
||||
if (!pi.apiKeySecretRef) throw new Error("未配置 Agent 上游密钥(设置 → pi.apiKey)");
|
||||
let apiKey: string | undefined;
|
||||
if (!fake && !upstream) {
|
||||
if (!pi.apiKeySecretRef) throw new Error("未配置 Pi 上游(设置 → 模型上游)");
|
||||
apiKey = await env.ctx.secrets.resolve(pi.apiKeySecretRef);
|
||||
if (!pi.model) throw new Error("未配置模型(设置 → pi.model)");
|
||||
}
|
||||
const maxTokens = budget.maxTokens ?? s.agent.maxTokensPerRun ?? undefined;
|
||||
const maxCost = budget.maxCostUsd ?? s.agent.maxCostUsdPerRun ?? undefined;
|
||||
const up = upstream?.row;
|
||||
return {
|
||||
provider: pi.provider === "openai-compatible" ? "openai-compatible" : "anthropic",
|
||||
...(pi.baseUrl && !fake ? { baseUrl: pi.baseUrl } : {}),
|
||||
apiKey,
|
||||
model: pi.model ?? "fake-model",
|
||||
thinkingLevel: pi.thinkingLevel,
|
||||
provider: up ? up.provider : pi.provider === "openai-compatible" ? "openai-compatible" : "anthropic",
|
||||
...(!up && pi.baseUrl && !fake ? { baseUrl: pi.baseUrl } : {}),
|
||||
...(apiKey ? { apiKey } : fake && !up ? { apiKey: "fake-key-not-used" } : {}),
|
||||
model: (up ? up.default_model : pi.model) ?? (fake ? "fake-model" : "default"),
|
||||
thinkingLevel: up ? up.thinking_level : pi.thinkingLevel,
|
||||
contextBudgetTokens: pi.contextBudget,
|
||||
compaction: {
|
||||
mode: pi.compaction.enabled ? "threshold" : "manual",
|
||||
@@ -311,11 +317,15 @@ export async function drive(
|
||||
const blocked = await dailyBudgetBlock(env, s, run);
|
||||
if (blocked) return { status: "failed", reason: "budget", budget: "daily", message: blocked, finalText: "" };
|
||||
|
||||
const fake = s.pi.provider === "fake";
|
||||
const scope = (run.scope ?? {}) as unknown as RunScope;
|
||||
// 上游:run.upstream_id(runAgent 的 upstreamId 覆盖)> settings.pi.defaultUpstreamId > 旧字段兼容
|
||||
let upstream: RunUpstream | null;
|
||||
let fake: boolean;
|
||||
let settings: AgentSettings;
|
||||
try {
|
||||
settings = await agentSettings(env, s, (run.budget ?? {}) as AgentBudget, fake);
|
||||
upstream = await resolveRunUpstream(ctx, run.upstream_id);
|
||||
fake = upstream ? upstream.row.provider === "fake" : s.pi.provider === "fake";
|
||||
settings = await agentSettings(env, s, (run.budget ?? {}) as AgentBudget, fake, upstream);
|
||||
} catch (err) {
|
||||
return { status: "failed", reason: "error", budget: null, message: (err as Error).message, finalText: "" };
|
||||
}
|
||||
@@ -324,9 +334,10 @@ export async function drive(
|
||||
.set({
|
||||
status: "running",
|
||||
started_at: run.started_at ?? new Date(),
|
||||
provider: fake ? "fake" : s.pi.provider,
|
||||
model: fake ? "fake-model" : s.pi.model,
|
||||
thinking_level: s.pi.thinkingLevel,
|
||||
provider: fake ? "fake" : settings.provider,
|
||||
model: upstream ? (upstream.row.default_model ?? (fake ? "fake-model" : null)) : fake ? "fake-model" : s.pi.model,
|
||||
thinking_level: settings.thinkingLevel ?? null,
|
||||
...(upstream ? { upstream_id: upstream.row.id } : {}),
|
||||
})
|
||||
.where("id", "=", runId)
|
||||
.execute();
|
||||
@@ -351,7 +362,12 @@ export async function drive(
|
||||
}
|
||||
const runtime = fake
|
||||
? createFakeRuntime({ registry, scripts, onEvent, onEventError }).runtime
|
||||
: new PiAgentRuntime({ registry, onEvent, onEventError });
|
||||
: new PiAgentRuntime({
|
||||
registry,
|
||||
onEvent,
|
||||
onEventError,
|
||||
...(upstream ? { upstream: { spec: upstream.spec, credentials: upstream.credentials } } : {}),
|
||||
});
|
||||
const maxSeq = await ctx.db
|
||||
.selectFrom("agent_run_event")
|
||||
.select(sql<number | null>`max(seq)`.as("m"))
|
||||
|
||||
Reference in new issue
Block a user