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
747 changes: 275 additions & 472 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[install]
registry = "https://registry.npmjs.org"
28 changes: 28 additions & 0 deletions packages/plugins/jianying-editor/convax-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"schema": "convax.package/1",
"kind": "plugin",
"id": "jianying-editor",
"name": "剪映导入",
"description": "将直接连接的 Canvas 图片和视频安全导入剪映当前草稿或新草稿。",
"version": "2.0.0",
"license": "MIT",
"compatibility": {
"pluginSchema": "convax.plugin/6",
"pluginHost": "convax.plugin-capability/1"
},
"companions": [
{
"command": "convax-jianying-editor-mcp",
"version": "1.0.0",
"source": "packages/tools/jianying-editor-mcp",
"targets": [
{
"platform": "darwin",
"arch": "arm64",
"path": "dist/darwin-arm64/convax-jianying-editor-mcp"
}
]
}
],
"yanked": false
}
15 changes: 15 additions & 0 deletions packages/plugins/jianying-editor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@microvoid/convax-plugin-jianying-editor",
"version": "2.0.0",
"private": true,
"type": "module",
"dependencies": {
"@microvoid/convax-jianying-editor-mcp": "workspace:*",
"@microvoid/convax-skill-jianying-editor": "workspace:*"
},
"scripts": {
"validate": "bun ../../../tooling/validate.mjs --kind plugin --id jianying-editor",
"pack": "bun ../../../tooling/pack.mjs --kind plugin --id jianying-editor",
"test": "bun test"
}
}
7 changes: 7 additions & 0 deletions packages/plugins/jianying-editor/package/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
MIT License

Copyright (c) 2026 Microvoid

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, subject to the conditions of the MIT License.
14 changes: 14 additions & 0 deletions packages/plugins/jianying-editor/package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 剪映导入 for Convax

该插件将直接连接到插件节点的 Canvas 图片和视频导入本机剪映。它不会随
Convax 默认安装;用户需要从 Convax Plugin Registry 主动安装。

插件 iframe 只能看到宿主提供的项目、画布和直接入边投影,不能读取原生
路径或文件字节。点击导入后,Convax 才会把仍然有效的直接连接素材暂存为
短期副本,并把副本交给 Registry 校验过的 `convax-jianying-editor-mcp`
companion。companion 负责 macOS 剪映进程/草稿识别、Deep Link 分发和
loopback 文件传输,不包含 Canvas、Project、IPC 或 Convax Desktop 逻辑。

Agent 工作流由插件拥有的 `jianying-editor` Skill 约束:先检查草稿,当前
草稿存在时必须让用户选择;新草稿只能从安全确认的无活动草稿状态创建。
不确定或部分传输结果不会自动重试,以免重复导入。
10 changes: 10 additions & 0 deletions packages/plugins/jianying-editor/package/UPSTREAM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Provenance

This package is independently authored for Convax from the public
`convax.plugin/6`, `convax.plugin-capability/1`, generation-call, MCP, and JianYing
Deep Link contracts.

