refactor(agent): 移除旧工具桥
This commit is contained in:
@@ -11,10 +11,6 @@ export default tool({
|
|||||||
.optional()
|
.optional()
|
||||||
.describe("Preferred SCADA device ids."),
|
.describe("Preferred SCADA device ids."),
|
||||||
device_id: tool.schema.string().optional().describe("Single SCADA device id."),
|
device_id: tool.schema.string().optional().describe("Single SCADA device id."),
|
||||||
feature_infos: tool.schema
|
|
||||||
.array(tool.schema.tuple([tool.schema.string(), tool.schema.string()]))
|
|
||||||
.optional()
|
|
||||||
.describe("Legacy [id, type] pairs."),
|
|
||||||
start_time: tool.schema.string().optional().describe("Optional ISO8601 start time."),
|
start_time: tool.schema.string().optional().describe("Optional ISO8601 start time."),
|
||||||
end_time: tool.schema.string().optional().describe("Optional ISO8601 end time."),
|
end_time: tool.schema.string().optional().describe("Optional ISO8601 end time."),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -85,17 +85,25 @@ src/
|
|||||||
```text
|
```text
|
||||||
.opencode/tools/
|
.opencode/tools/
|
||||||
tjwater_cli.ts
|
tjwater_cli.ts
|
||||||
|
store_render_ref.ts
|
||||||
locate_features.ts
|
locate_features.ts
|
||||||
view_history.ts
|
view_history.ts
|
||||||
view_scada.ts
|
view_scada.ts
|
||||||
show_chart.ts
|
show_chart.ts
|
||||||
|
render_junctions.ts
|
||||||
|
apply_layer_style.ts
|
||||||
|
memory_manager.ts
|
||||||
|
session_search.ts
|
||||||
|
skill_manager.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
这些是 opencode 可以调用的自定义工具。
|
这些是 opencode 可以调用的自定义工具。
|
||||||
|
|
||||||
`tjwater_cli.ts` 不直接保存用户 token。它会回调 `TJWaterAgent` 的内部接口,由上级服务层根据当前 session 补上用户 token、项目 ID 和 trace ID,再调用 `tjwater-cli` 二进制执行后端命令。
|
`tjwater_cli.ts` 不直接保存用户 token。它会回调 `TJWaterAgent` 的内部接口,由上级服务层根据当前 session 补上用户 token、项目 ID 和 trace ID,再调用 `tjwater-cli` 二进制执行后端命令。
|
||||||
|
|
||||||
前端类工具如 `locate_features`、`view_history`、`view_scada`、`show_chart` 主要用于触发 UI 动作或可视化,不应被当作数据查询工具。
|
`store_render_ref.ts` 用于把大型 junction 渲染 payload 存成 `render_ref`,再由 `render_junctions.ts` 交给前端回读并渲染。
|
||||||
|
|
||||||
|
前端类工具如 `locate_features`、`view_history`、`view_scada`、`show_chart`、`render_junctions`、`apply_layer_style` 主要用于触发 UI 动作或可视化,不应被当作数据查询工具。
|
||||||
|
|
||||||
### skills
|
### skills
|
||||||
|
|
||||||
|
|||||||
@@ -105,12 +105,6 @@ const envSchema = z
|
|||||||
.int()
|
.int()
|
||||||
.positive()
|
.positive()
|
||||||
.default(3600000),
|
.default(3600000),
|
||||||
// fetch_result_ref 默认最多返回的顶层项/字段数量。
|
|
||||||
RESULT_REF_MAX_RETRIEVAL_ITEMS: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.positive()
|
|
||||||
.default(50),
|
|
||||||
})
|
})
|
||||||
.superRefine((env, ctx) => {
|
.superRefine((env, ctx) => {
|
||||||
if (env.OPENCODE_MODE === "client" && !env.OPENCODE_CLIENT_BASE_URL) {
|
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 { atomicWriteJson, readJsonFile } from "../utils/fileStore.js";
|
||||||
import {
|
import {
|
||||||
type ResultReferenceKind,
|
type ResultReferenceKind,
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
|
|
||||||
type ResolveOptions = {
|
type ResolveOptions = {
|
||||||
expectedKind?: ResultReferenceKind;
|
expectedKind?: ResultReferenceKind;
|
||||||
maxItems?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type RegisterResultReferenceInput = {
|
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(
|
async getFullAuthorized(
|
||||||
resultRef: string,
|
resultRef: string,
|
||||||
context: RetrievalContext,
|
context: RetrievalContext,
|
||||||
@@ -250,16 +230,6 @@ const normalizeStringRecord = (value: Record<string, unknown>) =>
|
|||||||
.filter(([, entry]) => entry.length > 0),
|
.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> =>
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
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)?$/;
|
const RESULT_REF_FILE_PATTERN = /^(res-[a-f0-9-]{8,64})(?:\.json)?$/;
|
||||||
|
|
||||||
export const RESULT_REFERENCE_KIND = {
|
export const RESULT_REFERENCE_KIND = {
|
||||||
dynamicHttpResult: "dynamic-http-result",
|
|
||||||
renderJunctionsPayload: "render-junctions-payload",
|
renderJunctionsPayload: "render-junctions-payload",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const RESULT_REFERENCE_SOURCE = {
|
export const RESULT_REFERENCE_SOURCE = {
|
||||||
dynamicHttp: "dynamic_http",
|
|
||||||
agentGenerated: "agent_generated",
|
agentGenerated: "agent_generated",
|
||||||
legacy: "legacy",
|
legacy: "legacy",
|
||||||
migration: "migration",
|
migration: "migration",
|
||||||
@@ -280,9 +278,6 @@ export const normalizeResultReferenceRecord = (
|
|||||||
const normalizeResultReferenceKind = (
|
const normalizeResultReferenceKind = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): ResultReferenceKind | null => {
|
): ResultReferenceKind | null => {
|
||||||
if (value === undefined) {
|
|
||||||
return RESULT_REFERENCE_KIND.dynamicHttpResult;
|
|
||||||
}
|
|
||||||
return Object.values(RESULT_REFERENCE_KIND).includes(
|
return Object.values(RESULT_REFERENCE_KIND).includes(
|
||||||
value as ResultReferenceKind,
|
value as ResultReferenceKind,
|
||||||
)
|
)
|
||||||
@@ -293,9 +288,6 @@ const normalizeResultReferenceKind = (
|
|||||||
const normalizeResultReferenceSource = (
|
const normalizeResultReferenceSource = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): ResultReferenceSource | null => {
|
): ResultReferenceSource | null => {
|
||||||
if (value === undefined) {
|
|
||||||
return RESULT_REFERENCE_SOURCE.legacy;
|
|
||||||
}
|
|
||||||
return Object.values(RESULT_REFERENCE_SOURCE).includes(
|
return Object.values(RESULT_REFERENCE_SOURCE).includes(
|
||||||
value as ResultReferenceSource,
|
value as ResultReferenceSource,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ type ProgressPayload = {
|
|||||||
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
const toolLabels: Record<string, string> = {
|
const toolLabels: Record<string, string> = {
|
||||||
dynamic_http_call: "后端数据查询",
|
|
||||||
fetch_result_ref: "结果引用回读",
|
|
||||||
memory_manager: "记忆写入",
|
memory_manager: "记忆写入",
|
||||||
session_search: "历史会话检索",
|
session_search: "历史会话检索",
|
||||||
skill_manager: "流程沉淀",
|
skill_manager: "流程沉淀",
|
||||||
@@ -843,4 +841,4 @@ export const streamPromptResponse = async ({
|
|||||||
totalDurationMs: Math.max(0, Date.now() - requestStartedAt),
|
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 { buildChatRouter } from "./routes/chat.js";
|
||||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||||
import { SessionRuntimeContextStore } from "./sessions/runtimeContextStore.js";
|
import { SessionRuntimeContextStore } from "./sessions/runtimeContextStore.js";
|
||||||
import { DynamicHttpExecutor } from "./tools/dynamicHttpExecutor.js";
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const sessionBridge = new ChatSessionBridge(opencodeRuntime);
|
const sessionBridge = new ChatSessionBridge(opencodeRuntime);
|
||||||
@@ -32,10 +31,9 @@ const learningOrchestrator = new LearningOrchestrator(
|
|||||||
);
|
);
|
||||||
const resultReferenceStore = new ResultReferenceStore();
|
const resultReferenceStore = new ResultReferenceStore();
|
||||||
const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore);
|
const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore);
|
||||||
const dynamicHttpExecutor = new DynamicHttpExecutor(resultReferenceStore);
|
|
||||||
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
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;
|
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||||
|
|
||||||
app.use(cors());
|
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) => {
|
app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
|
||||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||||
res.status(403).json({ message: "forbidden" });
|
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) => {
|
app.post("/internal/tools/store-render-ref", async (req, res) => {
|
||||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||||
res.status(403).json({ message: "forbidden" });
|
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));
|
|
||||||
+20
-65
@@ -26,83 +26,50 @@ describe("ResultReferenceResolver", () => {
|
|||||||
await rm(tempDir, { force: true, recursive: true });
|
await rm(tempDir, { force: true, recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores metadata for new referenced results and resolves them", async () => {
|
it("stores metadata for render refs and resolves them", async () => {
|
||||||
const record = await resolver.register({
|
const record = await resolver.register({
|
||||||
actorKey: "actor-1",
|
actorKey: "actor-1",
|
||||||
clientSessionId: "client-1",
|
clientSessionId: "client-1",
|
||||||
data: [{ id: "J1" }, { id: "J2" }],
|
data: {
|
||||||
kind: RESULT_REFERENCE_KIND.dynamicHttpResult,
|
node_area_map: {
|
||||||
|
J1: "DMA-1",
|
||||||
|
J2: "DMA-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
projectKey: "project-key-1",
|
projectKey: "project-key-1",
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
source: RESULT_REFERENCE_SOURCE.dynamicHttp,
|
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||||
traceId: "trace-1",
|
traceId: "trace-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(record.kind).toBe(RESULT_REFERENCE_KIND.dynamicHttpResult);
|
expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
|
||||||
expect(record.schemaVersion).toBe(1);
|
expect(record.schemaVersion).toBe(1);
|
||||||
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.dynamicHttp);
|
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated);
|
||||||
|
|
||||||
const result = await resolver.getAuthorized(
|
const result = await resolver.getFullAuthorized(
|
||||||
record.resultRef,
|
record.resultRef,
|
||||||
{
|
{
|
||||||
actorKey: "actor-1",
|
actorKey: "actor-1",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
maxItems: 1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.kind).toBe(RESULT_REFERENCE_KIND.dynamicHttpResult);
|
expect(result?.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
|
||||||
expect(result?.schema_version).toBe(1);
|
expect(result?.schema_version).toBe(1);
|
||||||
expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.dynamicHttp);
|
expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated);
|
||||||
expect(result?.data).toEqual([{ id: "J1" }]);
|
expect(result?.data).toEqual({
|
||||||
});
|
node_area_map: {
|
||||||
|
J1: "DMA-1",
|
||||||
it("keeps legacy result refs readable while defaulting metadata", async () => {
|
J2: "DMA-2",
|
||||||
const legacyRef = "res-aaaaaaaaaaaaaaaa";
|
},
|
||||||
await writeFile(
|
|
||||||
join(tempDir, `${legacyRef}.json`),
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
resultRef: legacyRef,
|
|
||||||
actorKey: "actor-legacy",
|
|
||||||
clientSessionId: "client-legacy",
|
|
||||||
createdAt: "2026-05-21T00:00:00.000Z",
|
|
||||||
data: { nodes: ["J1"] },
|
|
||||||
preview: {
|
|
||||||
count: 1,
|
|
||||||
fields: ["nodes"],
|
|
||||||
sample: { nodes: ["J1"] },
|
|
||||||
summary: "object<1 fields>",
|
|
||||||
},
|
|
||||||
projectId: "project-legacy",
|
|
||||||
projectKey: "project-key-legacy",
|
|
||||||
sessionId: "session-legacy",
|
|
||||||
sizeBytes: 16,
|
|
||||||
traceId: "trace-legacy",
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
|
|
||||||
const record = await store.getAuthorizedRecord(legacyRef, {
|
|
||||||
actorKey: "actor-legacy",
|
|
||||||
projectId: "project-legacy",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(record).not.toBeNull();
|
|
||||||
expect(record?.kind).toBe(RESULT_REFERENCE_KIND.dynamicHttpResult);
|
|
||||||
expect(record?.schemaVersion).toBe(1);
|
|
||||||
expect(record?.source).toBe(RESULT_REFERENCE_SOURCE.legacy);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects malformed refs, mismatched kinds, and auth mismatches", async () => {
|
it("rejects malformed refs and auth mismatches", async () => {
|
||||||
const malformedRef = "res-bbbbbbbbbbbbbbbb";
|
const malformedRef = "res-bbbbbbbbbbbbbbbb";
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(tempDir, `${malformedRef}.json`),
|
join(tempDir, `${malformedRef}.json`),
|
||||||
@@ -152,18 +119,6 @@ describe("ResultReferenceResolver", () => {
|
|||||||
traceId: "trace-2",
|
traceId: "trace-2",
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrongKind = await resolver.getFullAuthorized(
|
|
||||||
renderRecord.resultRef,
|
|
||||||
{
|
|
||||||
actorKey: "actor-2",
|
|
||||||
projectId: "project-2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectedKind: RESULT_REFERENCE_KIND.dynamicHttpResult,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(wrongKind).toBeNull();
|
|
||||||
|
|
||||||
const wrongActor = await resolver.getFullAuthorized(renderRecord.resultRef, {
|
const wrongActor = await resolver.getFullAuthorized(renderRecord.resultRef, {
|
||||||
actorKey: "actor-other",
|
actorKey: "actor-other",
|
||||||
projectId: "project-2",
|
projectId: "project-2",
|
||||||
|
|||||||
Reference in New Issue
Block a user