Skip to content

Latest commit

 

History

History
285 lines (215 loc) · 22.7 KB

plugins.md

File metadata and controls

285 lines (215 loc) · 22.7 KB

Win顺 插件开发

Win顺 的插件给搜索框加关键字命令、把结果混进列表。它们永远以你的普通权限运行(Win顺 自己是管理员,插件不是),崩了、慢了都不拖累主程序。三种写法,一份协议:

写法 要写什么 适合
零代码 只有 plugin.json 关键字开网址、开程序、跳文件夹、保存的搜索、固定清单
一次性脚本(once) 任何语言;查询从环境变量进,结果打印到 stdout 几十行的小工具,偶尔用
常驻(resident) 任何语言;stdio 上一行一条 JSON-RPC 每键都查的、要缓存的、PowerShell(它冷启动慢)

随 Win顺 装好的插件在 <安装目录>\plugins\(默认全部关着,设置 → 插件 里打开):winshun.tools(计算器、单位换算、颜色、时间戳、UUID、Base64、哈希;C++)、winshun.kill(结束进程;PowerShell)、winshun.bookmarks(浏览器书签;Python 一次性)、winshun.translate(翻译;Python 常驻,要自己填 API 密钥)、winshun.semantic(按意思找:? 一句话 按文档讲的内容找文件,用你选的嵌入模型——本机的 Ollama 或云服务的密钥;C++ 常驻,用 winshun:content 拿 Win顺 已经读出的文字)、winshun.web 和 winshun.go(零代码)。每个都是一份能跑的示例,文件夹里有 README。

English version: plugins.en.md.

快速开始

插件 = 一个文件夹,放在 %LOCALAPPDATA%\WinShun\plugins\<id>\,里面一个 plugin.json。最快的路:设置 → 插件 → 高级 → 开发者模式 → 新建插件…,选一种语言,模板连 SDK 文件一起生成好,打开开关就能用;改了脚本会自动重新加载,"消息记录"里能看到每一条往来。

下面三个例子各自五分钟。

1. 零代码:一个关键字开一个网址

%LOCALAPPDATA%\WinShun\plugins\me.npm\plugin.json:

{
  "manifest": 1,
  "id": "me.npm",
  "name": "npm",
  "version": "1.0.0",
  "commands": [
    {"id": "npm", "keyword": "npm", "title": "搜 npm 包", "run": {"url": "https://www.npmjs.com/search?q=%s", "home": "https://www.npmjs.com/"}}
  ]
}

输入 npm lodash,Enter 打开搜索页;只输入 npm (带空格)打开首页。run 还可以是 {"program": "C:\\…\\code.exe", "args": "%s"}、{"folder": "%USERPROFILE%\\Downloads"}、{"search": {"query": "ext:pdf %s", "scope": "files"}}(等于在搜索框里输入它),或 {"items": [{"id": …, "title": …, "open": {"path": …}}]}(固定清单,输入的词用来筛选)。

2. Python 一次性:几行脚本

me.hello\plugin.json:

{
  "manifest": 1,
  "id": "me.hello",
  "name": "Hello",
  "version": "1.0.0",
  "runtime": {"mode": "once", "program": "python", "script": "main.py"},
  "commands": [{"id": "hi", "keyword": "hi", "title": "打招呼", "hint": "名字"}]
}

me.hello\main.py(把 winshun.py 从 src/plugins-sdk/ 或任何随包插件的文件夹复制过来):

import winshun

q = winshun.query()                      # q.text 是关键字后面的文字
winshun.reply([
    winshun.item("hi", f"你好,{q.text}", "按 Enter 复制", copy=q.text),
    winshun.item("len", f"{len(q.text)} 个字", valid=False),
])

每次查询起一个 Python 进程(几十毫秒)。不用 SDK 也行:从环境变量 WS_QUERY 读,往 stdout 打印 {"items": [...]}、一个数组,或者纯文本行(每行一条,Enter 复制)。

