chore: disable external and frontend tools
This commit is contained in:
@@ -82,13 +82,6 @@ const envSchema = z
|
||||
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
|
||||
// 旧版 client 模式环境变量名,保留兼容,解析时会映射到 OPENCODE_CLIENT_BASE_URL。
|
||||
OPENCODE_BASE_URL: z.string().url().optional(),
|
||||
// TJWater 后端 API 的基础地址。
|
||||
TJWATER_API_BASE_URL: z.string().default("http://127.0.0.1:8000"),
|
||||
// 本地实验环境默认开放受控领域工具;生产环境仍由下方鉴权校验保护。
|
||||
TJWATER_API_ENABLED: optionalBoolean(true),
|
||||
TJWATER_AUTH_MODE: z.enum(["disabled"]).default("disabled"),
|
||||
// 代理调用 TJWater 后端 API 的超时时间(毫秒)。
|
||||
TJWATER_API_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
|
||||
// 后端结果在直接内联返回给模型前允许的最大字节数。
|
||||
MAX_INLINE_RESULT_BYTES: z.coerce.number().int().positive().default(12000),
|
||||
// 生成结果 preview 时最多抽样的条目数。
|
||||
@@ -141,13 +134,6 @@ const envSchema = z
|
||||
.default(3600000),
|
||||
})
|
||||
.superRefine((env, ctx) => {
|
||||
if (env.NODE_ENV === "production" && env.TJWATER_API_ENABLED && env.TJWATER_AUTH_MODE === "disabled") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["TJWATER_AUTH_MODE"],
|
||||
message: "TJWATER API tools cannot run without authentication in production",
|
||||
});
|
||||
}
|
||||
if (env.OPENCODE_MODE === "client" && !env.OPENCODE_CLIENT_BASE_URL) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
|
||||
@@ -2,10 +2,7 @@ import { z } from "zod";
|
||||
import type { FrontendActionManifest } from "./types.js";
|
||||
|
||||
type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny };
|
||||
const zoomInput = z.object({ x: z.number().finite(), y: z.number().finite(), source_crs: z.enum(["EPSG:3857", "EPSG:4326"]).optional(), zoom: z.number().finite().optional(), duration_ms: z.number().finite().nonnegative().optional() }).strict();
|
||||
const definitions: Definition[] = [
|
||||
{ manifest: { id: "zoom_to_map", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["map_overlay"] }, inputSchema: zoomInput, outputSchema: z.object({}).passthrough() },
|
||||
];
|
||||
const definitions: Definition[] = [];
|
||||
|
||||
export const frontendActionRegistry = definitions.map((item) => item.manifest);
|
||||
export const getFrontendActionDefinition = (name: string) => definitions.find((item) => item.manifest.id === name);
|
||||
|
||||
@@ -830,7 +830,7 @@ export const streamPromptResponse = async ({
|
||||
params: toolParams,
|
||||
reason,
|
||||
});
|
||||
if (envelope && !(suppressLegacyFrontendActions && part.tool === "zoom_to_map")) {
|
||||
if (envelope) {
|
||||
write("ui_envelope", {
|
||||
session_id: clientSessionId,
|
||||
envelope_id: createEnvelopeId(part.tool),
|
||||
|
||||
@@ -61,11 +61,8 @@ const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
||||
|
||||
const toolLabels: Record<string, string> = {
|
||||
memory_manager: "记忆写入",
|
||||
geocode: "地理编码",
|
||||
session_search: "历史会话检索",
|
||||
skill_manager: "流程沉淀",
|
||||
web_search: "网页搜索",
|
||||
zoom_to_map: "地图缩放",
|
||||
};
|
||||
|
||||
export const logDevelopmentDebug = (
|
||||
|
||||
@@ -207,9 +207,6 @@ export const updateLastAssistantQuestion = (
|
||||
});
|
||||
|
||||
const getToolArtifactKind = (tool: string): ToolArtifactKind => {
|
||||
if (tool === "zoom_to_map") {
|
||||
return "map";
|
||||
}
|
||||
return "tool";
|
||||
};
|
||||
|
||||
@@ -217,7 +214,6 @@ const getToolArtifactTitle = (tool: string, params: Record<string, unknown>) =>
|
||||
if (typeof params.title === "string" && params.title.trim()) {
|
||||
return params.title.trim();
|
||||
}
|
||||
if (tool === "zoom_to_map") return "缩放到地图坐标";
|
||||
return tool || "工具调用";
|
||||
};
|
||||
|
||||
|
||||
+1
-174
@@ -15,12 +15,7 @@ import { ResultReferenceStore } from "./results/store.js";
|
||||
import { buildChatRouter } from "./routes/chat.js";
|
||||
import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js";
|
||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||
import { ServerApiGateway } from "./serverApi/gateway.js";
|
||||
import { isDomainToolName } from "./serverApi/schemas.js";
|
||||
import {
|
||||
getRuntimeSessionContext,
|
||||
type RuntimeSessionContext,
|
||||
} from "./runtime/sessionContext.js";
|
||||
import { getRuntimeSessionContext } from "./runtime/sessionContext.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -37,7 +32,6 @@ const learningOrchestrator = new LearningOrchestrator(
|
||||
);
|
||||
const resultReferenceStore = new ResultReferenceStore();
|
||||
const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore);
|
||||
const serverApiGateway = new ServerApiGateway(resultReferenceResolver);
|
||||
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
||||
const frontendActionCoordinator = new FrontendActionCoordinator();
|
||||
|
||||
@@ -46,25 +40,6 @@ process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
|
||||
app.post("/internal/tools/server-api/:tool", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
return;
|
||||
}
|
||||
const name = req.params.tool;
|
||||
if (!isDomainToolName(name)) {
|
||||
res.status(404).json({ message: "unknown domain tool" });
|
||||
return;
|
||||
}
|
||||
const sessionId = typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
|
||||
if (!context) {
|
||||
res.status(404).json({ message: "session context not found" });
|
||||
return;
|
||||
}
|
||||
res.json(await serverApiGateway.execute(name, req.body?.args ?? {}, context));
|
||||
});
|
||||
|
||||
app.post("/internal/frontend-actions/request", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
@@ -155,154 +130,6 @@ app.post("/internal/tools/session-search", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
const callBackendJson = async (
|
||||
path: string,
|
||||
_context: RuntimeSessionContext,
|
||||
payload: unknown,
|
||||
) => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
const response = await fetch(new URL(path, config.TJWATER_API_BASE_URL), {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
text,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
|
||||
const parseStringArray = (value: unknown) =>
|
||||
Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string")
|
||||
: undefined;
|
||||
|
||||
const webSearchFreshnessMap: Record<string, string> = {
|
||||
no_limit: "noLimit",
|
||||
one_day: "oneDay",
|
||||
one_week: "oneWeek",
|
||||
one_month: "oneMonth",
|
||||
one_year: "oneYear",
|
||||
};
|
||||
|
||||
const normalizeWebSearchFreshness = (value: unknown) => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return webSearchFreshnessMap[value] ?? value;
|
||||
};
|
||||
|
||||
app.post("/internal/tools/web-search", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId =
|
||||
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
|
||||
if (!context) {
|
||||
res.status(404).json({
|
||||
message: "session context not found",
|
||||
detail: sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const query = typeof req.body?.query === "string" ? req.body.query.trim() : "";
|
||||
if (!query) {
|
||||
res.status(400).json({ message: "query is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const count =
|
||||
typeof req.body?.count === "number" && Number.isFinite(req.body.count)
|
||||
? Math.trunc(req.body.count)
|
||||
: undefined;
|
||||
const payload = {
|
||||
query,
|
||||
freshness: normalizeWebSearchFreshness(req.body?.freshness),
|
||||
summary:
|
||||
typeof req.body?.summary === "boolean" ? req.body.summary : undefined,
|
||||
count,
|
||||
include: parseStringArray(req.body?.include),
|
||||
exclude: parseStringArray(req.body?.exclude),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await callBackendJson(
|
||||
"/api/v1/web-search",
|
||||
context,
|
||||
payload,
|
||||
);
|
||||
res
|
||||
.status(response.ok ? 200 : response.status)
|
||||
.type("application/json")
|
||||
.send(response.text);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
res.status(503).json({
|
||||
message: "web search service unavailable",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/internal/tools/geocode", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId =
|
||||
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
|
||||
if (!context) {
|
||||
res.status(404).json({
|
||||
message: "session context not found",
|
||||
detail: sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keyword =
|
||||
typeof req.body?.keyword === "string" ? req.body.keyword.trim() : "";
|
||||
if (!keyword) {
|
||||
res.status(400).json({ message: "keyword is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await callBackendJson(
|
||||
"/api/v1/geocode",
|
||||
context,
|
||||
{ keyword },
|
||||
);
|
||||
res
|
||||
.status(response.ok ? 200 : response.status)
|
||||
.type("application/json")
|
||||
.send(response.text);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
res.status(503).json({
|
||||
message: "geocoding service unavailable",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use(
|
||||
"/api/v1/agent/chat",
|
||||
buildChatRouter(
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { config } from "../config.js";
|
||||
import { ResultReferenceResolver } from "../results/resolver.js";
|
||||
import { RESULT_REFERENCE_KIND, RESULT_REFERENCE_SOURCE } from "../results/store.js";
|
||||
import type { RuntimeSessionContext } from "../runtime/sessionContext.js";
|
||||
import { domainToolSchemas, type DomainToolName } from "./schemas.js";
|
||||
|
||||
export type GatewayErrorCode = "VALIDATION_FAILED" | "NOT_FOUND" | "UPSTREAM_UNAVAILABLE" | "TIMEOUT" | "PROJECT_CONTEXT_INVALID";
|
||||
type Fetch = typeof fetch;
|
||||
|
||||
const responseSchemas: Record<DomainToolName, z.ZodTypeAny> = {
|
||||
get_project_context: z.object({
|
||||
project_id: z.string().uuid(),
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
status: z.enum(["active", "inactive"]),
|
||||
project_role: z.string(),
|
||||
gs_workspace: z.string(),
|
||||
map_extent: z.object({ xmin: z.number(), ymin: z.number(), xmax: z.number(), ymax: z.number() }).nullable(),
|
||||
}),
|
||||
list_scada_assets: z.object({ assets: z.array(z.unknown()) }),
|
||||
query_scada_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }),
|
||||
};
|
||||
|
||||
export class ServerApiGateway {
|
||||
constructor(private readonly resolver: ResultReferenceResolver, private readonly fetchImpl: Fetch = fetch) {}
|
||||
|
||||
async execute(name: DomainToolName, rawArgs: unknown, context: RuntimeSessionContext) {
|
||||
if (!config.TJWATER_API_ENABLED) return failure("UPSTREAM_UNAVAILABLE", "TJWater API tools are disabled");
|
||||
if (!context.projectId || !z.string().uuid().safeParse(context.projectId).success) {
|
||||
return failure("PROJECT_CONTEXT_INVALID", "project context is unavailable or invalid");
|
||||
}
|
||||
const parsed = domainToolSchemas[name].safeParse(rawArgs);
|
||||
if (!parsed.success) return failure("VALIDATION_FAILED", "tool parameters are invalid", parsed.error.flatten());
|
||||
try {
|
||||
const data = await this.dispatch(name, parsed.data, context);
|
||||
const validated = responseSchemas[name].safeParse(data);
|
||||
if (!validated.success) return failure("UPSTREAM_UNAVAILABLE", "upstream returned an invalid response");
|
||||
const bytes = Buffer.byteLength(JSON.stringify(validated.data));
|
||||
if (bytes <= config.MAX_INLINE_RESULT_BYTES) return { ok: true, data: validated.data, truncated: false };
|
||||
const record = await this.resolver.register({
|
||||
actorKey: context.actorKey, clientSessionId: context.clientSessionId, data: validated.data,
|
||||
kind: RESULT_REFERENCE_KIND.serverApiPayload, projectId: context.projectId,
|
||||
projectKey: context.projectKey, schemaVersion: 1, sessionId: context.clientSessionId,
|
||||
source: RESULT_REFERENCE_SOURCE.serverApi, traceId: context.traceId,
|
||||
});
|
||||
return { ok: true, result_ref: record.resultRef, preview: record.preview, result_size_bytes: bytes, truncated: true };
|
||||
} catch (error) {
|
||||
if (error instanceof GatewayHttpError) {
|
||||
if (error.status === 404) return failure("NOT_FOUND", "requested resource was not found");
|
||||
return failure("UPSTREAM_UNAVAILABLE", `upstream request failed (${error.status})`);
|
||||
}
|
||||
if (error instanceof DOMException && error.name === "AbortError") return failure("TIMEOUT", "upstream request timed out");
|
||||
return failure("UPSTREAM_UNAVAILABLE", "upstream service is unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
private async dispatch(name: DomainToolName, args: any, context: RuntimeSessionContext): Promise<unknown> {
|
||||
if (name === "get_project_context") {
|
||||
const projects = await this.request("GET", "/api/v1/projects", undefined, context);
|
||||
if (!Array.isArray(projects)) throw new GatewayHttpError(502);
|
||||
const project = projects.find((item) => (
|
||||
typeof item === "object"
|
||||
&& item !== null
|
||||
&& "project_id" in item
|
||||
&& item.project_id === context.projectId
|
||||
));
|
||||
if (!project) throw new GatewayHttpError(404);
|
||||
return project;
|
||||
}
|
||||
if (name === "list_scada_assets") {
|
||||
return this.request("GET", "/api/v1/scada/assets", undefined, context);
|
||||
}
|
||||
return this.request("POST", "/api/v1/scada/readings/query", args, context);
|
||||
}
|
||||
|
||||
private async request(method: string, path: string, body: unknown, context: RuntimeSessionContext) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await this.fetchImpl(new URL(path, config.TJWATER_API_BASE_URL), {
|
||||
method, signal: controller.signal, headers: {
|
||||
Accept: "application/json", "Content-Type": "application/json", "X-Project-Id": context.projectId!,
|
||||
"X-Request-Id": context.traceId,
|
||||
}, ...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
if (!response.ok) throw new GatewayHttpError(response.status);
|
||||
return await response.json();
|
||||
} finally { clearTimeout(timer); }
|
||||
}
|
||||
}
|
||||
|
||||
class GatewayHttpError extends Error { constructor(readonly status: number) { super("upstream request failed"); } }
|
||||
const failure = (code: GatewayErrorCode, message: string, detail?: unknown) => ({ ok: false, error: { code, message, ...(detail ? { detail } : {}) } });
|
||||
@@ -1,24 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const uuid = z.string().uuid();
|
||||
const dateTime = z.string().datetime({ offset: true });
|
||||
const metrics = z.enum([
|
||||
"conductivity_mean", "level_mean", "flow_mean", "temperature_mean",
|
||||
]);
|
||||
|
||||
export const domainToolSchemas = {
|
||||
get_project_context: z.object({}),
|
||||
list_scada_assets: z.object({}),
|
||||
query_scada_readings: z.object({
|
||||
asset_ids: z.array(uuid).min(1).max(100),
|
||||
observed_from: dateTime,
|
||||
observed_to: dateTime,
|
||||
granularity: z.enum(["5m", "1h", "1d"]).default("5m"),
|
||||
metrics: z.array(metrics).min(1).max(4),
|
||||
}).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), {
|
||||
message: "observed_from must precede observed_to",
|
||||
}),
|
||||
} as const;
|
||||
|
||||
export type DomainToolName = keyof typeof domainToolSchemas;
|
||||
export const isDomainToolName = (value: string): value is DomainToolName => value in domainToolSchemas;
|
||||
@@ -7,20 +7,4 @@ type ToolCallInput = {
|
||||
};
|
||||
|
||||
export const toUiEnvelopeFromToolCall = ({
|
||||
tool,
|
||||
params,
|
||||
reason,
|
||||
}: ToolCallInput): UIEnvelope | null => {
|
||||
if (tool === "zoom_to_map") {
|
||||
return {
|
||||
kind: "map_action",
|
||||
schemaVersion: "agent-ui@1",
|
||||
action: tool,
|
||||
surface: "map_overlay",
|
||||
params,
|
||||
fallbackText: reason || "已生成地图操作。",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}: ToolCallInput): UIEnvelope | null => null;
|
||||
|
||||
@@ -4,14 +4,7 @@ export const chartGrammar = "echarts-safe-subset" as const;
|
||||
|
||||
export const componentRegistry: ComponentManifest[] = [];
|
||||
|
||||
export const actionRegistry: ActionManifest[] = [
|
||||
{
|
||||
id: "zoom_to_map",
|
||||
version: "1.0.0",
|
||||
description: "Zoom the map to a coordinate.",
|
||||
supportedSurfaces: ["map_overlay"],
|
||||
},
|
||||
];
|
||||
export const actionRegistry: ActionManifest[] = [];
|
||||
|
||||
export const uiRegistryResponse = {
|
||||
schema_version: "agent-ui-registry@1",
|
||||
|
||||
Reference in New Issue
Block a user