Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions adapters/binance/binance-copy-trading.adapter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: bpa.adapter/v1alpha1
kind: Adapter
metadata:
id: binance-copy-trading
version: 1.0.0
title: Binance 合约跟单只读采集 Adapter
description: 从用户已登录的 Binance 合约跟单管理页读取账户汇总、项目摘要和当前可见仓位;不读取认证存储,不执行交易或设置操作。
platform: binance
origins: [https://www.binance.com]
extension: { minimumVersion: 0.6.2 }
capabilities:
- nodeId: binance.copy-trading.management.snapshot.read
nodeVersions: [1.0.0]
handlerId: binance.copy-trading.management.snapshot.read
handlerVersion: 1.0.0
implementationDigest: sha256:9996a1ee4f02435581bff8d92fe9ba6418d1889f014e215689a14cac9040e083
permissions: [browser.dom.read, browser.dom.write, browser.tabs.read]
- nodeId: binance.copy-trading.project.detail.collect
nodeVersions: [1.0.0]
handlerId: binance.copy-trading.project.detail.collect
handlerVersion: 1.0.0
implementationDigest: sha256:9996a1ee4f02435581bff8d92fe9ba6418d1889f014e215689a14cac9040e083
permissions: [browser.dom.read, browser.dom.write, browser.tabs.read]
17 changes: 17 additions & 0 deletions adapters/binance/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@bpa/adapter-binance",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@bpa/compiler": "workspace:*",
"@bpa/schemas": "workspace:*"
},
"devDependencies": {
"@types/jsdom": "^21.1.7",
"jsdom": "^27.0.1"
}
}
124 changes: 124 additions & 0 deletions adapters/binance/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { parseWorkflowYaml } from "@bpa/compiler";
import { JSDOM } from "jsdom";
import { describe, expect, it } from "vitest";
import {
collectBinanceManagementSnapshot,
detectBinanceRiskSignals,
readBinanceManagementSnapshot
} from "./index.js";

function page(body: string, url = "https://www.binance.com/zh-CN/copy-trading/copy-management"): Document {
return new JSDOM(`<body><main>${body}</main></body>`, { url }).window.document;
}

describe("Binance copy-trading adapter", () => {
it("pins the exact read-only browser implementation", () => {
const adapter = parseWorkflowYaml(readFileSync(
new URL("../binance-copy-trading.adapter.yaml", import.meta.url),
"utf8"
)) as {
metadata: { version: string };
extension: { minimumVersion: string };
capabilities: Array<{ implementationDigest: string; permissions: string[] }>;
};
const implementationDigest = `sha256:${createHash("sha256")
.update([
"apps/extension/src/entrypoints/content.ts",
"apps/extension/src/lib/capability-manifest.ts",
"apps/extension/src/lib/content-action-router.ts",
"apps/extension/src/lib/page-observer-registry.ts",
"apps/extension/src/lib/adapter-node-registry.ts",
"apps/extension/src/lib/binance-detail-background.ts",
"apps/extension/src/lib/binance-detail-content.ts",
"adapters/binance/src/index.ts",
"adapters/binance/src/project-detail.ts"
].map((path) => readFileSync(new URL(`../../../${path}`, import.meta.url))).join("\n"))
.digest("hex")}`;
expect(adapter).toMatchObject({
metadata: { version: "1.0.0" },
extension: { minimumVersion: "0.6.2" },
capabilities: [
{
implementationDigest,
permissions: ["browser.dom.read", "browser.dom.write", "browser.tabs.read"]
},
{
implementationDigest,
permissions: ["browser.dom.read", "browser.dom.write", "browser.tabs.read"]
}
]
});
});
it("reads account summary and a project without collecting trader names", () => {
const document = page(`
<div><span>保证金余额</span><span>1,000 USDT</span></div>
<div data-project-id="project_1001">
<span>项目 ID:project_1001</span><span>跟单时间</span><span>2026-08-01</span>
<span>净利润</span><span>25 USDT</span><span>交易员显示名</span><span>不得落库的姓名</span>
<button>展开详情</button>
</div>
`);
const result = readBinanceManagementSnapshot(document, document.defaultView!.location.href, new Date("2026-08-12T04:00:00.000Z"));
expect(result).toMatchObject({ status: "complete", accountSummary: { 保证金余额: "1,000 USDT" }, projects: [{ projectId: "project_1001", status: "ongoing", summary: { 跟单时间: "2026-08-01", 净利润: "25 USDT" } }], formMutations: 0 });
expect(JSON.stringify(result)).not.toContain("不得落库的姓名");
});

it("accepts an explicit empty state", () => {
expect(readBinanceManagementSnapshot(page("<div>暂无进行中跟单项目</div>"))).toMatchObject({ status: "empty_confirmed", projects: [] });
});

it("finds a project that was already expanded before collection", () => {
const document = page(`
<section data-project-id="project_1001">
<span>项目 ID:project_1001</span><span>净利润</span><span>25 USDT</span>
<button>收起详情</button><div><button role="tab" aria-selected="true">仓位</button></div>
</section>
`);
expect(readBinanceManagementSnapshot(document).projects).toMatchObject([
{ projectId: "project_1001", summary: { 净利润: "25 USDT" } }
]);
});

it("collects ongoing and ended projects and restores the original tab", async () => {
const document = page(`
<button role="tab" aria-selected="true">进行中</button>
<button role="tab" aria-selected="false">已结束</button>
<div id="projects" data-project-id="ongoing_1001">
<span>项目 ID:ongoing_1001</span><span>净利润</span><span>1 USDT</span><button>展开详情</button>
</div>
`);
const [ongoing, ended] = Array.from(
document.querySelectorAll<HTMLButtonElement>("[role='tab']")
);
const projects = document.querySelector<HTMLElement>("#projects")!;
const activate = (button: HTMLButtonElement, id: string): void => {
ongoing!.setAttribute("aria-selected", String(button === ongoing));
ended!.setAttribute("aria-selected", String(button === ended));
projects.setAttribute("data-project-id", id);
projects.innerHTML = `<span>项目 ID:${id}</span><span>净利润</span><span>1 USDT</span><button>展开详情</button>`;
};
ongoing!.addEventListener("click", () => activate(ongoing!, "ongoing_1001"));
ended!.addEventListener("click", () => activate(ended!, "ended_1001"));
const result = await collectBinanceManagementSnapshot(
document,
document.defaultView!.location.href,
{ deadline: new Date(Date.now() + 5_000).toISOString() }
);
expect(result.projects.map((project) => project.projectId)).toEqual([
"ongoing_1001",
"ended_1001"
]);
expect(document.querySelector("[role='tab'][aria-selected='true']")?.textContent).toBe("进行中");
});

it("fails closed on structure drift", () => {
expect(() => readBinanceManagementSnapshot(page("<div>页面已改版</div>"))).toThrow("BINANCE_STRUCTURE_UNCONFIRMED");
});

it("blocks login, captcha and risk control", () => {
expect(detectBinanceRiskSignals(page("请输入验证码"))[0]).toMatchObject({ code: "CAPTCHA_REQUIRED", severity: "blocking" });
expect(detectBinanceRiskSignals(page("", "https://www.binance.com/zh-CN/login"))[0]).toMatchObject({ code: "SESSION_EXPIRED" });
});
});
Loading
Loading