The package contains no JianYing application code, credential, browser profile,
private draft database reader, or vendor SDK. The local companion observes only
the running process' open draft lock and transfers host-staged media over a
short-lived loopback server.
145 changes: 145 additions & 0 deletions packages/plugins/jianying-editor/package/assets/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
(() => {
"use strict"

const PROTOCOL = "convax.plugin-capability/1"
const PLUGIN_ID = "jianying-editor"
const pending = new Map()
const elements = {
connection: document.getElementById("connection"),
count: document.getElementById("count"),
empty: document.getElementById("empty"),
export: document.getElementById("export"),
inputs: document.getElementById("inputs"),
inspect: document.getElementById("inspect"),
result: document.getElementById("result"),
scope: document.getElementById("scope"),
}
let inputs = []
let port = null
let sequence = 0
let busy = false

function object(value) {
return value !== null && typeof value === "object" && !Array.isArray(value)
}

function request(method, params) {
if (!port) return Promise.reject(new Error("Convax host is not connected"))
const id = `${PLUGIN_ID}-${++sequence}`
return new Promise((resolve, reject) => {
pending.set(id, { reject, resolve })
port.postMessage({
id,
method,
...(params === undefined ? {} : { params }),
protocol: PROTOCOL,
type: "request",
})
})
}

function receive(event) {
const message = event.data
if (!object(message) || message.protocol !== PROTOCOL) return
if (message.type === "event" && message.event === "canvas.connectedInputs.changed") {
void loadInputs()
return
}
if (message.type !== "response" || typeof message.id !== "string") return
const operation = pending.get(message.id)
if (!operation) return
pending.delete(message.id)
if (message.ok === true) operation.resolve(message.result)
else operation.reject(new Error(typeof message.error === "string" ? message.error : "Host request failed"))
}

function normalizeInput(value) {
if (!object(value)) return null
const id = typeof value.id === "string" ? value.id : value.nodeId
const kind = typeof value.kind === "string" ? value.kind.toLowerCase() : ""
if (typeof id !== "string" || !["image", "video"].includes(kind)) return null
return {
id,
kind,
name: typeof value.name === "string" ? value.name : typeof value.label === "string" ? value.label : id,
role: kind === "image" ? "reference_image" : "reference_video",
}
}

function render() {
elements.count.textContent = String(inputs.length)
elements.empty.hidden = inputs.length > 0
elements.inputs.replaceChildren(...inputs.map((input) => {
const item = document.createElement("li")
const name = document.createElement("span")
const kind = document.createElement("span")
name.textContent = input.name
kind.textContent = input.kind === "image" ? "图片" : "视频"
item.append(name, kind)
return item
}))
elements.inspect.disabled = !port || busy
elements.export.disabled = !port || busy || inputs.length === 0
}

async function loadInputs() {
const result = await request("canvas.connectedInputs.list")
const values = object(result) && Array.isArray(result.inputs) ? result.inputs : Array.isArray(result) ? result : []
inputs = values.map(normalizeInput).filter(Boolean)
render()
}

async function execute(toolId, references = []) {
const result = await request("generation.canvas.execute", {
output: "text",
prompt: toolId === "draft.status" ? "Inspect JianYing draft state" : "Import connected Canvas media into JianYing",
references,
resultMode: "return",
toolId,
})
if (!object(result) || typeof result.outputText !== "string") {
throw new Error("剪映工具没有返回有效结果")
}
return result.outputText
}

async function run(action) {
busy = true
render()
elements.result.textContent = action === "inspect" ? "正在检查剪映…" : "正在交给剪映读取素材…"
try {
elements.result.textContent = action === "inspect"
? await execute("draft.status")
: await execute("media.export", inputs.map((input) => ({ nodeId: input.id, role: input.role })))
} catch (error) {
elements.result.textContent = error instanceof Error ? error.message : String(error)
} finally {
busy = false
render()
}
}

async function initialize() {
const context = await request("host.context.get")
if (!object(context) || !object(context.project) || !object(context.canvas)) {
throw new Error("Convax returned an invalid Canvas context")
}
elements.scope.textContent = `${context.project.name ?? context.project.id} / ${context.canvas.name ?? context.canvas.id}`
elements.connection.textContent = "已连接"
await loadInputs()
}

elements.inspect.addEventListener("click", () => void run("inspect"))
elements.export.addEventListener("click", () => void run("export"))
window.addEventListener("message", (event) => {
if (event.source !== window.parent || event.ports.length !== 1 || port) return
port = event.ports[0]
port.addEventListener("message", receive)
port.start()
void initialize().catch((error) => {
elements.connection.textContent = "连接失败"
elements.result.textContent = error instanceof Error ? error.message : String(error)
render()
})
})
})()
81 changes: 81 additions & 0 deletions packages/plugins/jianying-editor/package/assets/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
:root {
color: #f5f6fb;
background: #111218;
font: 14px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; }
main {
min-height: 100vh;
padding: 18px;
background:
radial-gradient(circle at 90% -10%, rgb(50 226 217 / 18%), transparent 38%),
linear-gradient(145deg, #191b24, #0e0f14);
}
header { display: flex; align-items: center; gap: 11px; }
header > span:nth-child(2) { display: grid; flex: 1; }
header small, section > p, .empty { color: #9da3b4; }
.logo {
display: grid;
width: 42px;
height: 42px;
place-items: center;
border-radius: 13px;
color: #0c1516;
background: linear-gradient(135deg, #65f4e7, #25cbd1);
font-size: 20px;
font-weight: 800;
}
.status, .count {
padding: 4px 9px;
border: 1px solid rgb(255 255 255 / 10%);
border-radius: 999px;
color: #b9c0d1;
background: rgb(255 255 255 / 5%);
font-size: 12px;
}
section {
margin-top: 18px;
padding: 16px;
border: 1px solid rgb(255 255 255 / 8%);
border-radius: 16px;
background: rgb(255 255 255 / 3%);
}
.title-row { display: flex; align-items: center; justify-content: space-between; }
.title-row small { color: #61ded8; font-size: 10px; font-weight: 700; letter-spacing: .12em; }
h1 { margin: 2px 0 0; font-size: 17px; }
section > p { margin: 8px 0 12px; }
ol { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
li {
display: flex;
justify-content: space-between;
padding: 9px 11px;
border-radius: 10px;
background: rgb(255 255 255 / 5%);
}
li span:last-child { color: #8e96a9; }
.empty { margin: 0; text-align: center; }
.actions { display: grid; grid-template-columns: 1fr 1.6fr; gap: 10px; border: 0; padding: 0; background: none; }
button {
min-height: 42px;
border: 0;
border-radius: 11px;
color: inherit;
font: inherit;
font-weight: 700;
cursor: pointer;
}
button:disabled { cursor: not-allowed; opacity: .42; }
.secondary { background: #292c37; }
.primary { color: #071414; background: linear-gradient(135deg, #67f0e5, #2ad0d3); }
output {
display: block;
min-height: 42px;
margin-top: 14px;
padding: 10px 12px;
border-radius: 10px;
color: #b8bfce;
background: rgb(0 0 0 / 18%);
white-space: pre-wrap;
}
34 changes: 34 additions & 0 deletions packages/plugins/jianying-editor/package/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<title>剪映导入</title>
<link rel="stylesheet" href="assets/styles.css">
</head>
<body>
<main>
<header>
<span class="logo" aria-hidden="true">剪</span>
<span><strong>剪映导入</strong><small id="scope">正在连接 Convax…</small></span>
<span class="status" id="connection">等待宿主</span>
</header>
<section>
<div class="title-row">
<span><small>CANVAS INPUTS</small><h1>已连接素材</h1></span>
<strong class="count" id="count">0</strong>
</div>
<p>将图片或视频节点直接连入此节点。只有点击导入后,宿主才会暂存并交给本机剪映。</p>
<ol id="inputs"></ol>
<p class="empty" id="empty">暂无可导入素材。</p>
</section>
<section class="actions">
<button class="secondary" id="inspect" type="button" disabled>检查剪映</button>
<button class="primary" id="export" type="button" disabled>导入已连接素材</button>
</section>
<output id="result">安装插件不会自动启动剪映,也不会上传素材到网络。</output>
</main>
<script src="assets/app.js"></script>
</body>
</html>
Loading