3. PowerShell 常驻:一个进程一直开着

me.ps\plugin.json:

{
  "manifest": 1,
  "id": "me.ps",
  "name": "PS",
  "version": "1.0.0",
  "runtime": {"mode": "resident", "program": "powershell", "script": "main.ps1"},
  "commands": [{"id": "ps", "keyword": "ps", "title": "PowerShell"}]
}

me.ps\main.ps1(带 BOM 的 UTF-8;WinShun.psm1 复制到旁边):

Import-Module "$PSScriptRoot\WinShun.psm1"
Start-WinShunPlugin -Name 'me.ps' -OnQuery {
    param($q, $plugin)
    @(New-WinShunItem -Id 'hi' -Title "你好,$($q.text)" -Copy $q.text)
}

第一次用时 PowerShell 要几百毫秒启动,之后每次几毫秒;10 分钟不用自动退出。C++ 的写法见 src/plugins-sdk/winshun.h 开头和 src/tools-plugin/。

SDK

三个文件在仓库的 src/plugins-sdk/,"新建插件…"会复制进插件文件夹;随包插件的文件夹里也有。它们只是把协议包起来,不用也可以。

文件 一次性 常驻
winshun.py query()、settings()、item(...)、reply(items)、is_action() / action_request() / effects(...) serve(on_query, on_action=None, on_settings=None);处理函数收到 (q, plugin),plugin.partial()、plugin.log()、plugin.notify_user()、plugin.progress()、plugin.tool(name, **args)、plugin.settings
WinShun.psm1 Get-WinShunQuery、Get-WinShunSettings、New-WinShunItem、Send-WinShunReply、Send-WinShunEffects Start-WinShunPlugin -OnQuery {…} -OnAction {…}、Send-WinShunPartial、Write-WinShunLog、Send-WinShunNotify、Send-WinShunProgress、Invoke-WinShunTool
winshun.h winshun::onceQuery()、onceSettings()、once(items)、onceAction() / onceEffects() 派生 winshun::Plugin,重写 query()(和 action()、settingsChanged()),serve();成员 partial()、log()、notifyUser()、progress()、tool()、settings()

effects 的辅助:Python winshun.hide() / keep() / notify(text) / copy(text) / set_query(text) / open_path(path) / open_url(url) / replace(items);PowerShell New-WinShunEffect -Hide / -Keep / -Notify / -Copy / -SetQuery / -OpenPath / -OpenUrl / -Replace;C++ winshun::effect::hide() 等。

manifest(plugin.json)字段

顶层:

字段 必需 说明
manifest 是 架构版本,现在是 1;比 Win顺 认识的新就装不上
id 是 作者.名字,只许 [a-z0-9._-],和文件夹名一致
name 是 显示名;字符串,或 {"zh": …, "en": …} 按界面语言取(下同"文字")
version 是 如 1.2.0;同 id 装更低版本时会问
description 否 一句话(文字)
author、homepage、license 否 卡片上显示;homepage 是链接
icon 否 插件文件夹里的 PNG / JPG / ICO(深色主题有 名字.dark.png 就用它;不支持 SVG);没有就用第一个命令的图标
minWinShun 否 低于它不启用
runtime 脚本 / 常驻必需 见下
permissions 否 见"权限"
commands 是 至少一个
settings 否 见"设置项"
ai 否 {"expose": true}:允许命令成为 AI 的工具(用户还要在设置里打开),见"给 AI 用"

runtime:

