Compare commits
2
Commits
v2026.09.25.3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4744f1f20a | ||
|
|
54d8feca14 |
@@ -19,6 +19,7 @@ RUN if [ -n "${UBUNTU_APT_MIRROR}" ]; then \
|
||||
curl \
|
||||
jq \
|
||||
libseccomp2 \
|
||||
ripgrep \
|
||||
unzip \
|
||||
python3 \
|
||||
python3-venv && \
|
||||
@@ -96,6 +97,7 @@ RUN bun run check
|
||||
|
||||
FROM build AS test
|
||||
COPY --from=opencode-builder /out/opencode /usr/local/bin/opencode
|
||||
RUN command -v rg && rg --version
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY contracts ./contracts
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Part } from "@opencode-ai/sdk/v2";
|
||||
|
||||
import {
|
||||
getToolLabel,
|
||||
initialActivity,
|
||||
normalizeToolStatus,
|
||||
type ActivityActionPayload,
|
||||
type ActivityPayload,
|
||||
@@ -12,6 +13,51 @@ import {
|
||||
|
||||
type ToolPart = Extract<Part, { type: "tool" }>;
|
||||
type ActivityContext = Pick<ActivityPayload, "id" | "title" | "reason">;
|
||||
type AutomaticStageKind = "reference" | "data" | "analysis" | "visualization" | "other";
|
||||
|
||||
const automaticStages: Record<AutomaticStageKind, Pick<ActivityPayload, "title" | "reason">> = {
|
||||
reference: {
|
||||
title: "查找所需资料",
|
||||
reason: "正在检索完成本次请求所需的工作流和参考信息。",
|
||||
},
|
||||
data: {
|
||||
title: "获取业务数据",
|
||||
reason: "正在查询本次分析所需的数据和业务能力。",
|
||||
},
|
||||
analysis: {
|
||||
title: "处理分析数据",
|
||||
reason: "正在对已获取的信息进行计算和核查。",
|
||||
},
|
||||
visualization: {
|
||||
title: "更新地图与图表",
|
||||
reason: "正在将分析内容呈现在地图或图表中。",
|
||||
},
|
||||
other: {
|
||||
title: "执行分析操作",
|
||||
reason: "正在处理完成本次请求所需的操作。",
|
||||
},
|
||||
};
|
||||
|
||||
const referenceTools = new Set(["skill", "read", "glob", "grep", "session_search"]);
|
||||
const dataTools = new Set(["tjwater_cli", "web_search", "geocode"]);
|
||||
const visualizationTools = new Set([
|
||||
"store_render_ref",
|
||||
"locate_features",
|
||||
"zoom_to_map",
|
||||
"view_scada",
|
||||
"show_chart",
|
||||
"render_junctions",
|
||||
"view_history",
|
||||
"apply_layer_style",
|
||||
]);
|
||||
|
||||
const getAutomaticStageKind = (tool: string): AutomaticStageKind => {
|
||||
if (referenceTools.has(tool)) return "reference";
|
||||
if (dataTools.has(tool)) return "data";
|
||||
if (tool === "bash") return "analysis";
|
||||
if (visualizationTools.has(tool)) return "visualization";
|
||||
return "other";
|
||||
};
|
||||
|
||||
const getActionTarget = (params: Record<string, unknown>) => {
|
||||
for (const key of ["command", "file_path", "filePath", "path", "query", "keyword"]) {
|
||||
@@ -33,6 +79,9 @@ export const createActivityTracker = ({
|
||||
const activities = new Map<string, ActivityPayload>();
|
||||
const actionActivityIds = new Map<string, string>();
|
||||
let currentActivityId: string | null = null;
|
||||
let currentStageOrigin: "startup" | "explicit" | "automatic" = "startup";
|
||||
let currentAutomaticKind: AutomaticStageKind | null = null;
|
||||
let automaticStageCount = 0;
|
||||
|
||||
const emit = (activity: ActivityPayload, todos?: TodoItemPayload[]) => {
|
||||
const now = Date.now();
|
||||
@@ -88,11 +137,13 @@ export const createActivityTracker = ({
|
||||
emit(nextActivity);
|
||||
};
|
||||
|
||||
const start = (
|
||||
const startStage = (
|
||||
id: string,
|
||||
title: string,
|
||||
reason: string,
|
||||
todos?: TodoItemPayload[],
|
||||
todos: TodoItemPayload[] | undefined,
|
||||
origin: typeof currentStageOrigin,
|
||||
automaticKind: AutomaticStageKind | null = null,
|
||||
) => {
|
||||
finalize("completed");
|
||||
const activity: ActivityPayload = {
|
||||
@@ -106,17 +157,47 @@ export const createActivityTracker = ({
|
||||
};
|
||||
activities.set(id, activity);
|
||||
currentActivityId = id;
|
||||
currentStageOrigin = origin;
|
||||
currentAutomaticKind = automaticKind;
|
||||
emit(activity, todos);
|
||||
return activity;
|
||||
};
|
||||
|
||||
const start = (
|
||||
id: string,
|
||||
title: string,
|
||||
reason: string,
|
||||
todos?: TodoItemPayload[],
|
||||
) => startStage(
|
||||
id,
|
||||
title,
|
||||
reason,
|
||||
todos,
|
||||
id === initialActivity.id ? "startup" : "explicit",
|
||||
);
|
||||
|
||||
const ensure = (tool: string) => {
|
||||
const activity = current();
|
||||
if (activity?.status === "running") return activity;
|
||||
return start(
|
||||
`activity-fallback-${Date.now().toString(36)}`,
|
||||
"执行分析操作",
|
||||
`为完成当前请求,需要使用${getToolLabel(tool)}处理相关信息。`,
|
||||
if (activity?.status === "running" && currentStageOrigin === "explicit") {
|
||||
return activity;
|
||||
}
|
||||
const kind = getAutomaticStageKind(tool);
|
||||
if (
|
||||
activity?.status === "running" &&
|
||||
currentStageOrigin === "automatic" &&
|
||||
(currentAutomaticKind === kind ||
|
||||
activity.actions.some((action) => action.status === "running"))
|
||||
) {
|
||||
return activity;
|
||||
}
|
||||
const stage = automaticStages[kind];
|
||||
return startStage(
|
||||
`activity-auto-${++automaticStageCount}`,
|
||||
stage.title,
|
||||
stage.reason,
|
||||
undefined,
|
||||
"automatic",
|
||||
kind,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,72 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { createActivityTracker } from "../../src/routes/chatActivityTracker.js";
|
||||
import { initialActivity } from "../../src/routes/chatStreamEvents.js";
|
||||
|
||||
describe("createActivityTracker", () => {
|
||||
it("moves real tool work out of preparation when the model omits activity updates", () => {
|
||||
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||
const tracker = createActivityTracker({
|
||||
clientSessionId: "client-session-1",
|
||||
write: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
const action = (id: string, tool: string, status: string) =>
|
||||
tracker.upsertAction(
|
||||
{ id, tool, state: { status, input: {} } } as never,
|
||||
{},
|
||||
);
|
||||
|
||||
tracker.start(initialActivity.id, initialActivity.title, initialActivity.reason);
|
||||
action("skill-1", "skill", "completed");
|
||||
action("read-1", "read", "completed");
|
||||
action("data-1", "tjwater_cli", "completed");
|
||||
action("data-1", "tjwater_cli", "completed");
|
||||
action("compute-1", "bash", "completed");
|
||||
action("map-1", "show_chart", "completed");
|
||||
tracker.finalize("completed");
|
||||
|
||||
const started = events
|
||||
.filter(({ data }) =>
|
||||
(data.activity as { actions: unknown[] }).actions.length === 0 &&
|
||||
(data.activity as { status: string }).status === "running",
|
||||
)
|
||||
.map(({ data }) => (data.activity as { title: string }).title);
|
||||
expect(started).toEqual([
|
||||
initialActivity.title,
|
||||
"查找所需资料",
|
||||
"获取业务数据",
|
||||
"处理分析数据",
|
||||
"更新地图与图表",
|
||||
]);
|
||||
expect(tracker.getActionContext("read-1")?.title).toBe("查找所需资料");
|
||||
expect(tracker.getActionContext("data-1")?.title).toBe("获取业务数据");
|
||||
expect(tracker.getActionContext("map-1")?.title).toBe("更新地图与图表");
|
||||
});
|
||||
|
||||
it("keeps a model-provided stage across tool categories", () => {
|
||||
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||
const tracker = createActivityTracker({
|
||||
clientSessionId: "client-session-1",
|
||||
write: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
tracker.start(initialActivity.id, initialActivity.title, initialActivity.reason);
|
||||
tracker.start("model-stage", "验证分析结果", "核对本次计算与数据。");
|
||||
for (const [id, tool] of [["tool-1", "tjwater_cli"], ["tool-2", "show_chart"]]) {
|
||||
tracker.upsertAction(
|
||||
{ id, tool, state: { status: "completed", input: {} } } as never,
|
||||
{},
|
||||
);
|
||||
}
|
||||
expect(tracker.getActionContext("tool-1")?.title).toBe("验证分析结果");
|
||||
expect(tracker.getActionContext("tool-2")?.title).toBe("验证分析结果");
|
||||
expect(
|
||||
events.filter(({ data }) =>
|
||||
(data.activity as { actions: unknown[] }).actions.length === 0 &&
|
||||
(data.activity as { status: string }).status === "running",
|
||||
),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("groups actions and closes running children with their activity", () => {
|
||||
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||
const tracker = createActivityTracker({
|
||||
|
||||
Reference in New Issue
Block a user