Compare commits

..
Author SHA1 Message Date
drakezhang 50a64d45c0 chore(release): 0.2.0 2026-09-02 18:08:34 +08:00
10 changed files with 32 additions and 129 deletions
+2
View File
@@ -9,6 +9,8 @@ Starting from 0.2.0, CLI / Extension / DSH Plugin share the same version number.
## [0.2.0] - 2026-09-02
## [0.2.0] - 2026-09-XX
### Added
- File transfer: upload and download support across CLI, Extension, and DSH Plugin
- File transfer: drag-and-drop upload (`drop-to-upload`)
@@ -45,32 +45,10 @@ function portHarness() {
describe("RecordFrameAgent", () => {
afterEach(() => {
vi.unstubAllGlobals();
document.documentElement.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE);
document.body.replaceChildren();
});
it("starts when crypto.randomUUID is unavailable", async () => {
const harness = portHarness();
const getRandomValues = vi.fn((bytes: Uint8Array) => {
bytes.fill(0x11);
return bytes;
});
vi.stubGlobal("crypto", { getRandomValues });
vi.stubGlobal("chrome", {
runtime: {
connect: vi.fn(() => harness.port),
sendMessage: vi.fn(),
},
});
const agent = new RecordFrameAgent();
await expect(
agent.start({ type: RECORD_FRAME_START, requestId: "rec-http", startedAtMs: 10 }),
).resolves.toEqual({ ok: true });
expect(getRandomValues).toHaveBeenCalled();
});
it("keeps a failed stop retryable and flushes the final dirty fill", async () => {
const harness = portHarness();
const sendMessage = vi
@@ -1,4 +1,3 @@
import { createRandomUuid } from "@/lib/random-uuid";
import {
isAcceptedRecordStepAck,
RECORD_STEP,
@@ -19,7 +18,7 @@ export class RecordStepDelivery {
constructor(
requestId: string,
send: SendRecordStepMessage = (message) => chrome.runtime.sendMessage(message),
producerId: string = createRandomUuid(),
producerId: string = crypto.randomUUID(),
) {
this.#requestId = requestId;
this.#producerId = producerId;
@@ -1,4 +1,3 @@
import { createRandomUuid } from "@/lib/random-uuid";
import {
markRecordingDocument,
type RecordingDocumentMarker,
@@ -68,7 +67,7 @@ export class RecordFrameAgent {
async #start(
message: RecordFrameStartMessage,
): Promise<{ ok: true } | { ok: false; error: string }> {
const producerId = createRandomUuid();
const producerId = crypto.randomUUID();
const root = await waitForDocumentElement();
if (this.#disposed) return { ok: false, error: "recording document is disposed" };
const marker = markRecordingDocument(producerId, root);
@@ -122,8 +122,8 @@ describe("App", () => {
expect(screen.queryByText(/^扩展 v/)).toBeNull();
expect(screen.queryByText(/^daemon v/)).toBeNull();
expect(screen.getByTitle("扩展版本").textContent).toBe(`Ext ${EXTENSION_VERSION}`);
expect(screen.getByTitle("bsk 版本").textContent).toBe(`CLI ${mockDaemonVersion}`);
expect(screen.getByTitle("扩展版本").textContent).toBe(EXTENSION_VERSION);
expect(screen.getByTitle("bsk 版本").textContent).toBe(mockDaemonVersion);
expect(screen.getByText("03c3e47f")).toBeTruthy();
const copyButton = screen.getByRole("button", { name: "复制实例 ID" });
@@ -1,30 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createRandomUuid } from "../random-uuid";
describe("createRandomUuid", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("uses crypto.randomUUID when it is available", () => {
const randomUUID = vi.fn(() => "00000000-0000-4000-8000-000000000000");
vi.stubGlobal("crypto", { randomUUID, getRandomValues: vi.fn() });
expect(createRandomUuid()).toBe("00000000-0000-4000-8000-000000000000");
expect(randomUUID).toHaveBeenCalledOnce();
});
it("falls back to getRandomValues and sets UUID v4 bits", () => {
const bytes = new Uint8Array(16).fill(0xff);
const getRandomValues = vi.fn((target: Uint8Array) => {
target.set(bytes);
return target;
});
vi.stubGlobal("crypto", { getRandomValues });
const uuid = createRandomUuid();
expect(uuid).toBe("ffffffff-ffff-4fff-bfff-ffffffffffff");
expect(getRandomValues).toHaveBeenCalledOnce();
});
});
-26
View File
@@ -1,26 +0,0 @@
/**
* Generate a UUID v4 in browser contexts where randomUUID() is unavailable.
*
* Crypto.randomUUID() is restricted to secure contexts, while
* crypto.getRandomValues() is available to content scripts on ordinary HTTP
* origins. BrowserSkill records many intranet pages served over HTTP, so the
* fallback is required for recording to work on those pages.
*/
export function createRandomUuid(): string {
const cryptoApi = globalThis.crypto;
if (typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID();
const bytes = new Uint8Array(16);
cryptoApi.getRandomValues(bytes);
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20),
].join("-");
}
@@ -1,4 +1,3 @@
import { createRandomUuid } from "../random-uuid";
import {
isRecordFrameQueryMessage,
RECORD_FRAME_PORT,
@@ -189,7 +188,7 @@ export class RecordFrameCoordinator {
const recording = this.#recordings.get(requestId);
if (!recording) return true;
recording.finishing = true;
const commandId = createRandomUuid();
const commandId = crypto.randomUUID();
const results = await Promise.all(
[...recording.agents.values()].map((agent) => this.#stopAgent(agent, requestId, commandId)),
);
+2 -5
View File
@@ -761,7 +761,6 @@ fn stage_windows_replacement(
.with_context(|| format!("write {}", paths.script_path.display()))?;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
let command = format!("\"{}\"", paths.script_path.display());
if let Err(err) = Command::new("cmd.exe")
.args(["/D", "/S", "/C"])
@@ -769,7 +768,7 @@ fn stage_windows_replacement(
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)
.creation_flags(CREATE_NO_WINDOW)
.spawn()
{
let _ = std::fs::remove_file(&paths.binary_path);
@@ -807,7 +806,7 @@ fn windows_replacement_script(target: &Path, staged_binary: &Path, restart_daemo
:retry\r\n\
move /Y \"{staged_binary}\" \"{target}\" >nul 2>nul\r\n\
if errorlevel 1 (\r\n\
ping -n 2 127.0.0.1 >nul\r\n\
timeout /t 1 /nobreak >nul\r\n\
goto retry\r\n\
)\r\n\
{restart}\
@@ -1008,8 +1007,6 @@ mod tests {
assert!(script.contains(":retry"));
assert!(script.contains("move /Y"));
assert!(script.contains("ping -n 2 127.0.0.1"));
assert!(!script.contains("timeout"));
assert!(script.contains(r#"start "" /B "C:\Program Files\bsk.exe" daemon start"#));
assert!(script.contains(r#"del /F /Q "%~f0""#));
}
+23 -38
View File
@@ -51,9 +51,7 @@ const { values, positionals } = parseArgs({
});
if (values.help) {
console.log(
readFileSync(new URL(import.meta.url), "utf8").match(/\/\*\*([\s\S]*?)\*\//)?.[1] ?? "",
);
console.log(readFileSync(new URL(import.meta.url), "utf8").match(/\/\*\*([\s\S]*?)\*\//)?.[1] ?? "");
process.exit(0);
}
@@ -62,7 +60,9 @@ const pushAfter = values.push;
const pushTagsOnly = values["push-tags"];
const noCommit = values["no-commit"];
const onlyRaw = values.only;
const components = onlyRaw ? onlyRaw.split(",").map((s) => s.trim().toLowerCase()) : ALL_COMPONENTS;
const components = onlyRaw
? onlyRaw.split(",").map((s) => s.trim().toLowerCase())
: ALL_COMPONENTS;
for (const c of components) {
if (!ALL_COMPONENTS.includes(c)) {
@@ -95,9 +95,7 @@ if (pushTagsOnly) {
const newVersion = positionals[0];
if (!newVersion) {
console.error(
"Usage: node scripts/release.mjs <version> [--dry-run] [--push] [--only cli,ext,dsh]",
);
console.error("Usage: node scripts/release.mjs <version> [--dry-run] [--push] [--only cli,ext,dsh]");
process.exit(1);
}
@@ -112,9 +110,8 @@ if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(newVersion)) {
function readCurrentVersions() {
const cargo = readFileSync(VERSION_FILES.cargo, "utf8");
const cargoVer =
cargo.match(/^\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/m)?.[1] ??
cargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
const cargoVer = cargo.match(/^\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/m)?.[1]
?? cargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
const extPkg = JSON.parse(readFileSync(VERSION_FILES.extension, "utf8"));
const dshPkg = JSON.parse(readFileSync(VERSION_FILES.dshPlugin, "utf8"));
@@ -173,17 +170,19 @@ function updateChangelog(version) {
}
const today = new Date().toISOString().slice(0, 10);
const escapedVer = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// First: fill in any date-placeholder line for this version (e.g. "## [0.2.0] - 2026-09-XX")
const versionPlaceholder = new RegExp(`^## \\[${escapedVer}\\]\\s*-\\s*\\d{4}-\\d{2}-XX`, "m");
if (versionPlaceholder.test(content)) {
content = content.replace(versionPlaceholder, `## [${version}] - ${today}`);
const unreleased = /^## \[Unreleased\]/m;
if (unreleased.test(content)) {
content = content.replace(
unreleased,
`## [Unreleased]\n\n## [${version}] - ${today}`,
);
} else {
// Otherwise: insert a new section after [Unreleased]
const unreleased = /^## \[Unreleased\]/m;
if (unreleased.test(content)) {
content = content.replace(unreleased, `## [Unreleased]\n\n## [${version}] - ${today}`);
const versionPlaceholder = new RegExp(
`^## \\[${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\s*-\\s*\\d{4}-\\d{2}-XX`,
"m",
);
if (versionPlaceholder.test(content)) {
content = content.replace(versionPlaceholder, `## [${version}] - ${today}`);
}
}
@@ -196,25 +195,13 @@ function updateChangelog(version) {
const writes = [
{ path: VERSION_FILES.cargo, content: bumpCargo(newVersion), label: "Cargo.toml" },
{
path: VERSION_FILES.extension,
content: bumpPackageJson(VERSION_FILES.extension, newVersion),
label: "Extension package.json",
},
{
path: VERSION_FILES.dshPlugin,
content: bumpPackageJson(VERSION_FILES.dshPlugin, newVersion),
label: "DSH Plugin package.json",
},
{ path: VERSION_FILES.extension, content: bumpPackageJson(VERSION_FILES.extension, newVersion), label: "Extension package.json" },
{ path: VERSION_FILES.dshPlugin, content: bumpPackageJson(VERSION_FILES.dshPlugin, newVersion), label: "DSH Plugin package.json" },
];
const changelogUpdate = updateChangelog(newVersion);
if (changelogUpdate) {
writes.push({
path: changelogUpdate.path,
content: changelogUpdate.content,
label: "CHANGELOG.md",
});
writes.push({ path: changelogUpdate.path, content: changelogUpdate.content, label: "CHANGELOG.md" });
}
for (const w of writes) {
@@ -250,9 +237,7 @@ if (noCommit || dryRun) {
}
console.log("\nCommitting...");
run(
"git add Cargo.toml Cargo.lock apps/extension/package.json packages/dsh-plugin-browserskill/package.json CHANGELOG.md 2>/dev/null || true",
);
run("git add Cargo.toml Cargo.lock apps/extension/package.json packages/dsh-plugin-browserskill/package.json CHANGELOG.md 2>/dev/null || true");
run(`git commit -m "chore(release): ${newVersion}"`);
console.log("Tagging...");