feat: initialize experimental agent repo

This commit is contained in:
2026-06-30 14:03:49 +08:00
commit e83ac54a9e
75 changed files with 11690 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description:
"在前端地图上对节点或管道图层应用样式,或重置为默认样式。样式参数应尽量与前端样式编辑器字段保持一致。",
args: {
reason: tool.schema
.string()
.describe(
"Why this style action is needed for the current user request.",
),
layer_id: tool.schema
.enum(["junctions", "pipes"])
.describe("Target layer id. Must be exactly 'junctions' or 'pipes'."),
reset_to_default: tool.schema
.boolean()
.optional()
.describe("Whether to reset the target layer to its default style."),
style_config: tool.schema
.object({
property: tool.schema
.string()
.optional()
.describe("Data property to render."),
classification_method: tool.schema
.enum(["pretty_breaks", "custom_breaks"])
.optional()
.describe("Classification method."),
segments: tool.schema
.number()
.optional()
.describe("Number of segments."),
min_size: tool.schema
.number()
.optional()
.describe("Minimum point radius."),
max_size: tool.schema
.number()
.optional()
.describe("Maximum point radius."),
min_stroke_width: tool.schema
.number()
.optional()
.describe("Minimum line width."),
max_stroke_width: tool.schema
.number()
.optional()
.describe("Maximum line width."),
fixed_stroke_width: tool.schema
.number()
.optional()
.describe("Fixed line width when width is not data-driven."),
color_type: tool.schema
.enum(["single", "gradient", "rainbow", "custom"])
.optional()
.describe("Color strategy."),
single_palette_index: tool.schema.number().optional(),
gradient_palette_index: tool.schema.number().optional(),
rainbow_palette_index: tool.schema.number().optional(),
show_labels: tool.schema
.boolean()
.optional()
.describe("Whether to show labels."),
show_id: tool.schema
.boolean()
.optional()
.describe("Whether to show ids."),
opacity: tool.schema.number().optional().describe("Opacity in [0, 1]."),
adjust_width_by_property: tool.schema
.boolean()
.optional()
.describe("Whether line width is driven by the rendered property."),
custom_breaks: tool.schema
.array(tool.schema.number())
.optional()
.describe("Custom break values."),
custom_colors: tool.schema
.array(tool.schema.string())
.optional()
.describe("Custom rgba colors."),
})
.optional()
.describe(
"Optional style config overrides. Omit when reset_to_default is true.",
),
},
async execute(args) {
const layerLabel = args.layer_id === "junctions" ? "节点" : "管道";
if (args.reset_to_default) {
return `已将${layerLabel}图层重置为默认样式。`;
}
return `已对${layerLabel}图层应用样式。`;
},
});
+37
View File
@@ -0,0 +1,37 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"调用 TJWater 后端的天地图地理编码服务,将中国境内结构化地址或地点名称转换为经纬度。若需缩放地图,把返回的 location.lon/location.lat 传给 zoom_to_map,并设置 source_crs='EPSG:4326'。",
args: {
reason: tool.schema
.string()
.describe("Why geocoding is required for the current user request."),
keyword: tool.schema
.string()
.describe("Address or place name to geocode, such as 北京市人民政府."),
},
async execute(args, context) {
const response = await fetch(`${internalBaseUrl}/internal/tools/geocode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
session_id: context.sessionID,
keyword: args.keyword,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+22
View File
@@ -0,0 +1,22 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description: "在前端地图上定位并高亮指定的管网要素。",
args: {
reason: tool.schema
.string()
.describe(
"Why this map positioning action is needed for the user request.",
),
ids: tool.schema
.array(tool.schema.string())
.describe("Feature ids to locate."),
feature_type: tool.schema
.enum(["junction", "pipe", "valve", "reservoir", "pump", "tank"])
.describe("Type of feature to locate."),
},
async execute() {
// 前端工具只负责生成 tool part,真正的地图动作由 Agent SSE 适配层转发给浏览器执行。
return "已在地图上定位到指定要素。";
},
});
+155
View File
@@ -0,0 +1,155 @@
import { tool } from "@opencode-ai/plugin";
import { MemoryStore } from "../../src/memory/store.js";
import {
getRuntimeSessionContext,
setRuntimeSessionContext,
} from "../../src/runtime/sessionContext.js";
const memoryStore = new MemoryStore();
const initializePromise = memoryStore.initialize();
export default tool({
description:
"管理长期有效的用户偏好或项目事实。支持 add/list/replace/remove。add 前必须先对同 scope 执行 list 并阅读现有记忆,再决定 add、replace 或 remove;不要跳过读取直接新增。禁止写入 token、password、secret、system prompt 或一次性上下文。scope 仅允许 'user' 或 'workspace'。",
args: {
action: tool.schema
.enum(["add", "list", "replace", "remove"])
.describe("Memory operation to perform."),
reason: tool.schema
.string()
.describe("Why this memory should be persisted for future requests."),
scope: tool.schema
.string()
.describe(
"Required exact keyword. Use only 'user' for user-level durable preferences/constraints, or 'workspace' for project/environment durable facts. Do not use 'project', Chinese labels, or any alias.",
),
content: tool.schema
.string()
.optional()
.describe(
"The durable fact or preference to remember, written as one concise sentence.",
),
target_id: tool.schema
.string()
.optional()
.describe("Stable memory entry id used by replace/remove."),
},
async execute(args, context) {
await initializePromise;
const sessionContext = getRuntimeSessionContext(context.sessionID);
if (!sessionContext) {
throw new Error(`session context not found for ${context.sessionID}`);
}
const scope =
args.scope === "user"
? "user"
: args.scope === "workspace"
? "workspace"
: null;
if (!scope) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: `unsupported scope: ${args.scope}; use exact keyword 'user' or 'workspace'`,
});
}
if (sessionContext.allowLearningWrite === false && args.action !== "list") {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: "memory writes are disabled for this session",
});
}
const scopeKey =
scope === "user" ? sessionContext.actorKey : sessionContext.projectKey;
if (args.action === "list") {
const readScopes = {
...(sessionContext.memoryListReadScopes ?? {}),
[scope]: true,
};
setRuntimeSessionContext({
...sessionContext,
memoryListReadScopes: readScopes,
});
return JSON.stringify({
ok: true,
kind: "memory",
decision: "accepted",
detail: "memory listed",
items: await memoryStore.list(scope, scopeKey),
target: scope,
});
}
if (args.action === "add") {
if (sessionContext.memoryListReadScopes?.[scope] !== true) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: `must list ${scope} memory and review existing entries before add`,
target: scope,
});
}
const result = await memoryStore.upsert(scope, scopeKey, {
content: args.content ?? "",
sessionId: sessionContext.clientSessionId,
source: "tool",
traceId: sessionContext.traceId,
});
if (!result.entry) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: "content rejected by persistence policy",
});
}
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "deduped",
detail: result.detail,
entry: result.entry,
target: scope,
});
}
if (args.action === "replace") {
const result = await memoryStore.replace(
scope,
scopeKey,
args.target_id ?? "",
{
content: args.content ?? "",
sessionId: sessionContext.clientSessionId,
source: "tool",
traceId: sessionContext.traceId,
},
);
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: scope,
});
}
const result = await memoryStore.remove(
scope,
scopeKey,
args.target_id ?? "",
);
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: scope,
});
},
});
+22
View File
@@ -0,0 +1,22 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description:
"在前端地图上对 junctions 图层应用分区渲染。使用前必须完成两步:① 准备数据结构(JSON 文件,结构为 { node_area_map: Record<string, string>, area_ids?: string[], area_colors?: Record<string, string> },其中 node_area_map 的 key 是 junction/node idvalue 是 area id);② 调用 store_render_ref 将 JSON 文件存储到受控路径,获取 render_ref(格式为 res-...);③ 将 render_ref 传入本工具完成前端渲染。注意:不要先把 ref 内容完整读出再传给前端,也不要直接传本地文件路径。",
args: {
reason: tool.schema
.string()
.describe(
"Why this junction rendering action is needed for the user request.",
),
render_ref: tool.schema
.string()
.describe(
"上一步通过 store_render_ref 获得的渲染引用 ID(res-...)。前端会按该引用拉取完整 payload 并渲染。不可直接传入本地文件路径或完整 JSON 数据。",
),
},
async execute() {
// 工具参数里只需要 render_ref;浏览器端会再用该引用回读完整 payload.data 并完成渲染。
return "已在地图上应用节点分区渲染。";
},
});
+46
View File
@@ -0,0 +1,46 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"搜索当前用户和项目范围内的历史会话 transcript。适合回忆过去讨论过的案例、约束和结论,避免把一次性案例写入 memory。",
args: {
reason: tool.schema
.string()
.describe("Why prior session history is needed for the current request."),
query: tool.schema
.string()
.describe("What to search for in prior session history."),
max_results: tool.schema
.number()
.int()
.positive()
.optional()
.describe("Optional maximum number of hits to return."),
},
async execute(args, context) {
const response = await fetch(
`${internalBaseUrl}/internal/tools/session-search`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
max_results: args.max_results,
query: args.query,
session_id: context.sessionID,
}),
},
);
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+42
View File
@@ -0,0 +1,42 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description:
"在前端对话界面中渲染图表。折线图/柱状图必须使用 x_data 作为横轴标签,series[].data 作为同长度的一维数值数组,不要把折线数据写成 ECharts 的 [x, y] 二维点数组。",
args: {
reason: tool.schema
.string()
.describe("Why this chart should be rendered for the user request."),
title: tool.schema.string().optional().describe("Chart title."),
chart_type: tool.schema
.enum(["line", "bar", "pie"])
.optional()
.describe("Chart type."),
x_data: tool.schema
.array(tool.schema.string())
.describe("X-axis labels. For line charts, put time/category labels here."),
series: tool.schema
.array(
tool.schema.object({
name: tool.schema.string(),
data: tool.schema
.array(tool.schema.number())
.describe("Y values only. Must align by index with x_data."),
type: tool.schema.enum(["line", "bar"]).optional(),
}),
)
.describe("Series data."),
x_axis_name: tool.schema
.string()
.optional()
.describe("X-axis display name."),
y_axis_name: tool.schema
.string()
.optional()
.describe("Y-axis display name."),
},
async execute() {
// 图表数据已经在工具参数里,前端收到 tool_call 后直接渲染,不再二次请求后端。
return "图表将在对话中显示。";
},
});
+158
View File
@@ -0,0 +1,158 @@
import { tool } from "@opencode-ai/plugin";
import { SkillStore } from "../../src/skills/store.js";
import {
getRuntimeSessionContext,
type RuntimeSessionContext,
} from "../../src/runtime/sessionContext.js";
type ToolContextReader = {
read(sessionId: string): RuntimeSessionContext | null;
};
const runtimeContextReader: ToolContextReader = {
read: getRuntimeSessionContext,
};
export const createSkillManagerTool = (
skillStore = new SkillStore(),
toolContextStore: ToolContextReader = runtimeContextReader,
initializePromise: Promise<unknown> = Promise.resolve(),
) =>
tool({
description:
"维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、write_skill、remove_skill、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。",
args: {
action: tool.schema
.enum([
"list",
"write_skill",
"remove_skill",
"append_pattern",
"remove_pattern",
"write_reference",
"remove_reference",
"write_script",
"remove_script",
])
.describe("Skill maintenance operation."),
reason: tool.schema
.string()
.describe(
"Why this skill maintenance action is justified for future reuse.",
),
skill_path: tool.schema
.string()
.describe(
"Target skill directory path relative to .opencode/skills. Use 'workflow' for the workflow index, or '__root__' for the root skills index.",
),
pattern: tool.schema
.string()
.optional()
.describe("Pattern text used by append_pattern."),
target_id: tool.schema
.string()
.optional()
.describe("Stable learned pattern id used by remove_pattern."),
file_path: tool.schema
.string()
.optional()
.describe(
"Asset file path. For references use references/*.md; for scripts use scripts/*.py.",
),
content: tool.schema
.string()
.optional()
.describe(
"Content used by write_skill, write_reference, or write_script.",
),
},
async execute(args, context) {
await initializePromise;
const sessionContext = toolContextStore.read(context.sessionID);
if (!sessionContext) {
throw new Error(`session context not found for ${context.sessionID}`);
}
if (
sessionContext.allowLearningWrite === false &&
args.action !== "list"
) {
return JSON.stringify({
ok: true,
kind: "skill",
decision: "rejected",
detail: "skill writes are disabled for this session",
});
}
if (args.action === "list") {
const result = await skillStore.list(args.skill_path);
if (!result) {
return JSON.stringify({
ok: true,
kind: "skill",
decision: "rejected",
detail:
"invalid skill_path; expected a relative path under .opencode/skills",
});
}
return JSON.stringify({
ok: true,
kind: "skill",
decision: "accepted",
detail: "skill listed",
references: result.references,
scripts: result.scripts,
skill_path: result.skillPath,
target: result.target,
patterns: result.patterns,
});
}
const result =
args.action === "write_skill"
? await skillStore.writeSkill(args.skill_path, args.content ?? "")
: args.action === "remove_skill"
? await skillStore.removeSkill(args.skill_path)
: args.action === "append_pattern"
? await skillStore.appendPattern(
args.skill_path,
args.pattern ?? "",
)
: args.action === "remove_pattern"
? await skillStore.removePattern(
args.skill_path,
args.target_id ?? "",
)
: args.action === "write_reference"
? await skillStore.writeReference(
args.skill_path,
args.file_path ?? "",
args.content ?? "",
)
: args.action === "remove_reference"
? await skillStore.removeReference(
args.skill_path,
args.file_path ?? "",
)
: args.action === "write_script"
? await skillStore.writeScript(
args.skill_path,
args.file_path ?? "",
args.content ?? "",
)
: await skillStore.removeScript(
args.skill_path,
args.file_path ?? "",
);
return JSON.stringify({
ok: true,
kind: "skill",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: result.target,
});
},
});
export default createSkillManagerTool();
+44
View File
@@ -0,0 +1,44 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"将本地 JSON 渲染数据文件存储到受控路径,返回可供 render_junctions 使用的 render_refres-...)。前置步骤:先准备好符合 render_junctions 数据结构的 JSON 文件 { node_area_map, area_ids?, area_colors? },写入本地路径后再调用本工具传入该路径,获取 render_ref 后传给 render_junctions 完成前端渲染。",
args: {
reason: tool.schema
.string()
.describe(
"为何需要将此本地渲染数据持久化为 render_ref,以便后续通过 render_junctions 渲染到前端。",
),
file_path: tool.schema
.string()
.describe(
"本地 JSON 文件的绝对路径,内容为 render_junctions 所需的数据结构 { node_area_map, area_ids?, area_colors? }。",
),
},
async execute(args, context) {
const response = await fetch(
`${internalBaseUrl}/internal/tools/store-render-ref`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
session_id: context.sessionID,
file_path: args.file_path,
}),
},
);
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+30
View File
@@ -0,0 +1,30 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description: "为选定的管网要素打开前端的历史记录或计算结果面板。",
args: {
reason: tool.schema
.string()
.describe(
"Why this history panel should be opened for the current task.",
),
feature_infos: tool.schema
.array(tool.schema.tuple([tool.schema.string(), tool.schema.string()]))
.describe("List of [id, type] pairs."),
data_type: tool.schema
.enum(["realtime", "scheme", "none"])
.describe("History data source type."),
start_time: tool.schema
.string()
.optional()
.describe("Optional ISO8601 start time."),
end_time: tool.schema
.string()
.optional()
.describe("Optional ISO8601 end time."),
},
async execute() {
// 返回短确认即可;面板打开动作由前端根据 tool_call 参数完成。
return "已打开计算结果面板。";
},
});
+30
View File
@@ -0,0 +1,30 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description: "打开前端的 SCADA 监测数据历史面板。",
args: {
reason: tool.schema
.string()
.describe("Why SCADA panel interaction is required for this request."),
device_ids: tool.schema
.array(tool.schema.string())
.optional()
.describe("Preferred SCADA device ids."),
device_id: tool.schema
.string()
.optional()
.describe("Single SCADA device id."),
start_time: tool.schema
.string()
.optional()
.describe("Optional ISO8601 start time."),
end_time: tool.schema
.string()
.optional()
.describe("Optional ISO8601 end time."),
},
async execute() {
// SCADA 面板仍在浏览器侧执行,工具结果不承载实际监测数据。
return "已打开 SCADA 监测面板。";
},
});
+62
View File
@@ -0,0 +1,62 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"调用 TJWater 后端的实时网页搜索服务。适合查询新闻、政策、规范、产品资料、公开网页事实等可能变化的信息。",
args: {
reason: tool.schema
.string()
.describe("Why web search is required for the current user request."),
query: tool.schema.string().describe("Search query text."),
freshness: tool.schema
.enum(["no_limit", "one_day", "one_week", "one_month", "one_year"])
.optional()
.describe("Optional freshness filter. Defaults to no_limit."),
summary: tool.schema
.boolean()
.optional()
.describe("Whether the backend should include page summaries."),
count: tool.schema
.number()
.int()
.positive()
.optional()
.describe("Optional result count, backend accepts 1 to 50."),
include: tool.schema
.array(tool.schema.string())
.optional()
.describe("Optional domains to include."),
exclude: tool.schema
.array(tool.schema.string())
.optional()
.describe("Optional domains to exclude."),
},
async execute(args, context) {
const response = await fetch(`${internalBaseUrl}/internal/tools/web-search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
session_id: context.sessionID,
query: args.query,
freshness: args.freshness,
summary: args.summary,
count: args.count,
include: args.include,
exclude: args.exclude,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+32
View File
@@ -0,0 +1,32 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description:
"在前端地图上缩放定位到坐标。默认坐标为 EPSG:3857;如果来自天地图 geocode 的 lon/lat,传 source_crs='EPSG:4326',前端会转换为 EPSG:3857 后缩放。",
args: {
reason: tool.schema
.string()
.describe("Why this map zoom action is needed for the current request."),
x: tool.schema
.number()
.describe("X coordinate. For EPSG:4326 this is longitude; for EPSG:3857 this is meters."),
y: tool.schema
.number()
.describe("Y coordinate. For EPSG:4326 this is latitude; for EPSG:3857 this is meters."),
source_crs: tool.schema
.enum(["EPSG:3857", "EPSG:4326"])
.optional()
.describe("Input coordinate CRS. Defaults to EPSG:3857."),
zoom: tool.schema
.number()
.optional()
.describe("Optional OpenLayers zoom level. Defaults to 18."),
duration_ms: tool.schema
.number()
.optional()
.describe("Optional animation duration in milliseconds. Defaults to 1000."),
},
async execute() {
return "已缩放到指定地图坐标。";
},
});