mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-10-02 07:34:35 +08:00
fix(dsh-plugin): support DSH 0.2 hosts
This commit is contained in:
@@ -37,6 +37,11 @@ In a conversation, try:
|
||||
|
||||
By default, the browser tools become available when the skill is invoked.
|
||||
|
||||
The plugin declares support for DSH `^0.1.5-rc.3 || ^0.2.0-rc.1`.
|
||||
DSH 0.2 checks these peer requirements before loading the plugin; versions outside
|
||||
this range are not declared compatible. The range preserves the existing 0.1.x
|
||||
baseline and admits 0.2.x without opting into future 0.3.x hosts.
|
||||
|
||||
## Updating
|
||||
|
||||
Installed plugins do not update automatically. To upgrade this plugin to npm's
|
||||
|
||||
@@ -125,8 +125,12 @@ events continue to fold without repeated registration attempts or warnings.
|
||||
explicitly shared platform modules (React and `dsh-client-ui-primitives`) external,
|
||||
everything else inlined, CSS Modules compiled by lightningcss.
|
||||
- **Host compatibility**: development dependencies are pinned to the DSH `0.1.5-rc.3`
|
||||
SDK shipped with DSH `0.1.5-rc.2`, including the current renderer, Session Controller,
|
||||
and chat type contracts. Previously, tests used `0.1.0-rc.6`, which exported `MessageImage`
|
||||
SDK as the minimum build baseline; peer requirements also admit `0.2.0-rc.1`
|
||||
and the 0.2.x line. Keep the 0.3.x boundary closed until that host is verified.
|
||||
Agent-scoped skills use the shared `agent/created` lifecycle event. History
|
||||
recovery accepts both 0.1 tool-result blocks and 0.2 tool-result envelopes,
|
||||
preserving call identity and success checks. Previously, tests used `0.1.0-rc.6`,
|
||||
which exported `MessageImage`
|
||||
from the attachment client; the current attachment client exposes only plugin hooks.
|
||||
Importing the old component caused the screenshot-card expansion crash. Keep service
|
||||
contracts type-only and restrict runtime imports to the host's shared module table.
|
||||
|
||||
@@ -67,11 +67,11 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.2",
|
||||
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.3",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.3",
|
||||
"@deepseek-ai/dsh-client-ui-tool": "^0.1.5-rc.3",
|
||||
"@deepseek-ai/dsh-llm": "^0.1.5-rc.3",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.5-rc.3",
|
||||
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.3 || ^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.3 || ^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-tool": "^0.1.5-rc.3 || ^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.1.5-rc.3 || ^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.5-rc.3 || ^0.2.0-rc.1",
|
||||
"@deepseek-ai/schemastery": "^3.18.2",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
@@ -119,12 +119,14 @@
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"happy-dom": "^15.11.7",
|
||||
"lightningcss": "^1.30.0",
|
||||
"postcss": "^8.5.0",
|
||||
"postcss-prefix-selector": "^2.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "7.8.5",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tsdown": "^0.22.14",
|
||||
"typescript": "^5.8.0",
|
||||
|
||||
@@ -33,7 +33,7 @@ interface ViewModel {
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
const TERMINAL_LABELS: TerminalBlockLabels = {
|
||||
const TERMINAL_LABELS: TerminalBlockLabels & { noExitCode: string } = {
|
||||
signal: (signal) => `Signal ${signal}`,
|
||||
exitCode: (exitCode) => `Exit code ${exitCode}`,
|
||||
running: "Running",
|
||||
@@ -42,6 +42,7 @@ const TERMINAL_LABELS: TerminalBlockLabels = {
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
noOutput: "No output",
|
||||
noExitCode: "No exit code",
|
||||
collapseAria: "Collapse output",
|
||||
collapse: "Collapse",
|
||||
expandAria: (hidden) => `Expand the remaining ${hidden} output lines`,
|
||||
@@ -128,7 +129,8 @@ function commandOf(argsRaw: string, callId: string): { command: string; title: s
|
||||
/** Derive the display model from the frozen block only. */
|
||||
export function viewModelOf(block: ToolCallViewProps["block"]): ViewModel {
|
||||
const settled = "kind" in block;
|
||||
const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? "";
|
||||
// DSH 0.2 can render a preparing call before arguments have been dispatched.
|
||||
const argsRaw = (settled ? block.call?.argsRaw : "argsRaw" in block ? block.argsRaw : "") ?? "";
|
||||
const { command, title } = commandOf(argsRaw, block.callId);
|
||||
if (!settled) {
|
||||
return { state: "running", command, output: null, image: null, summary: command, title };
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
* tool-skill's per-step catalog digest treats visibility changes as a
|
||||
* first-class cache-invalidation input — the suite simply appears in the
|
||||
* NEXT step's assembly.
|
||||
* - Durable results use message.source.callId and a tool-result content
|
||||
* block. Older hosts stored callId/isError directly on the message.
|
||||
* - DSH 0.2 stores toolCallId/isError on the message; DSH 0.1 uses a
|
||||
* tool-result content block. Both carry message.source.callId. Older hosts
|
||||
* stored callId/isError directly on the message.
|
||||
*/
|
||||
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
@@ -61,6 +62,16 @@ function toolResultOf(data: unknown): { callId: string; isError: boolean } | und
|
||||
if (message === undefined) return;
|
||||
const source = record(message.source);
|
||||
if (source?.kind === "tool") {
|
||||
if (message.role === "tool" && "toolCallId" in message) {
|
||||
if (
|
||||
isCallId(source.callId) &&
|
||||
message.toolCallId === source.callId &&
|
||||
typeof message.isError === "boolean"
|
||||
) {
|
||||
return { callId: source.callId, isError: message.isError };
|
||||
}
|
||||
return;
|
||||
}
|
||||
const blocks = message.content;
|
||||
const block = Array.isArray(blocks) && blocks.length === 1 ? record(blocks[0]) : undefined;
|
||||
if (
|
||||
|
||||
@@ -67,8 +67,8 @@ export function registerBskSkill(ctx: Context): () => void {
|
||||
* Registering the embedded skill through `agent.ctx` makes the DSH protocol
|
||||
* contract authoritative for that agent without touching the shared CLI skill.
|
||||
*
|
||||
* New agents are handled at `agent/session-start`, the first supported startup
|
||||
* injection point and still before the first prompt assembly. Existing agents
|
||||
* New agents are handled at `agent/created`, supported by both DSH 0.1 and 0.2
|
||||
* after agent setup and before the first prompt assembly. Existing agents
|
||||
* are registered immediately so plugin reloads take effect without recreating
|
||||
* the conversation. Returns a disposer for all plugin-owned registrations.
|
||||
*/
|
||||
@@ -81,11 +81,12 @@ export function armAgentScopedBskSkill(ctx: Context): () => void {
|
||||
registrations.set(agent, registerBskSkill(agent.ctx));
|
||||
};
|
||||
|
||||
let stopSessionStart = () => {};
|
||||
let stopCreated = () => {};
|
||||
let stopDisposed = () => {};
|
||||
if (typeof ctx.on === "function") {
|
||||
stopSessionStart = ctx.on("agent/session-start", ({ agent }) => {
|
||||
stopCreated = ctx.on("agent/created", ({ agent }) => {
|
||||
registerForAgent(agent);
|
||||
return undefined;
|
||||
});
|
||||
stopDisposed = ctx.on("agent/disposed", ({ agent }) => {
|
||||
// Agent-scoped effects have already unwound at this lifecycle edge. Drop
|
||||
@@ -103,7 +104,7 @@ export function armAgentScopedBskSkill(ctx: Context): () => void {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
stopDisposed();
|
||||
stopSessionStart();
|
||||
stopCreated();
|
||||
for (const unregister of [...registrations.values()].reverse()) unregister();
|
||||
registrations.clear();
|
||||
};
|
||||
|
||||
@@ -57,6 +57,14 @@ const PATH_TEXT = "[session s1] screenshot saved to /tmp/shot.png (800x457px, 8
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("viewModelOf", () => {
|
||||
it("keeps a DSH 0.2 preparing call renderable before arguments exist", () => {
|
||||
const model = viewModelOf({ callId: "c1", name: "browser_inspect" } as never);
|
||||
expect(model.state).toBe("running");
|
||||
expect(model.command).toBe("browser_inspect (c1)");
|
||||
expect(model.image).toBeNull();
|
||||
expect(model.output).toBeNull();
|
||||
});
|
||||
|
||||
it("derives the running model from the call frame", () => {
|
||||
const model = viewModelOf(runningBlock('{"action":"screenshot","session":"s1"}'));
|
||||
expect(model.state).toBe("running");
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { satisfies } from "semver";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
const dshPeers = Object.entries(manifest.peerDependencies).filter(
|
||||
([name]) => name === "@deepseek-ai/dsh" || name.startsWith("@deepseek-ai/dsh-"),
|
||||
);
|
||||
|
||||
// DSH checks every DSH peer against its runtime version before loading the
|
||||
// bundle, including optional peers. Its preflight includes prereleases.
|
||||
describe("DSH host admission", () => {
|
||||
it("declares host peer requirements", () => {
|
||||
expect(dshPeers.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe.each(dshPeers)("%s (%s)", (_name, range) => {
|
||||
it.each([
|
||||
"0.1.5-rc.3",
|
||||
"0.1.5",
|
||||
"0.1.7-rc.2",
|
||||
"0.2.0-rc.1",
|
||||
"0.2.0",
|
||||
"0.2.1",
|
||||
])("admits supported host %s", (version) => {
|
||||
expect(satisfies(version, range, { includePrerelease: true })).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"0.1.0",
|
||||
"0.1.5-rc.2",
|
||||
"0.2.0-rc.0",
|
||||
"0.3.0-rc.1",
|
||||
"0.3.0",
|
||||
"1.0.0",
|
||||
])("keeps unsupported host %s outside the declared range", (version) => {
|
||||
expect(satisfies(version, range, { includePrerelease: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -528,13 +528,45 @@ describe("hasSuccessfulSkillInvocation", () => {
|
||||
{ type: "tool/result", data: { message: { isError: false } } },
|
||||
]),
|
||||
).toBe(false);
|
||||
const message = structuredClone(skillResult().data.message);
|
||||
message.content[0].toolCallId = "different" as never;
|
||||
const message = {
|
||||
source: { kind: "tool", callId: "skill-1" },
|
||||
content: [{ type: "tool-result", toolCallId: "different", isError: false }],
|
||||
};
|
||||
expect(
|
||||
hasSuccessfulSkillInvocation([skillCall(), { type: "tool/result", data: { message } }]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("restores DSH 0.2 results with matching envelope identities and a successful outcome", () => {
|
||||
const message = {
|
||||
role: "tool",
|
||||
source: { kind: "tool", callId: "skill-1" },
|
||||
toolCallId: "skill-1",
|
||||
content: [{ type: "text", text: "skill instructions" }],
|
||||
isError: false,
|
||||
};
|
||||
const result = { type: "tool/result", data: { message } };
|
||||
expect(hasSuccessfulSkillInvocation([skillCall(), result])).toBe(true);
|
||||
expect(hasSuccessfulSkillInvocation([result])).toBe(false);
|
||||
expect(hasSuccessfulSkillInvocation([result, skillCall()])).toBe(false);
|
||||
|
||||
for (const invalid of [
|
||||
{ ...message, isError: true },
|
||||
{ ...message, isError: undefined },
|
||||
{ ...message, role: "assistant" },
|
||||
{ ...message, toolCallId: "different" },
|
||||
{ ...message, source: { kind: "tool", callId: "different" } },
|
||||
{ ...message, source: { kind: "tool" } },
|
||||
]) {
|
||||
expect(
|
||||
hasSuccessfulSkillInvocation([
|
||||
skillCall(),
|
||||
{ type: "tool/result", data: { message: invalid } },
|
||||
]),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("pairs call and result by callId and honors gestures", () => {
|
||||
expect(hasSuccessfulSkillInvocation([])).toBe(false);
|
||||
expect(
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("registerBskSkill", () => {
|
||||
});
|
||||
|
||||
describe("armAgentScopedBskSkill", () => {
|
||||
it("overrides a nearer legacy CLI skill when the DSH agent starts", async () => {
|
||||
it("overrides a nearer legacy CLI skill when the DSH agent is created", async () => {
|
||||
const root = new Context();
|
||||
const skillFiber = root.plugin(SkillRegistry);
|
||||
await skillFiber;
|
||||
@@ -143,10 +143,8 @@ describe("armAgentScopedBskSkill", () => {
|
||||
expect(before?.source).toBe("user-agents");
|
||||
|
||||
const disarm = armAgentScopedBskSkill(pluginCtx);
|
||||
pluginCtx.emit(scopeTarget(agent, agentKey), "agent/session-start", {
|
||||
agent,
|
||||
source: "startup",
|
||||
});
|
||||
const payload = { agent, source: "startup" as const };
|
||||
await pluginCtx.serial(scopeTarget(agent, agentKey), "agent/created", payload);
|
||||
|
||||
const after = await skillFiber.ctx.skills.get("browser-skill", { scope: agentKey });
|
||||
expect(after?.content).toMatch(/All browser work\s+must use the injected tools directly/);
|
||||
@@ -188,7 +186,7 @@ describe("armAgentScopedBskSkill", () => {
|
||||
expect(registered).toHaveLength(1);
|
||||
|
||||
// A later lifecycle notification for the same agent must not duplicate it.
|
||||
listeners.get("agent/session-start")?.({ agent });
|
||||
listeners.get("agent/created")?.({ agent });
|
||||
expect(registered).toHaveLength(1);
|
||||
|
||||
disarm();
|
||||
|
||||
Generated
+18
@@ -167,6 +167,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^18.3.0
|
||||
version: 18.3.7(@types/react@18.3.31)
|
||||
'@types/semver':
|
||||
specifier: 7.7.1
|
||||
version: 7.7.1
|
||||
happy-dom:
|
||||
specifier: ^15.11.7
|
||||
version: 15.11.7
|
||||
@@ -185,6 +188,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
semver:
|
||||
specifier: 7.8.5
|
||||
version: 7.8.5
|
||||
tailwindcss:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3
|
||||
@@ -1812,6 +1818,9 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
@@ -3617,6 +3626,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-value@4.1.0:
|
||||
resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==}
|
||||
engines: {node: '>=11.0'}
|
||||
@@ -5482,6 +5496,8 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@ungap/structured-clone@1.4.0': {}
|
||||
@@ -7377,6 +7393,8 @@ snapshots:
|
||||
|
||||
semver@7.8.0: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
|
||||
set-value@4.1.0:
|
||||
dependencies:
|
||||
is-plain-object: 2.0.4
|
||||
|
||||
Reference in New Issue
Block a user