字段 说明
mode once 或 resident
program python、pwsh、powershell、node,或插件文件夹里的 exe(相对路径)。找不到解释器时卡片上写"需要 Python 3"并给下载链接
script 入口脚本,默认 main.py / main.ps1 / main.js
args 附加参数(数组)
cwd 工作目录,默认插件文件夹
env 额外环境变量(WS_* 不能覆盖)
encoding stdout 的编码:utf-8(默认)或 oem(Windows PowerShell 5.1 不设编码时的输出;模板已经把输出设成 UTF-8,不需要)
timeoutMs 一次查询的上限:常驻默认 1500,一次性默认 3000,最大 10000
debounceMs 一次性插件的防抖,默认 120(每键都起进程太浪费)
idleExitSec 常驻插件空闲多久退出,默认 600;0 不退
start 常驻:lazy(默认,第一次用到时起)或 eager(Win顺 启动 10 秒后预起;推送型插件用)
memoryMB 作业对象的内存上限,默认 512,最大 2048
sandbox normal(默认:中完整性,和你一样)或 strict(低完整性:写不了用户目录,只能写自己的数据文件夹;给不需要写文件的 exe 插件)

commands[]:

字段 说明
id 命令 id,插件内唯一;设置项的键和 AI 工具名用它
title、description、hint 显示名、一句话、关键字模式下输入框的占位文字(都是文字)
icon 见"图标"
keyword 字符串或数组。输入关键字 + 空格进入关键字模式;单独输入关键字时列表第一行是这条命令的提示行(Enter 或 Tab 进入,↓ 到名字结果);输入两个字以上、是关键字开头的(uu 对 uuid),也在应用之后、文件之前多一行(光标不动,按 Tab 就补全成 uuid ;最多两行,候选太多就不出);用户可以在设置里改
triggers 关键字以外的触发方式:{"regex": "…", "minLength": 1, "maxLength": 64}(整个输入匹配时,结果推进"全部";match 是捕获组)、{"push": {"minLength": 2, "maxItems": 3}}(每键都发,常驻 + eager 才合适)
mode list(默认)或 instant(单独输入关键字就执行,不列结果)
actions [{"id": "kill", "title": "结束", "key": "Enter"}, …]:顺序决定 Enter / Ctrl+Enter / Shift+Enter;右键菜单列全部;不写就只有一个 default
run 零代码专用:url(%s 是输入的文字,按网址规则编码;home 是单独输入关键字时开的)、program(%VAR% 会展开;args 里 %s)、folder、search(`{"query": "ext:pdf %s", "scope": "all
cache {"seconds": 60}:同一命令同一文字的结果缓存多久
ai {"description": "…", "parameters": {JSON Schema}}:作为 AI 工具时的描述和参数;不写就用 description 加一个 query 字符串参数

未知字段忽略。校验错误是一句人话("commands[0].keyword 含空格"),设置页原样显示。

协议

一次性(once)

每次查询起一个进程,stdin 关着。

进(环境变量):WS_QUERY(关键字之后的文字)、WS_TEXT(整个输入)、WS_COMMAND、WS_TRIGGER(keyword / regex / push)、WS_KEYWORD、WS_MATCH(正则捕获组,JSON 数组)、WS_PLUGIN_DIR、WS_DATA_DIR(插件可写的数据文件夹 <id>.data)、WS_SETTINGS(JSON)、WS_LOCALE(zh / en)、WS_THEME(light / dark)、WS_VERSION。Enter 落到没有声明式 open 的行时再起一次进程做动作,另有 WS_ACTION(动作 id)和 WS_ITEM(那条 item 的 JSON)。

出(stdout,UTF-8):一个 JSON 对象 {"items": [...], "rerun": 秒, "cache": 秒},或一个数组(就是 items),或纯文本行(每行一条,Enter 复制)。动作时打印 {"effects": [...]},什么都不打印就是收起窗口。stderr 进 plugins.log。退出码非 0 = 失败(底栏提示)。

常驻(resident)

进程一直开着,stdin / stdout 上跑 JSON-RPC 2.0,一行一条,UTF-8。stdout 只许写协议消息,调试信息写 stderr。

Win顺 → 插件:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1","winshun":"0.7.0","pluginDir":"…","dataDir":"…","locale":"zh","theme":"dark","settings":{…}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1","name":"…"}}
{"jsonrpc":"2.0","id":2,"method":"query","params":{"queryId":17,"command":"kill","trigger":"keyword","keyword":"kill","text":"note","raw":"kill note","limit":200}}
←(可选,任意条){"jsonrpc":"2.0","method":"query/partial","params":{"queryId":17,"items":[…],"replace":false}}
← {"jsonrpc":"2.0","id":2,"result":{"items":[…],"rerun":0,"cache":0}}
{"jsonrpc":"2.0","method":"cancel","params":{"queryId":17}}         (新的按键来了;可以不理,Win顺 按 queryId 丢)
{"jsonrpc":"2.0","id":3,"method":"action","params":{"command":"kill","action":"kill","item":{…原样…},"text":"note"}}
← {"jsonrpc":"2.0","id":3,"result":{"effects":[{"type":"notify","text":"已结束 notepad"},{"type":"hide"}]}}
{"jsonrpc":"2.0","method":"settingsChanged","params":{"settings":{…}}}
{"jsonrpc":"2.0","id":5,"method":"shutdown"}  ← {"jsonrpc":"2.0","id":5,"result":{}} 然后退出

插件 → Win顺:

{"jsonrpc":"2.0","id":"p1","method":"winshun/tool","params":{"name":"search_files","arguments":{"query":"*.pdf","limit":20}}}
{"jsonrpc":"2.0","method":"winshun/log","params":{"level":"info","text":"…"}}        进 plugins.log 和开发者模式的记录
{"jsonrpc":"2.0","method":"winshun/notify","params":{"text":"…"}}                   底栏提示
{"jsonrpc":"2.0","method":"winshun/progress","params":{"queryId":17,"text":"正在读书签…"}}   "正在等…"那行的文字

winshun/tool 和 Win顺 给 AI 的 MCP 工具同名同参数:search_files、search_content、recent_files、list_folder、get_file_info、open_item、show_in_folder、clipboard_history、clipboard_get、read_file_text、index_status,另有只给插件的 list_documents(内容索引里有文字的文件,分页,带一个文件变了就变的 stamp);按 manifest 的权限放行(winshun:search、winshun:open、clipboard:read;read_file_text 和 list_documents 要 winshun:content;index_status 总可以)。结果和 MCP 的 structuredContent 一样。

超时:每次查询一个计时器(timeoutMs),到点这次的结果作废;常驻插件连续 3 次超时当挂死,结束后退避重启(1 → 5 → 30 秒);5 分钟内崩 3 次就停用,卡片上红字说明。空闲 idleExitSec 后收到 shutdown。每条响应最多 200 条结果,一行最多 16 MB。

item 字段

{
  "id": "pid-1234",                  // 稳定 id:原位更新、动作的目标;AI 的 _run 工具也靠它
  "title": "notepad.exe",            // 必需
  "subtitle": "PID 1234 · 38 MB",
  "icon": "file:C:\\Windows\\notepad.exe",   // 见"图标"
  "score": 80,                       // 0–100;推送混排时和应用同一个尺度,没给按 50
  "match": "notepad 记事本",          // 零代码 items 用它筛选;别的插件自己筛
  "autocomplete": "kill notepad",    // Tab 填进输入框
  "open": {"path": "C:\\…", "args": "…", "cwd": "…"} 或 {"url": "https://…"},   // 声明式默认动作:Win顺 自己打开(普通权限),不往返;url 写 "winshun:settings" 则打开 Win顺 的设置、定位到这个插件的卡片("还没有填密钥"这类行用)
  "copy": "1234",                    // Ctrl+C 复制什么;没有就 title
  "preview": {"text": "…"},          // 悬停提示
  "actions": [{"id": "kill", "title": "结束", "key": "Enter"}],   // 覆盖命令级的
  "valid": true,                     // false:Enter 无效(提示行)
  "data": {"pid": 1234}              // 不透明,随 action 原样回来;AI 看不到它
}

effects(动作的回答,按顺序执行):hide(收起窗口,默认)、keep(留着)、setQuery {text}、notify {text}、copy {text}、open {path | url}(url 同样认 winshun:settings)、replace {items}(原地换成这些结果,做二级列表)。

图标

icon.png(插件文件夹里的 PNG / JPG / ICO,相对路径;深色主题优先 icon.dark.png)、ext:pdf(扩展名的图标)、file:C:\…(某个文件的图标)、app:<AUMID>、glyph:U+E8E5(Segoe Fluent Icons 字形)、color:#ff8800(色块)、data:image/png;base64,…(≤ 64 KB)、site:https://…(网站自己的图标,和浏览器地址栏左边那个一样:Win顺 只向该网站首页取一次、存在本地、一个月更新;没取到前显示浏览器图标;网址的路径部分不会发出去)。插件文件夹以外的路径不认,SVG 不认。

权限

manifest 的 permissions 是数组:["processes", "clipboard:read", {"files:read": ["%LOCALAPPDATA%\\Google\\Chrome\\User Data\\*\\Bookmarks"]}]。打开开关时逐条列给用户,Windows 能强制的打勾,不能的标"仅声明"——说实话:

权限 含义 Windows 能否强制 怎么做到
(总是)不是管理员 插件永远以你的普通权限运行 是 令牌去掉 Administrators 组和特权;normal 中完整性,strict 低完整性;内存上限、作业对象,Win顺 退出时一起结束
processes 起子进程 是 没有它:作业 ActiveProcessLimit = 1,CreateProcess 报 1816;有它:子进程也在作业里
clipboard:read / clipboard:write 直接读写剪贴板 是 作业的 UI 限制;没有时 GetClipboardData 返回空(读到空或抛异常)
ui 开自己的窗口、找别的窗口 部分 没有时作业限制桌面和 USER 句柄
winshun:search / winshun:open / winshun:history 调 Win顺 的工具 是 winshun/tool 按权限放行
winshun:content 读 Win顺 从文档和文本文件里取出的文字(list_documents、read_file_text) 是 winshun/tool 按权限放行;不用自己解析 Word、PDF,也不用再读一遍硬盘
files:write 写你的文件 只在 strict 下 strict 写不了用户目录,只能写 <id>.data;normal 和你自己一样能写,只是声明
files:read 读哪些文件 否 只是告诉用户它读什么(中完整性本来就能读)
network 联网 否 Win顺 挡不住;卡片上写"Win顺 不能阻止它联网"

只安装你信得过来源的插件。

设置项

settings[]:{"key", "type", "title", "hint", "default", "options"},type ∈ text | password | bool | number | choice | folder | file | hotkey。Win顺 用自己的控件画在卡片上,值存在 WinShun.ini 的 [Plugin.<id>] Setting.<key>=;password 型存 Windows 凭据管理器,不进 INI,不进日志(消息记录里打码)。choice 的 options 是字符串数组或 [{"value": "deepl", "title": {"zh": …, "en": …}}]。

插件看到的是合并后的 JSON(声明的默认值 + 用户的值):一次性在 WS_SETTINGS,常驻在 initialize 的 settings,改了再发 settingsChanged。

给 AI 用

manifest 写 "ai": {"expose": true},并在设置 → AI 助手 打开"AI 可以使用插件"后,每个命令成为 MCP 工具 plugin_<id>_<command>(id 里的点换成下划线:plugin_winshun_tools_calc)。AI 调用 = 一次 query(没运行就起它,等它 initialize),返回 {items: [{id, title, subtitle, url?, path?, copy?}], returned},不含 data;readOnlyHint 为真。命令的 ai.description(英文,给模型看)和 ai.parameters 可以改描述和参数,默认是 {query, limit}。

动作默认不暴露;"允许 AI 执行插件动作"打开后再多一个 plugin_<id>_<command>_run(destructiveHint):AI 先用查询工具拿到行的 id,再把同样的 query 和 item_id(可选 action)交给它,Win顺 重新查一次、找到那行、发 action,回 {done, message, effects}。每个插件卡片上还有"给 AI 用"开关;插件停用时 AI 调用会被告知"插件 X 已停用"。工具列表变了会通知已连接的 AI 客户端。

调试

  • 开发者模式(设置 → 插件 → 高级):改了脚本自动重启常驻插件(底栏"已重新加载 X");"消息记录"记下每一条往来(时间、插件、方向 → ← stderr ·、方法、请求到响应的毫秒),点一条看整个消息;"打开 plugins.log"(%LOCALAPPDATA%\WinShun\plugins.log,插件的 stderr、协议错误、崩溃都在那)。
  • 命令行(源码编译出的 build\release\wsbench.exe,不用开界面):
wsbench --plugin D:\my.plugin --query "hi 你好" --runs 20                 # 校验 manifest、经 runner 起进程、打印结果 JSON 和耗时(冷启动 / 首次往返 / 稳态中位数),以及进程的完整性、是否在作业里
wsbench --plugin D:\my.plugin --query "hi 你好" --setting greeting=嗨    # 给声明的设置项一个值
wsbench --plugin D:\my.plugin --query "kill note" --action kill          # 对第一条结果执行动作,打印 effects
wsbench --launcher-sim --plugins D:\plugins "hi 你好" "#ff8800"          # 按启动器的合并逻辑逐键输入,看行怎么混进列表
wsbench --mcp-host --plugins D:\plugins                                 # 把插件当 AI 工具提供在测试管道上(WinShunMcp.exe --pipe …)
  • 卡片上的状态行:就绪 / 运行中 · 内存 / 超时 n 次 / 崩溃,稍后重启 / 连续崩溃,已停用 / 无法使用:manifest 的错误。

常见问题

  • 中文乱码。 两头都按 UTF-8:Python 加了 -X utf8,自己打印就行;PowerShell 脚本必须存成带 BOM 的 UTF-8(Windows PowerShell 5.1 把没有 BOM 的 .ps1 按 ANSI 读,脚本里的中文全坏),并在开头设 [Console]::OutputEncoding = [Text.Encoding]::UTF8(WinShun.psm1 已经设了)。不想改脚本的老 PowerShell 脚本可以在 manifest 里写 "encoding": "oem",但那只救得了 Latin 字符。
  • 找不到 Python。 Win顺 按注册表 HKCU/HKLM\Software\Python\PythonCore\*\InstallPath 和 PATH 找 python.exe,不用 py.exe(它再起一个子进程,作业不许);Store 版 Python 的 WindowsApps\python.exe 也是转一手,跳过。卡片上写"需要 Python 3"时装一个 python.org 的就行。
  • 超时 / 没有响应。 一次查询的预算是 timeoutMs(常驻 1500、一次性 3000,最大 10000);慢的工作先发 query/partial 或 winshun/progress,大头放后台线程。起进程本身约 15 ms(Defender 的开销),Python 几十毫秒,PowerShell 几百毫秒——所以 PowerShell 用常驻。
  • 常驻插件 print 了没反应。 管道上 stdout 默认是块缓冲:每行后 flush(SDK 都做了;Python 还加了 -u)。
  • Enter 什么都不做。 行有 "valid": false,或者命令没有 actions 而插件的 action 没回。有 open 的行由 Win顺 自己打开,不发 action。
  • 想写文件。 写 WS_DATA_DIR(<id>.data,卸载时问用户要不要一起删);strict 沙箱的插件只能写那里。
  • 图标不显示。 只认 PNG / JPG / ICO,路径要在插件文件夹里;SVG 不支持。
  • 关键字被占。 和设置里"网页搜索"的关键字或别的插件撞了,用户设的赢,卡片上写明;改一个或让用户改。
  • 插件目录能被改吗。 normal 插件就是以你的身份运行,你能改的它都能改——这是开发者模式"改了脚本自动重载"的前提。要防插件改自己,用 strict。