mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(dsh-plugin-browserskill): add the web client half with a browser_screenshot toolview
Make the package dual-face: a dsh.client declaration (platform 'web') plus an exports["./client"] bundle built in dsh's closure-factory shape (window.__ModuleLoader__.load handoff, platform modules external, everything else inlined, CSS Modules via lightningcss). The client registers a keyed 'tool.call.toolview' view for browser_screenshot that keeps the terminal block (command + output) and, when the settled result carries an image block, resolves the durable attachment through the client session's authorized readAttachment RPC and renders it with the shared MessageImage thumbnail/lightbox atoms. Path-only results render unchanged; every other browser_* tool keeps the stock terminal card. Tests: view-model derivation (running/ok/error, image vs path-only), component rendering through @testing-library/react + happy-dom, and the registration key. biome.json now excludes the package's lib/ build output (same treatment as dist/).
This commit is contained in:
@@ -1,10 +1,124 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename, dirname, resolve as resolvePath } from "node:path";
|
||||
import { transform } from "lightningcss";
|
||||
import { defineConfig, type UserConfig } from "tsdown";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
/**
|
||||
* Two build faces of the dual-face package:
|
||||
* - host: the Cordis tool plugin (lib/index.mjs, ESM, dsh packages external);
|
||||
* - client: the Web UI bundle (lib/client.js) in dsh's closure-factory shape —
|
||||
* the artifact registers itself through `window.__ModuleLoader__.load`, with
|
||||
* platform modules resolved from the loader's frozen module table and every
|
||||
* other dependency inlined. Mirrors deepseek-harness `packages/client/tsdown.client.ts`.
|
||||
*/
|
||||
|
||||
/** Module specifiers the dsh web shell shares into its frozen module table. */
|
||||
const CLIENT_EXTERNALS: readonly string[] = [
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"@deepseek-ai/cordis",
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-web-react",
|
||||
"@deepseek-ai/dsh-client-ui-primitives",
|
||||
"@deepseek-ai/dsh-client-ui-attachment",
|
||||
"@deepseek-ai/dsh-client-schema-form",
|
||||
"@deepseek-ai/dsh-client-runtime/client",
|
||||
];
|
||||
|
||||
/** Wire/type layers with no shared runtime identity: safe to inline. */
|
||||
const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand|attachment)(\/|$)/;
|
||||
const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/;
|
||||
|
||||
const CSS_VIRTUAL_PREFIX = "\0bsk-css:";
|
||||
const CSS_VIRTUAL_SUFFIX = ".mjs";
|
||||
const CLIENT_ID = "dsh-plugin-browserskill";
|
||||
|
||||
const client: UserConfig = {
|
||||
name: `${CLIENT_ID}/client`,
|
||||
entry: { client: "src/client/index.ts" },
|
||||
outDir: "lib",
|
||||
// dsh host packages are provided by the profile the plugin is installed into.
|
||||
external: [/^@deepseek-ai\//],
|
||||
});
|
||||
format: "cjs",
|
||||
platform: "browser",
|
||||
dts: false,
|
||||
clean: false,
|
||||
external: [...CLIENT_EXTERNALS],
|
||||
// Anything not in the loader module table must inline; an unanswerable
|
||||
// require() is a guaranteed runtime throw.
|
||||
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "production"),
|
||||
"import.meta.env.MODE": JSON.stringify(process.env.NODE_ENV ?? "production"),
|
||||
"import.meta.env": JSON.stringify({ MODE: process.env.NODE_ENV ?? "production" }),
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
// Bundle purity gate: platform modules stay external, wire layers
|
||||
// inline, every other @deepseek-ai value import is a build error.
|
||||
name: "bsk-client-bundle-purity",
|
||||
resolveId(source: string) {
|
||||
if (!source.startsWith("@deepseek-ai/")) return null;
|
||||
if (CLIENT_EXTERNALS.includes(source)) return null;
|
||||
if (VENDORED_LIBRARY.test(source)) return null;
|
||||
if (INLINE_SAFE.test(source)) return null;
|
||||
throw new Error(
|
||||
`client bundle purity: "${source}" is not a platform module or an inline-safe wire layer — ` +
|
||||
"collaborate through cordis services (type-only imports are erased and never reach this gate)",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bsk-css-modules-inline",
|
||||
resolveId(source: string, importer: string | undefined) {
|
||||
if (!source.endsWith(".module.css")) return null;
|
||||
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source;
|
||||
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX;
|
||||
},
|
||||
async load(virtualId: string) {
|
||||
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null;
|
||||
const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
|
||||
this.addWatchFile(fileId);
|
||||
const source = await readFile(fileId);
|
||||
const { code, exports: cssExports } = transform({
|
||||
filename: fileId,
|
||||
code: source,
|
||||
cssModules: { pattern: "[hash]_[local]" },
|
||||
minify: true,
|
||||
});
|
||||
const classMap: Record<string, string> = {};
|
||||
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name;
|
||||
return [
|
||||
`const css = ${JSON.stringify(code.toString())};`,
|
||||
`const tagId = ${JSON.stringify(`${CLIENT_ID}/${basename(fileId)}`)};`,
|
||||
"if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {",
|
||||
" const tag = document.createElement('style');",
|
||||
` tag.dataset.plugin = ${JSON.stringify(CLIENT_ID)};`,
|
||||
" tag.dataset.pluginCss = tagId;",
|
||||
" tag.textContent = css;",
|
||||
" document.head.appendChild(tag);",
|
||||
"}",
|
||||
`export default ${JSON.stringify(classMap)};`,
|
||||
].join("\n");
|
||||
},
|
||||
},
|
||||
],
|
||||
outputOptions: {
|
||||
entryFileNames: "client.js",
|
||||
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(CLIENT_ID)}, factory: (require) => {`,
|
||||
footer: "return module.exports; } });",
|
||||
intro: "var module = { exports: {} }; var exports = module.exports;",
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
outDir: "lib",
|
||||
// dsh host packages are provided by the profile the plugin is installed into.
|
||||
external: [/^@deepseek-ai\//],
|
||||
},
|
||||
client,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user