本页提供从零到一的任务型示例,帮助你在最短时间内完成一个可运行的插件(包含一个 View 命令和一个 No-View 命令)。
前置:
- 已安装 如快 v0.9.0+
- 已使用脚手架创建工程,并能运行
pnpm dev与pnpm build
任务一:添加一个 View 命令(带 UI)
- 目标:在 UI 中读取/清空搜索框内容、触发截图、并使用插件存储。
- 步骤:
- 在工程中安装依赖:
@sofastapp/api - 在 UI 代码中调用 API(示例基于 React,框架不限)。
- 在工程中安装依赖:
示例 UI 组件:
tsx
import { useEffect, useMemo, useRef, useState } from 'react';
import { Context, Screenshot, LocalStorage } from '@sofastapp/api';
export default function App() {
const sid = useMemo(
() => new URLSearchParams(location.search).get('sid') || '',
[],
);
const [ctx, setCtx] = useState('');
const unref = useRef<null | (() => void)>(null);
const [store, setStore] = useState<Record<string, any>>({});
useEffect(
() => () => {
unref.current?.();
unref.current = null;
},
[],
);
return (
<div style={{ padding: 16 }}>
<h3>My View Command</h3>
<div style={{ opacity: 0.6, fontSize: 12 }}>sessionId: {sid}</div>
<div style={{ marginTop: 12, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button onClick={async () => setCtx(await Context.getSearchContent())}>
读取 Context
</button>
<button
onClick={async () => {
await Context.clearSearchContent();
setCtx('');
}}
>
清空 Context
</button>
<button
onClick={() => {
if (!unref.current)
unref.current = Context.watchSearchContent(setCtx);
else {
unref.current();
unref.current = null;
}
}}
>
{unref.current ? '停止监听' : '开始监听'}
</button>
<button onClick={() => Screenshot.start()}>开始截图</button>
<button
onClick={async () => {
const k = 'counter';
const n = (await LocalStorage.getItem<number>(k)) ?? 0;
await LocalStorage.setItem(k, n + 1);
setStore(await LocalStorage.allItems<Record<string, any>>());
}}
>
计数+1
</button>
<button
onClick={async () => {
await LocalStorage.clear();
setStore({});
}}
>
清空存储
</button>
</div>
<div style={{ marginTop: 12 }}>
当前 Context:<b>{ctx || '(empty)'}</b>
</div>
<pre style={{ marginTop: 12, background: '#fafafa', padding: 8 }}>
{JSON.stringify(store, null, 2)}
</pre>
</div>
);
}在清单中声明该命令(节选):
json
{
"commands": [
{
"name": "my-view",
"title": "My View Command",
"mode": "view",
"searchable": true
}
]
}任务二:添加一个 No-View 命令(无 UI)
- 目标:在后台执行逻辑、输出日志与进度,并使用插件存储。
- 步骤:
- 安装依赖:
@sofastapp/api/node - 新建源文件(如
src/no-view/hello.ts),并通过独立构建配置产出dist/hello.mjs
- 安装依赖:
最小入口:
ts
import {
ctx,
log,
progress,
done,
onError,
LocalStorage,
} from '@sofastapp/api/node';
onError();
(async () => {
const { args } = ctx();
log('hello: start', { args });
progress(0.2);
const key = 'demo.counter';
const n = (await LocalStorage.getItem<number>(key)) ?? 0;
await LocalStorage.setItem(key, n + 1);
progress(1);
done({ ok: true, counter: n + 1 });
})();No-View 构建(自动发现 src/no-view/*.ts):
ts
// vite.worker.config.ts(要点)
import { defineConfig } from 'vite';
import fs from 'node:fs';
import path from 'node:path';
function discoverNoViewInputs() {
const inputs: Record<string, string> = {};
const dir = path.resolve(__dirname, 'src/no-view');
try {
for (const it of fs.readdirSync(dir, { withFileTypes: true }))
if (it.isFile() && it.name.endsWith('.ts'))
inputs[it.name.replace(/\.ts$/, '')] = path.join(dir, it.name);
} catch {}
return inputs;
}
export default defineConfig({
build: {
outDir: 'dist',
emptyOutDir: false,
rollupOptions: {
external: ['worker_threads', /^node:.*/],
input: discoverNoViewInputs(),
output: {
entryFileNames: '[name].mjs',
chunkFileNames: 'assets/[name]-[hash].mjs',
manualChunks: undefined,
},
},
},
});在清单中声明该命令(节选):
json
{
"commands": [
{ "name": "hello", "title": "Hello (No View)", "mode": "no-view" }
]
}任务三:构建与安装
- 在工程根目录执行:
bash
pnpm build构建输出目录需要包含:
- UI 入口与静态资源(如
index.html) package.json(包含顶层commands清单)- No-View 产物(如
hello.mjs)
- UI 入口与静态资源(如
将构建输出目录放置到应用安装目录:
<应用安装目录>/extensions/<your-plugin>
完成后,打开应用,通过命令面板搜索并运行:
- 运行
My View Command体验 UI 能力 - 搜索并运行
Hello (No View)触发后台任务