refactor(agent): 移除旧工具桥
This commit is contained in:
@@ -105,12 +105,6 @@ const envSchema = z
|
||||
.int()
|
||||
.positive()
|
||||
.default(3600000),
|
||||
// fetch_result_ref 默认最多返回的顶层项/字段数量。
|
||||
RESULT_REF_MAX_RETRIEVAL_ITEMS: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(50),
|
||||
})
|
||||
.superRefine((env, ctx) => {
|
||||
if (env.OPENCODE_MODE === "client" && !env.OPENCODE_CLIENT_BASE_URL) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { config } from "../config.js";
|
||||
import { atomicWriteJson, readJsonFile } from "../utils/fileStore.js";
|
||||
import {
|
||||
type ResultReferenceKind,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
|
||||
type ResolveOptions = {
|
||||
expectedKind?: ResultReferenceKind;
|
||||
maxItems?: number;
|
||||
};
|
||||
|
||||
type RegisterResultReferenceInput = {
|
||||
@@ -89,24 +87,6 @@ export class ResultReferenceResolver {
|
||||
});
|
||||
}
|
||||
|
||||
async getAuthorized(resultRef: string, context: RetrievalContext, options: ResolveOptions = {}) {
|
||||
const record = await this.getResolvedRecord(resultRef, context, options);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
result_ref: record.resultRef,
|
||||
result_size_bytes: record.sizeBytes,
|
||||
stored_at: record.createdAt,
|
||||
data: projectData(record.data, options.maxItems ?? config.RESULT_REF_MAX_RETRIEVAL_ITEMS),
|
||||
preview: record.preview,
|
||||
kind: record.kind,
|
||||
schema_version: record.schemaVersion,
|
||||
source: record.source,
|
||||
};
|
||||
}
|
||||
|
||||
async getFullAuthorized(
|
||||
resultRef: string,
|
||||
context: RetrievalContext,
|
||||
@@ -250,16 +230,6 @@ const normalizeStringRecord = (value: Record<string, unknown>) =>
|
||||
.filter(([, entry]) => entry.length > 0),
|
||||
);
|
||||
|
||||
const projectData = (data: unknown, maxItems: number) => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.slice(0, maxItems);
|
||||
}
|
||||
if (isRecord(data)) {
|
||||
return Object.fromEntries(Object.entries(data).slice(0, maxItems));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ export const RESULT_REF_PATTERN = /^res-[a-f0-9-]{8,64}$/;
|
||||
const RESULT_REF_FILE_PATTERN = /^(res-[a-f0-9-]{8,64})(?:\.json)?$/;
|
||||
|
||||
export const RESULT_REFERENCE_KIND = {
|
||||
dynamicHttpResult: "dynamic-http-result",
|
||||
renderJunctionsPayload: "render-junctions-payload",
|
||||
} as const;
|
||||
|
||||
export const RESULT_REFERENCE_SOURCE = {
|
||||
dynamicHttp: "dynamic_http",
|
||||
agentGenerated: "agent_generated",
|
||||
legacy: "legacy",
|
||||
migration: "migration",
|
||||
@@ -280,9 +278,6 @@ export const normalizeResultReferenceRecord = (
|
||||
const normalizeResultReferenceKind = (
|
||||
value: unknown,
|
||||
): ResultReferenceKind | null => {
|
||||
if (value === undefined) {
|
||||
return RESULT_REFERENCE_KIND.dynamicHttpResult;
|
||||
}
|
||||
return Object.values(RESULT_REFERENCE_KIND).includes(
|
||||
value as ResultReferenceKind,
|
||||
)
|
||||
@@ -293,9 +288,6 @@ const normalizeResultReferenceKind = (
|
||||
const normalizeResultReferenceSource = (
|
||||
value: unknown,
|
||||
): ResultReferenceSource | null => {
|
||||
if (value === undefined) {
|
||||
return RESULT_REFERENCE_SOURCE.legacy;
|
||||
}
|
||||
return Object.values(RESULT_REFERENCE_SOURCE).includes(
|
||||
value as ResultReferenceSource,
|
||||
)
|
||||
|
||||
@@ -36,8 +36,6 @@ type ProgressPayload = {
|
||||
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
||||
|
||||
const toolLabels: Record<string, string> = {
|
||||
dynamic_http_call: "后端数据查询",
|
||||
fetch_result_ref: "结果引用回读",
|
||||
memory_manager: "记忆写入",
|
||||
session_search: "历史会话检索",
|
||||
skill_manager: "流程沉淀",
|
||||
@@ -843,4 +841,4 @@ export const streamPromptResponse = async ({
|
||||
totalDurationMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
+1
-92
@@ -16,7 +16,6 @@ import { ResultReferenceStore } from "./results/store.js";
|
||||
import { buildChatRouter } from "./routes/chat.js";
|
||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||
import { SessionRuntimeContextStore } from "./sessions/runtimeContextStore.js";
|
||||
import { DynamicHttpExecutor } from "./tools/dynamicHttpExecutor.js";
|
||||
|
||||
const app = express();
|
||||
const sessionBridge = new ChatSessionBridge(opencodeRuntime);
|
||||
@@ -32,10 +31,9 @@ const learningOrchestrator = new LearningOrchestrator(
|
||||
);
|
||||
const resultReferenceStore = new ResultReferenceStore();
|
||||
const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore);
|
||||
const dynamicHttpExecutor = new DynamicHttpExecutor(resultReferenceStore);
|
||||
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
||||
|
||||
// 这个 token 只用于仍需服务端上下文的工具桥(dynamic_http_call / fetch_result_ref / store_render_ref)。
|
||||
// 这个 token 只用于仍需服务端上下文的工具桥(store_render_ref)。
|
||||
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||
|
||||
app.use(cors());
|
||||
@@ -60,52 +58,6 @@ app.get("/health", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/internal/tools/dynamic-http-call", 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 ? await sessionRuntimeContextStore.read(sessionId) : null;
|
||||
if (!context) {
|
||||
res.status(404).json({
|
||||
message: "session context not found",
|
||||
detail: sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// opencode 工具运行在 .opencode 侧,这里负责把工具调用重新绑定到当前用户/项目上下文。
|
||||
const result = await dynamicHttpExecutor.execute(
|
||||
{
|
||||
reason: req.body?.reason,
|
||||
path: req.body?.path,
|
||||
method: req.body?.method,
|
||||
arguments: req.body?.arguments,
|
||||
},
|
||||
{
|
||||
accessToken: context.accessToken,
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId,
|
||||
projectId: context.projectId,
|
||||
projectKey: context.projectKey,
|
||||
sessionId: context.clientSessionId,
|
||||
traceId: context.traceId,
|
||||
},
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
res.status(400).json({
|
||||
message: "dynamic http execution failed",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
@@ -209,49 +161,6 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/internal/tools/fetch-result-ref", 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 resultRef = typeof req.body?.result_ref === "string" ? req.body.result_ref : "";
|
||||
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
|
||||
if (!context) {
|
||||
res.status(404).json({
|
||||
message: "session context not found",
|
||||
detail: sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!resultRef) {
|
||||
res.status(400).json({ message: "result_ref is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resultReferenceResolver.getAuthorized(
|
||||
resultRef,
|
||||
{
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId,
|
||||
projectId: context.projectId,
|
||||
},
|
||||
{
|
||||
maxItems:
|
||||
typeof req.body?.max_items === "number" ? req.body.max_items : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ message: "result_ref not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.post("/internal/tools/store-render-ref", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { config } from "../config.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { RESULT_REFERENCE_KIND, RESULT_REFERENCE_SOURCE } from "../results/store.js";
|
||||
import { ResultReferenceStore } from "../results/store.js";
|
||||
|
||||
export type DynamicHttpInput = {
|
||||
reason?: string;
|
||||
path: string;
|
||||
method?: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SessionToolContext = {
|
||||
accessToken?: string;
|
||||
actorKey: string;
|
||||
clientSessionId: string;
|
||||
projectKey: string;
|
||||
sessionId: string;
|
||||
projectId?: string;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
const allowedMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
||||
|
||||
export class DynamicHttpExecutor {
|
||||
constructor(private readonly resultStore: ResultReferenceStore) {}
|
||||
|
||||
async execute(input: DynamicHttpInput, context: SessionToolContext) {
|
||||
const method = (input.method ?? "GET").trim().toUpperCase();
|
||||
if (!allowedMethods.has(method)) {
|
||||
throw new Error(`unsupported method: ${method}`);
|
||||
}
|
||||
|
||||
const path = input.path.trim();
|
||||
if (!path.startsWith("/")) {
|
||||
throw new Error("path must start with '/'");
|
||||
}
|
||||
|
||||
const query = buildQuery(input.arguments ?? {});
|
||||
const url = new URL(path, config.TJWATER_API_BASE_URL);
|
||||
for (const [key, value] of query) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
|
||||
// 这里复用 chat session 绑定的用户上下文,保持后端鉴权与项目隔离语义不变。
|
||||
const headers = new Headers({
|
||||
Accept: "application/json",
|
||||
"x-trace-id": context.traceId,
|
||||
});
|
||||
if (context.accessToken) {
|
||||
headers.set("Authorization", `Bearer ${context.accessToken}`);
|
||||
}
|
||||
if (context.projectId) {
|
||||
headers.set("x-project-id", context.projectId);
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
signal: AbortSignal.timeout(config.TJWATER_API_TIMEOUT_MS),
|
||||
});
|
||||
const durationMs = Date.now() - startedAt;
|
||||
logger.info(
|
||||
{
|
||||
method,
|
||||
path,
|
||||
reason: typeof input.reason === "string" ? input.reason : undefined,
|
||||
statusCode: response.status,
|
||||
durationMs,
|
||||
traceId: context.traceId,
|
||||
projectId: context.projectId,
|
||||
},
|
||||
"dynamic_http_call completed",
|
||||
);
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
const rawText = await response.text();
|
||||
const data =
|
||||
contentType.includes("application/json") && rawText
|
||||
? JSON.parse(rawText)
|
||||
: rawText;
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
trace_id: context.traceId,
|
||||
upstream: {
|
||||
method,
|
||||
path,
|
||||
status_code: response.status,
|
||||
},
|
||||
error: {
|
||||
message: "upstream API returned error",
|
||||
detail: data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
trace_id: context.traceId,
|
||||
upstream: {
|
||||
method,
|
||||
path,
|
||||
status_code: response.status,
|
||||
},
|
||||
...(await normalizeSuccessResult(data, context, this.resultStore)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const buildQuery = (argumentsObject: Record<string, unknown>) => {
|
||||
const pairs: Array<[string, string]> = [];
|
||||
for (const [key, value] of Object.entries(argumentsObject)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
continue;
|
||||
}
|
||||
pairs.push([key, value.map(String).join(",")]);
|
||||
continue;
|
||||
}
|
||||
pairs.push([key, String(value)]);
|
||||
}
|
||||
return pairs;
|
||||
};
|
||||
|
||||
const normalizeSuccessResult = async (
|
||||
data: unknown,
|
||||
context: SessionToolContext,
|
||||
resultStore: ResultReferenceStore,
|
||||
) => {
|
||||
const sizeBytes = estimateBytes(data);
|
||||
if (sizeBytes <= config.MAX_INLINE_RESULT_BYTES) {
|
||||
return {
|
||||
result_mode: "inline",
|
||||
result_size_bytes: sizeBytes,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
// 大结果转成持久化引用,支持 review 和跨重启回读。
|
||||
const record = await resultStore.store({
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId,
|
||||
data,
|
||||
kind: RESULT_REFERENCE_KIND.dynamicHttpResult,
|
||||
projectId: context.projectId,
|
||||
projectKey: context.projectKey,
|
||||
schemaVersion: 1,
|
||||
sessionId: context.sessionId,
|
||||
source: RESULT_REFERENCE_SOURCE.dynamicHttp,
|
||||
traceId: context.traceId,
|
||||
});
|
||||
|
||||
return {
|
||||
result_mode: "referenced",
|
||||
result_size_bytes: sizeBytes,
|
||||
result_ref: record.resultRef,
|
||||
preview: record.preview,
|
||||
};
|
||||
};
|
||||
|
||||
const estimateBytes = (data: unknown) => Buffer.byteLength(JSON.stringify(data));
|
||||
Reference in New Issue
Block a user