fix(agent): stabilize large tool results
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { emitApi } from "../core/http.js";
|
||||
import { parseOptions, requiredString } from "../core/options.js";
|
||||
import { emitApi, requestAllPages } from "../core/http.js";
|
||||
import { optionalNumber, parseOptions, requiredString } from "../core/options.js";
|
||||
import { success } from "../core/output.js";
|
||||
import type { HandlerMap, RuntimeContext } from "../core/types.js";
|
||||
|
||||
function apiGet(ctx: RuntimeContext, argv: string[], summary: string, path: string, key: string): Promise<void> {
|
||||
@@ -7,20 +8,28 @@ function apiGet(ctx: RuntimeContext, argv: string[], summary: string, path: stri
|
||||
return emitApi(ctx, summary, { method: "GET", path, params: { [key]: requiredString(values, key) }, requireProject: true });
|
||||
}
|
||||
|
||||
function apiGetAll(ctx: RuntimeContext, summary: string, path: string): Promise<void> {
|
||||
return emitApi(ctx, summary, { method: "GET", path, requireProject: true });
|
||||
async function apiGetAll(ctx: RuntimeContext, argv: string[], summary: string, path: string): Promise<void> {
|
||||
const { values } = parseOptions(argv, { limit: "integer", "page-size": "integer" });
|
||||
const requestedPageSize = optionalNumber(values, "page-size") ?? optionalNumber(values, "limit") ?? 1000;
|
||||
const pageSize = Math.min(1000, Math.max(1, requestedPageSize));
|
||||
const [data, durationMs] = await requestAllPages(
|
||||
ctx,
|
||||
{ method: "GET", path, requireProject: true },
|
||||
pageSize,
|
||||
);
|
||||
success(summary, data, ctx, durationMs);
|
||||
}
|
||||
|
||||
export const networkHandlers: HandlerMap = {
|
||||
"network get-junction-properties": (ctx, argv) => apiGet(ctx, argv, "读取节点属性成功", "/junctions/properties", "junction"),
|
||||
"network get-pipe-properties": (ctx, argv) => apiGet(ctx, argv, "读取管道属性成功", "/pipes/properties", "pipe"),
|
||||
"network get-all-pipes-properties": (ctx) => apiGetAll(ctx, "读取全部管道属性成功", "/pipes"),
|
||||
"network get-all-pipes-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部管道属性成功", "/pipes"),
|
||||
"network get-reservoir-properties": (ctx, argv) => apiGet(ctx, argv, "读取水库属性成功", "/reservoirs/properties", "reservoir"),
|
||||
"network get-all-reservoirs-properties": (ctx) => apiGetAll(ctx, "读取全部水库属性成功", "/reservoirs"),
|
||||
"network get-all-reservoirs-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部水库属性成功", "/reservoirs"),
|
||||
"network get-tank-properties": (ctx, argv) => apiGet(ctx, argv, "读取水箱属性成功", "/tanks/properties", "tank"),
|
||||
"network get-all-tanks-properties": (ctx) => apiGetAll(ctx, "读取全部水箱属性成功", "/tanks"),
|
||||
"network get-all-tanks-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部水箱属性成功", "/tanks"),
|
||||
"network get-pump-properties": (ctx, argv) => apiGet(ctx, argv, "读取水泵属性成功", "/pumps/properties", "pump"),
|
||||
"network get-all-pumps-properties": (ctx) => apiGetAll(ctx, "读取全部水泵属性成功", "/pumps"),
|
||||
"network get-all-pumps-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部水泵属性成功", "/pumps"),
|
||||
"network get-valve-properties": (ctx, argv) => apiGet(ctx, argv, "读取阀门属性成功", "/valves/properties", "valve"),
|
||||
"network get-all-valves-properties": (ctx) => apiGetAll(ctx, "读取全部阀门属性成功", "/valves"),
|
||||
"network get-all-valves-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部阀门属性成功", "/valves"),
|
||||
};
|
||||
|
||||
@@ -76,6 +76,100 @@ export async function requestJson(ctx: RuntimeContext, request: RequestOptions):
|
||||
return [payload, durationMs];
|
||||
}
|
||||
|
||||
export async function requestAllPages(
|
||||
ctx: RuntimeContext,
|
||||
request: RequestOptions,
|
||||
pageSize: number,
|
||||
): Promise<[unknown[], number]> {
|
||||
const items: unknown[] = [];
|
||||
let durationMs = 0;
|
||||
let offset = 0;
|
||||
let expectedTotal: number | null = null;
|
||||
|
||||
while (expectedTotal === null || offset < expectedTotal) {
|
||||
const [payload, pageDurationMs] = await requestJson(ctx, {
|
||||
...request,
|
||||
params: {
|
||||
...request.params,
|
||||
limit: pageSize,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
durationMs += pageDurationMs;
|
||||
const page = normalizePage(payload);
|
||||
if (!page) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"INVALID_PAGINATION_RESPONSE",
|
||||
"backend collection response must contain items, total, limit, and offset",
|
||||
7,
|
||||
false,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
if (expectedTotal === null) {
|
||||
expectedTotal = page.total;
|
||||
} else if (page.total !== expectedTotal) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"PAGINATION_TOTAL_CHANGED",
|
||||
`backend collection total changed from ${expectedTotal} to ${page.total}`,
|
||||
7,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (page.offset !== offset) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"PAGINATION_OFFSET_MISMATCH",
|
||||
`backend collection returned offset ${page.offset}, expected ${offset}`,
|
||||
7,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (page.items.length === 0 && offset < expectedTotal) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"PAGINATION_STALLED",
|
||||
`backend collection returned an empty page at offset ${offset} before total ${expectedTotal}`,
|
||||
7,
|
||||
true,
|
||||
);
|
||||
}
|
||||
items.push(...page.items);
|
||||
offset += page.items.length;
|
||||
}
|
||||
|
||||
return [items.slice(0, expectedTotal ?? 0), durationMs];
|
||||
}
|
||||
|
||||
function normalizePage(
|
||||
payload: unknown,
|
||||
): { items: unknown[]; limit: number; offset: number; total: number } | null {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
||||
const page = payload as Record<string, unknown>;
|
||||
if (
|
||||
!Array.isArray(page.items) ||
|
||||
typeof page.limit !== "number" ||
|
||||
!Number.isInteger(page.limit) ||
|
||||
typeof page.offset !== "number" ||
|
||||
!Number.isInteger(page.offset) ||
|
||||
typeof page.total !== "number" ||
|
||||
!Number.isInteger(page.total) ||
|
||||
page.limit <= 0 ||
|
||||
page.offset < 0 ||
|
||||
page.total < 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
items: page.items,
|
||||
limit: page.limit,
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
};
|
||||
}
|
||||
|
||||
function mapStatus(status: number): number {
|
||||
if (status === 400 || status === 422) return 2;
|
||||
if (status === 401) return 3;
|
||||
|
||||
@@ -32,15 +32,15 @@ type CommandSpec = readonly [path: string, summary: string, options: readonly st
|
||||
const commandSpecs: readonly CommandSpec[] = [
|
||||
["network get-junction-properties", "读取节点属性", ["--junction <JUNCTION>"], ["tjwater-cli network get-junction-properties --junction J1"]],
|
||||
["network get-pipe-properties", "读取管道属性", ["--pipe <PIPE>"], ["tjwater-cli network get-pipe-properties --pipe P1"]],
|
||||
["network get-all-pipes-properties", "读取全部管道属性", [], ["tjwater-cli network get-all-pipes-properties"]],
|
||||
["network get-all-pipes-properties", "读取全部管道属性", ["[--page-size <PAGE_SIZE>]"], ["tjwater-cli network get-all-pipes-properties"]],
|
||||
["network get-reservoir-properties", "读取水库属性", ["--reservoir <RESERVOIR>"], ["tjwater-cli network get-reservoir-properties --reservoir R1"]],
|
||||
["network get-all-reservoirs-properties", "读取全部水库属性", [], ["tjwater-cli network get-all-reservoirs-properties"]],
|
||||
["network get-all-reservoirs-properties", "读取全部水库属性", ["[--page-size <PAGE_SIZE>]"], ["tjwater-cli network get-all-reservoirs-properties"]],
|
||||
["network get-tank-properties", "读取水箱属性", ["--tank <TANK>"], ["tjwater-cli network get-tank-properties --tank T1"]],
|
||||
["network get-all-tanks-properties", "读取全部水箱属性", [], ["tjwater-cli network get-all-tanks-properties"]],
|
||||
["network get-all-tanks-properties", "读取全部水箱属性", ["[--page-size <PAGE_SIZE>]"], ["tjwater-cli network get-all-tanks-properties"]],
|
||||
["network get-pump-properties", "读取水泵属性", ["--pump <PUMP>"], ["tjwater-cli network get-pump-properties --pump PU1"]],
|
||||
["network get-all-pumps-properties", "读取全部水泵属性", [], ["tjwater-cli network get-all-pumps-properties"]],
|
||||
["network get-all-pumps-properties", "读取全部水泵属性", ["[--page-size <PAGE_SIZE>]"], ["tjwater-cli network get-all-pumps-properties"]],
|
||||
["network get-valve-properties", "读取阀门属性", ["--valve <VALVE>"], ["tjwater-cli network get-valve-properties --valve V1"]],
|
||||
["network get-all-valves-properties", "读取全部阀门属性", [], ["tjwater-cli network get-all-valves-properties"]],
|
||||
["network get-all-valves-properties", "读取全部阀门属性", ["[--page-size <PAGE_SIZE>]"], ["tjwater-cli network get-all-valves-properties"]],
|
||||
["component option schema", "读取选项 schema", ["--kind <KIND>", "[--pump <PUMP>]"], ["tjwater-cli component option schema --kind time", "tjwater-cli component option schema --kind energy", "tjwater-cli component option schema --kind pump-energy --pump PUMP1", "tjwater-cli component option schema --kind network"]],
|
||||
["component option get", "读取选项属性", ["--kind <KIND>", "[--pump <PUMP>]"], ["tjwater-cli component option get --kind time", "tjwater-cli component option get --kind energy", "tjwater-cli component option get --kind pump-energy --pump PUMP1", "tjwater-cli component option get --kind network"]],
|
||||
["simulation run", "触发指定绝对时间的模拟运行", ["--start-time <START_TIME>", "--duration <DURATION>"], ["tjwater-cli simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30"], ["tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00", "tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"]],
|
||||
|
||||
Reference in New Issue
Block a user