
Quicker Skill
- 2 installs
- 14 repo stars
- Updated March 22, 2026
- luoluoluo22/quicker-skill
quicker-skill is a Claude Code skill for ai & agent building.
About
quicker-skill is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- quicker-skill
- AI & Agent Building
- AI-coding skill
Quicker Skill by the numbers
- 2 all-time installs (skills.sh)
- Ranked #13,956 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/luoluoluo22/quicker-skill --skill quicker-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 22, 2026 |
| Repository | luoluoluo22/quicker-skill ↗ |
How do I helps with ai & agent building tasks during AI-assisted development.?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
Best when you're working on ai & agent building and need structured help with quicker skill.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks during AI-assisted development., or when quicker-skill is a claude code skill for ai & agent building.
What you get
Structured output aligned to quicker-skill: quicker-skill, AI & Agent Building.
Files
📁 技能内置资产 (Assets)
在 quicker-skill/ 下提供了丰富的脚手架工具:
scripts/build_action.ps1:自动调用编译器并解析日志的一键构建脚本。scripts/export_fontawesome_icons.ps1:从本机 Quicker 安装目录导出内置 FontAwesome 图标索引。scripts/search_fontawesome_icons.ps1:按关键词/风格快速搜索 Quicker 内置图标。templates/basic_action.json:带输入/输出的标准快捷动作包裹容器。templates/ui_action.cs:集成了 WPF UI 线程调度、异常捕获的实用 C# 代码骨架。templates/toast_notification_action.cs:动态版本悬浮通知逻辑 (Toast),自动扫描 Quicker Notifier 字段,解决混淆名随版本变动的问题。templates/sync_subprogram_action.cs:安全同步调用 Quicker 内部子程序(如 OCR),防死锁的标准模板代码。references/quicker_fontawesome_icons.csv:Quicker 内置图标索引表,包含前缀、枚举名、token、动作图标写法。references/quicker_fontawesome_icons.md:图标命名规则、搜索方法和已验证图标说明。
Quicker 动作开发技能 (quicker-skill)
目标
协助用户在 Windows 环境下按照 普通模式 v2 (Roslyn) 引擎规范,高效地开发、部署、发布和维护 Quicker 自动化动作。
指令集
1. 开发阶段(零样板架构)
- 文件生成:在当前工作目录下生成三个关键文件,使用相同的基准文件名(BaseName)。
- JSON 配置 (.json):定义
ActionId(留空自动生成),Title,Variables,Icon,Menus,References等。 - C# 逻辑 (.cs):纯逻辑代码,严禁 包含
namespace或class。 - 简介文档 (.md):作为线上简介,建议命名为
基准名_简介.md。 - 变量操作 (IStepContext API):严格仅允许使用以下两个方法:
context.GetVarValue("变量名"):获取变量。context.SetVarValue("变量名", object值):设置变量。- 严禁使用:
LogMsg,LogException,ShowMessage等不存在的方法。 - 入口函数:必须是
public static string Exec(Quicker.Public.IStepContext context)。必须返回字符串(如"OK"或结果)。 - 窗口管理规范:遵循
references/window_guidelines.md中的规范,确保窗口能够成功激活、前置且具备交互完整性。
2. 执行命令(PowerShell)
所有命令均通过“QK 扳手”执行 (ID 见 config.json 中的 wrench_action_id 字段)。 警告:以下命令是与 Quicker 交互的唯一合法协议,严禁重构。
Quicker 构建指令集合
- 本地一键构建 (Build & Verify):
直接调用封装脚本触发构建。该命令是同步的,会直接在终端返回编译结果(成功或报错),一定查看结果后再执行后续步骤 执行 scripts/build.ps1 -JsonPath <你的文件> 即可。
& ".\scripts\build.ps1" -JsonPath "{{JSON绝对路径}}"- 云端发布/更新 (Publish)暂不可用:
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:{{wrench_id}}?action=publish&filePath=$([System.Net.WebUtility]::UrlEncode('{{JSON绝对路径}}'))" | Out-String🏗️ 动作构建核心原则 (Minimalism)
为确保 build 命令成功并自动覆盖逻辑,必须严格遵守以下原则: 1. JSON 极简设计:templates/basic_action.json 中严禁包含 Steps 数组。它仅应定义元数据(ID、变量、图标等)。 2. 同名配对原则:构建时,JSON 的文件名必须与 C# 核心逻辑脚本文件名完全一致且位于同一目录下。例如:MyTask.json 对应 MyTask.cs。
- Quicker 构建器检测到此模式时,会自动将
.cs内容填充到动作主步骤中,无需在 JSON 中声明 Steps。
3. 独立 ID:新动作的 ActionId 必须与 config.json 中的“扳手 ID”区分开(以免修改到扳手本身)。
---
- 更新简介 (Update Docs):
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:{{wrench_id}}?action=update&filePath=$([System.Net.WebUtility]::UrlEncode('{{JSON绝对路径}}'))" | Out-String- 运行动作:
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:{{生成的动作ID}}" | Out-String变量类型代码 (Type)
- 0: 文本 (String)
- 1: 数字 (Double)
- 2: 布尔 (Bool)
- 4: 列表 (List)
- 12: 整数 (Integer)
- 13: 表格 (Table)
示例文件
1. JSON 配置示例 (Demo.json)
{
"ActionId": "",
"SharedActionId": "00000000-0000-0000-0000-000000000000",
"Title": "动作标题",
"Description": "本地动作描述",
"Keywords": "标签1,标签2;标签3",
"ChangeLog": "v1.0.0 初始化版本",
"ShareUrl": "",
"Icon": "fa:Solid_Robot:#0080FF",
"Variables": [
{
"Type": 0,
"Key": "input_var",
"DefaultValue": "默认值",
"IsInput": true,
"Desc": "输入变量说明"
}
],
"Menus": {
"config": "[fa:Regular_Sun]设置"
},
"References": [ "Microsoft.Web.WebView2.Wpf.dll"]
}2. C# 逻辑示例 (Demo.cs)
using System;
using System.Windows;
using Quicker.Public;
// Roslyn v2 零样板模式:直接编写代码,禁止 namespace/class
public static string Exec(IStepContext context)
{
// 获取输入变量
string input = context.GetVarValue("input_var") as string;
// UI 交互必须在 Dispatcher 线程中执行
Application.Current.Dispatcher.Invoke(() => {
MessageBox.Show($"你好!这是来自 Quicker 的输入:{input}", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
});
return "OK";
}🚫 常见错误拨乱反正 (Anti-Hallucination)
在编写 C# 脚本时,AI 经常犯以下错误,必须规避: 1. 虚构 IStepContext 方法:IStepContext 没有 LogMsg, LogException 或 ShowMessage。记录信息请使用 MessageBox 或返回错误字符串。 2. 遗漏返回类型:Exec 的签名必须是 string 而不是 void。 3. 遗漏绝对路径:构建脚本调用时,-JsonPath 必须是绝对路径。 4. 遗漏 UI 线程保护:操作任何 WPF 对象(如 MainWindow, Toast)必须包裹在 Application.Current.Dispatcher.Invoke(() => { ... }) 中。
约束条件
- 环境限制:仅限 Windows 操作系统。
- 代码禁令:C# 文件内绝对禁止出现
namespace或class定义。 - 路径要求:命令中的路径必须是绝对路径,并进行 URL 编码。
- 自动变量:
text,rtn,errMessage,menuKey,silent会由构建器自动注入。 - 内部核心模块 (高级) & 获取选中文本:包含已验证过的 Toast, WindowsToast, SelectOperationWindow 及 GetSelectedText 调用规则,详见
references/internal_modules.md。 - 调用 Quicker 子程序 (推荐):使用官方提供的
IStepContext.RunSpAsync执行外部功能封装,详见references/calling_subprograms.md。 - 窗口唤起规范:详细规则参考
references/window_guidelines.md。 - 云端同步指南:详细同步流程和配置参考
references/cloud_sync_guide.md。 - 发布指南:参考
references/publishing_workflow.md。
{
"wrench_action_id": "3eebe8d9-7521-46fa-b2e1-502754bce14f"
}Kimi 网页转 API (Kimi Relay)
这是一个基于 Edge WebView2 的本地中继动作。它会在后台打开 Kimi 网页,复用网页登录态,把本地 OpenAI 兼容请求转发到网页端,再将网页返回的流式内容回传给客户端。
核心特性:
- 支持本地
http://127.0.0.1:56000/v1/chat/completions - 兼容 SSE 流式输出
- 保留网页登录态,不需要单独维护 cookie
- 提供可视化控制台和托盘菜单
- 支持通过模型名映射“普通对话 / 联网搜索 / 深度思考”
使用方法:
1. 运行本动作。 2. 首次启动时在弹出的 Kimi 网页中手动登录。 3. 看到控制台显示“正在运行”后,在任意 OpenAI Compatible 客户端中配置: Base URL: http://127.0.0.1:56000 API Key: sk-any Model: kimi-k2.5-fast 4. 如需后台启动,可给动作输入参数 silent。 5. 如需停止后台服务,可给动作输入参数 stop 或 shutdown。
可用模型名:
kimi-k2.5-fastkimi-k2.5-fast-searchkimi-k2.5-thinkingkimi-k2.5-thinking-search
注意事项:
- 这是基于现有 DeepSeek 中继动作派生出的 Kimi 试验版本,发送按钮与模式按钮选择器尚未针对 Kimi 页面做专项校准。
- 如果 Kimi 网页 DOM 结构与当前脚本不一致,可能出现“输入成功但未发出”或模式切换失效,需要进一步抓取并适配实际页面结构。
- 需要系统已安装 WebView2 Runtime。
{
"ActionId": "4283635b-ea40-405c-aa82-2f45254eda6a",
"SharedActionId": "00000000-0000-0000-0000-000000000000",
"Title": "Kimi网页转API",
"Description": "通过 WebView2 驱动 Kimi 网页并在本地暴露 OpenAI 兼容接口。",
"Keywords": "kimi,webview2,openai compatible,relay,api",
"ChangeLog": "v0.1.0 初始版本,基于 deepseekWebRelay 派生 Kimi 适配动作。",
"ShareUrl": "",
"Icon": "https://files.getquicker.net/_icons/F61D79E8F06DE37A4D3C8D76A6DE4835E15F0720.png",
"Variables": [
{
"Type": 0,
"Key": "input",
"DefaultValue": "",
"IsInput": true,
"Desc": "启动参数:留空正常启动,silent 后台启动,stop/shutdown 停止服务。"
},
{
"Type": 0,
"Key": "rtn",
"DefaultValue": "",
"IsOutput": false,
"Desc": "返回值(自动)"
},
{
"Type": 0,
"Key": "errMessage",
"DefaultValue": "",
"IsOutput": false,
"Desc": "错误信息(自动)"
},
{
"Type": 0,
"Desc": "结果文本(自动)",
"IsOutput": false,
"Key": "text"
},
{
"Type": 0,
"Desc": "菜单参数(自动)",
"IsOutput": false,
"Key": "menuKey"
},
{
"Type": 2,
"Desc": "静默启动",
"DefaultValue": "true",
"IsInput": false,
"Key": "silent"
}
],
"Menus": {
"config": "[fa:Regular_Sun]设置"
}
}Kimi Web Relay Case
这个案例展示了如何用 quicker-skill 构建一个基于 WebView2 + Quicker + 本地 HTTP 中继 的网页转 API 动作。
包含文件
KimiWebRelay.json:动作配置。KimiWebRelay.cs:核心逻辑,包含 Kimi 页面驱动、模型切换、联网搜索、附件上传、本地 HTTP 兼容层。KimiWebRelay_简介.md:动作简介。test_kimi_relay.ps1:基础单请求验证。verify_kimi_relay.ps1:构建后自动停服、重建、双请求验证。test_kimi_content_matrix.ps1:代码/HTML/SVG/长文本完整性验证。test_kimi_attachments.ps1:文本文件与图片上传验证。
当前能力
- 支持
kimi-k2.5-fast、kimi-k2.5-fast-search、kimi-k2.5-thinking、kimi-k2.5-thinking-search - 端口默认
56000 - 首次请求走网页发送建立模板,后续请求优先走 C# 直发
- 已验证联网搜索菜单的开启和关闭
- 已验证文本、HTML、SVG、长文本完整输出
- 已验证文本文件和图片上传
使用提示
1. 构建前优先请求 /shutdown 停掉旧实例。 2. 用 scripts/build.ps1 构建 KimiWebRelay.json。 3. 构建后动作会自动启动,再用测试脚本做回归。
补充文档
- `最小可复用改造清单.md`:从 Kimi 迁移到其他网页聊天站时的最小改造路线。
param(
[string]$BaseUrl = "http://127.0.0.1:56000",
[string]$Model = "kimi-k2.5-fast",
[int]$TimeoutSec = 180
)
$ErrorActionPreference = "Stop"
$workDir = Join-Path $PSScriptRoot "temp_attachments"
New-Item -ItemType Directory -Path $workDir -Force | Out-Null
$fileToken = "FILE_TOKEN_52481"
$imageToken = "IMG_TOKEN_85294"
$textPath = Join-Path $workDir "relay_test_note.txt"
$imagePath = Join-Path $workDir "relay_test_image.png"
Set-Content -Path $textPath -Value "文本附件口令:$fileToken" -Encoding UTF8
Add-Type -AssemblyName System.Drawing
$bmp = New-Object System.Drawing.Bitmap 900,260
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.Clear([System.Drawing.Color]::White)
$font = New-Object System.Drawing.Font("Microsoft YaHei", 36, [System.Drawing.FontStyle]::Bold)
$brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::Black)
$g.DrawString("图片口令:$imageToken", $font, $brush, 30, 90)
$bmp.Save($imagePath, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose()
$font.Dispose()
$brush.Dispose()
$bmp.Dispose()
$attachments = @(
@{
name = [System.IO.Path]::GetFileName($textPath)
mime_type = "text/plain"
data_base64 = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($textPath))
},
@{
name = [System.IO.Path]::GetFileName($imagePath)
mime_type = "image/png"
data_base64 = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($imagePath))
}
)
$prompt = "请读取我上传的文本文件和图片中的口令,只按如下格式回复:FILE=<文本口令>;IMG=<图片口令>。不要添加其他内容。"
$uri = "$BaseUrl/v1/chat/completions"
$body = @{
model = $Model
stream = $false
messages = @(
@{
role = "user"
content = $prompt
}
)
attachments = $attachments
} | ConvertTo-Json -Depth 12
Write-Host "POST $uri" -ForegroundColor Cyan
Write-Host "Model: $Model"
Write-Host "Text file token: $fileToken"
Write-Host "Image token: $imageToken"
Write-Host ""
try {
$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-ContentType "application/json; charset=utf-8" `
-Headers @{ Authorization = "Bearer sk-any" } `
-Body $body `
-TimeoutSec $TimeoutSec
$content = $response.choices[0].message.content
Write-Host "Response received:" -ForegroundColor Green
$response | ConvertTo-Json -Depth 20
Write-Host ""
Write-Host "Attachment validation:" -ForegroundColor Yellow
Write-Host ("file_token_present={0}" -f ($content -like "*$fileToken*"))
Write-Host ("image_token_present={0}" -f ($content -like "*$imageToken*"))
}
catch {
Write-Host "Request failed:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
if ($_.ErrorDetails.Message) {
Write-Host ""
Write-Host "Server details:" -ForegroundColor Yellow
Write-Host $_.ErrorDetails.Message
}
exit 1
}
param(
[string]$BaseUrl = "http://127.0.0.1:56000",
[string]$Model = "kimi-k2.5-fast"
)
$ErrorActionPreference = "Stop"
$tests = @(
@{
name = "code"
prompt = "请只输出一个 JavaScript 代码块,内容是 add(a,b) 函数并返回 a+b。不要解释。"
},
@{
name = "html"
prompt = "请只输出完整 HTML,包含 doctype、html、head、body,并在 body 里放一个标题 Hello Relay。不要解释。"
},
@{
name = "svg"
prompt = "请只输出一个完整 SVG,画一个蓝色圆和白色文字 OK。不要解释。"
},
@{
name = "longtext"
prompt = '请输出 1 到 20 的编号列表,每行格式为"序号. 测试内容"。不要解释。'
}
)
function Invoke-Test {
param([string]$Prompt)
$uri = "$BaseUrl/v1/chat/completions"
$body = @{
model = $Model
stream = $false
messages = @(
@{
role = "user"
content = $Prompt
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod `
-Uri $uri `
-Method Post `
-ContentType "application/json; charset=utf-8" `
-Headers @{ Authorization = "Bearer sk-any" } `
-Body $body `
-TimeoutSec 120
}
$results = @()
foreach ($test in $tests) {
Write-Host "Running $($test.name)..." -ForegroundColor Cyan
$resp = Invoke-Test -Prompt $test.prompt
$content = $resp.choices[0].message.content
$snippet = if ($content.Length -gt 160) { $content.Substring(0,160) + "..." } else { $content }
$results += [pscustomobject]@{
name = $test.name
length = $content.Length
snippet = $snippet
content = $content
}
}
$results | ConvertTo-Json -Depth 6
param(
[string]$BaseUrl = "http://127.0.0.1:56000",
[string]$Model = "kimi-k2.5-fast",
[string]$Prompt = "请只回复:测试成功",
[int]$TimeoutSec = 90
)
$ErrorActionPreference = "Stop"
$uri = "$BaseUrl/v1/chat/completions"
$body = @{
model = $Model
stream = $false
messages = @(
@{
role = "user"
content = $Prompt
}
)
} | ConvertTo-Json -Depth 10
Write-Host "POST $uri" -ForegroundColor Cyan
Write-Host "Model: $Model"
Write-Host "Prompt: $Prompt"
Write-Host ""
try {
$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-ContentType "application/json; charset=utf-8" `
-Headers @{ Authorization = "Bearer sk-any" } `
-Body $body `
-TimeoutSec $TimeoutSec
Write-Host "Response received:" -ForegroundColor Green
$response | ConvertTo-Json -Depth 20
}
catch {
Write-Host "Request failed:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
if ($_.ErrorDetails.Message) {
Write-Host ""
Write-Host "Server details:" -ForegroundColor Yellow
Write-Host $_.ErrorDetails.Message
}
exit 1
}
param(
[string]$JsonPath = "f:\Desktop\kaifa\deepseek-网页转api\KimiWebRelay.json",
[string]$BaseUrl = "http://127.0.0.1:56000",
[string]$Prompt1 = "请只回复:自动验证第一次",
[string]$Prompt2 = "请只回复:自动验证第二次",
[int]$StartupWaitSec = 45
)
$ErrorActionPreference = "Stop"
$serverLog = "f:\Desktop\kaifa\deepseek-网页转api\服务端.log"
$buildScript = "f:\Desktop\kaifa\deepseek-网页转api\.agent\skills\quicker-skill\scripts\build.ps1"
function Invoke-RelayTest {
param([string]$Prompt)
& "f:\Desktop\kaifa\deepseek-网页转api\test_kimi_relay.ps1" -BaseUrl $BaseUrl -Prompt $Prompt
}
function Wait-RelayUp {
param([int]$TimeoutSec = 25)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while ((Get-Date) -lt $deadline) {
try {
$resp = Invoke-WebRequest -Uri "$BaseUrl/models" -UseBasicParsing -TimeoutSec 3
if ($resp.StatusCode -eq 200) { return $true }
} catch {}
try {
$listener = Get-NetTCPConnection -LocalPort 56000 -State Listen -ErrorAction Stop
if ($listener) { return $true }
} catch {}
Start-Sleep -Seconds 1
}
return $false
}
Write-Host "Preflight: shutdown existing relay over HTTP..." -ForegroundColor Cyan
try {
Invoke-WebRequest -Uri "$BaseUrl/shutdown" -UseBasicParsing -TimeoutSec 3 | Out-Null
} catch {}
Start-Sleep -Seconds 3
if (Test-Path $serverLog) {
Clear-Content $serverLog
}
Write-Host "Building action (build auto-starts relay)..." -ForegroundColor Cyan
& $buildScript -JsonPath $JsonPath | Out-Host
if (-not (Wait-RelayUp -TimeoutSec $StartupWaitSec)) {
throw "Relay did not start within $StartupWaitSec seconds."
}
Write-Host "First request: build template" -ForegroundColor Cyan
$first = Invoke-RelayTest -Prompt $Prompt1
Start-Sleep -Seconds 2
Write-Host "Second request: verify direct request path" -ForegroundColor Cyan
$second = Invoke-RelayTest -Prompt $Prompt2
Start-Sleep -Seconds 2
if (-not (Test-Path $serverLog)) {
throw "Server log not found: $serverLog"
}
$logText = Get-Content $serverLog -Raw
$usedDirect = $logText -match 'Direct request \(C#\): https://www\.kimi\.com/apiv2/kimi\.gateway\.chat\.v1\.ChatService/Chat'
$fallback = $logText -match 'Direct request failed, fallback to UI'
$nativeReq = $logText -match '\[REQ-NATIVE\] 已捕获原始请求体'
$secondResponseOk = $second -match [regex]::Escape($Prompt2.Replace("请只回复:", ""))
Write-Host ""
Write-Host "Verification summary" -ForegroundColor Green
Write-Host "Native request captured: $nativeReq"
Write-Host "Direct request used: $usedDirect"
Write-Host "Fallback happened: $fallback"
Write-Host "Second response matches prompt: $secondResponseOk"
Write-Host ""
Write-Host "Last 80 log lines:" -ForegroundColor Yellow
Get-Content $serverLog -Tail 80
[pscustomobject]@{
native_request_captured = $nativeReq
direct_request_used = $usedDirect
fallback_happened = $fallback
second_response_matches_prompt = $secondResponseOk
first_response = $first
second_response = $second
server_log = $serverLog
} | ConvertTo-Json -Depth 6
网页转 API 最小可复用改造清单
这份清单的目标不是“完整重写一个新站点适配器”,而是帮助你从现有的 KimiWebRelay 快速迁移到另一个网页聊天站,并尽量保留已经验证过的稳定链路。
1. 先判断哪些部分可以复用
优先复用的部分:
- 本地 HTTP 兼容层
- OpenAI 风格
/v1/chat/completions输出 - SSE chunk 封装
- WebView2 容器、托盘、日志、自动构建脚本
- 首次网页发送、后续直发的双路径架构
- 自动化验证脚本结构
通常必须重做的部分:
- 目标站点 URL
- 登录态判断
- 输入框定位
- 发送按钮定位
- 模型切换逻辑
- 联网开关逻辑
- 文件/图片上传逻辑
- 真实聊天接口的请求体和响应帧解析
2. 第一步只做“能发”
迁移时不要一开始就攻克直发。
建议顺序: 1. 先把页面打开并确认登录态正常。 2. 只实现“把纯文本输入到网页并成功发送”。 3. 只拦截真实聊天请求,不处理别的埋点请求。 4. 先把响应文本稳定回传。 5. 最后再做模型切换、联网开关、附件上传、直发优化。
判断标准:
- 右侧日志能看到发送动作。
- WebView 中用户消息气泡只出现一次。
- 本地接口能收到非空回答。
3. 先锁定这五类选择器
迁移一个新站点时,优先找这五类 DOM:
- 模型选择入口
- 模型菜单项
- 主输入框
- 发送按钮
- 工具箱入口(搜索、上传等)
如果是分层菜单,再额外找:
- 一级菜单项
- 二级弹层容器
- 二级选项项
经验规则:
- 先用稳定 class 或结构选择器。
- 文本匹配只做兜底。
- 每一步动作之间都要留足等待时间。
4. 网络拦截只盯真实聊天接口
不要用“包含 chat/completion 就拦”的宽松规则直接上线。
迁移时先确认:
- 域名
- 路径
- 请求方法
- 返回
Content-Type
推荐做法:
- JS 侧只拦目标聊天接口
- 原生
WebResourceResponseReceived也只看同一个接口 - 把埋点、列表、订阅、标题更新全部排除
否则很容易出现:
- 提前
FinishRequest() - 返回空字符串
- 被别的接口污染模板
5. 先搞清响应协议,再决定怎么解析
常见网页聊天响应形式:
text/event-streamapplication/connect+json- 普通 JSON 分片
- XHR 累积文本
迁移时优先做两件事: 1. 把原始响应流完整打到日志 2. 找出真正的文本增量字段
要重点看:
- 文本块字段名
- thinking/reasoning 字段
- done/finish 结束标记
- 引用来源字段
- 错误块字段
6. 直发路径不要太早做
推荐的稳定路径仍然是:
- 第一次请求:网页发送,抓模板
- 后续请求:C# 直发
只有确认下面四项都稳定后,再启用直发:
- 已捕获真实请求 URL
- 已捕获真实请求头
- 已捕获真实请求体
- 已确认请求体替换 prompt 后仍合法
如果站点使用了带帧头协议,例如 connect+json,必须确认:
- 替换文本后是否要重算帧长
- 是否需要保留 flags/header bytes
7. 模型切换与联网开关要分开验证
不要一次同时改模型和搜索开关然后一起测。
建议分四步: 1. 默认模型,搜索关闭 2. 默认模型,搜索开启 3. 思考模型,搜索关闭 4. 思考模型,搜索开启
每一步都要检查:
- 页面日志是否真的点击到了目标项
- 原始请求体里是否真的反映了该状态
判断成功的标准不是“页面看起来变了”,而是:
- 请求体字段发生了正确变化
8. 附件上传优先走 UI,不要先攻直发
文件和图片上传的最稳方案通常是:
- 找到隐藏
input[type=file] - 构造
File/DataTransfer - 触发
input和change - 等待上传完成后再发送
验证标准:
- 发送请求体里出现文件块
- 文件状态变成成功
- 模型能真正读到附件中的内容
9. 自动化验证至少保留三套脚本
建议每个网页转 API 案例都保留:
test_xxx_relay.ps1
作用:单次基本请求验证
verify_xxx_relay.ps1
作用:停服务、构建、启动、双请求、判定是否走直发
test_xxx_content_matrix.ps1
作用:代码、HTML、SVG、长文本完整性验证
如果支持附件,再加:
test_xxx_attachments.ps1
10. 迁移时最值得保留的日志
建议长期保留这些日志:
- 页面初始化完成
- 发送动作开始/结束
- 模型切换动作
- 搜索开关动作
- 原始请求体长度
- 原始请求体关键片段
- 响应
Content-Type - 文本增量块
- 结束标记
- 直发启用/回退原因
这些日志基本足够支撑后续所有站点迁移。
11. 一份最小迁移流程
可以直接按这个顺序做: 1. 复制 KimiWebRelay 为新站点基线。 2. 改站点 URL、标题、图标、端口、模型展示名。 3. 只修输入框和发送按钮。 4. 把聊天接口拦截范围收窄到真实接口。 5. 打通文本响应解析。 6. 做模型切换。 7. 做联网开关。 8. 做附件上传。 9. 最后再做直发。
12. 当前 Kimi 案例里最适合直接抄的部分
最值得直接复用的是:
- 本地接口包装
- 日志体系
- 构建后自动验证脚本
- 首次网页发送 + 后续直发的分层策略
connect+json的处理思路- 工具箱二级菜单的串行点击时序
- 附件通过隐藏文件输入框注入的做法
最不应该盲抄的是:
- 具体 class 选择器
- 模型名称
- 搜索菜单文本
- 请求体字段名
- 响应增量字段名
这些一定要按目标站点重新确认。
Quicker-Skill: AI 驱动的动作极速开发套件
本工具包通过 Antigravity AI 技能 (Skill) 与 QK 扳手 (Quicker 动作) 的深度联动,实现“对话即代码,部署即运行”的自动化开发体验。
---
🛠️ 安装与配置指南
第一步:安装 QK 扳手 (Quicker 端)
这是本技能在 Quicker 面板中的“执行端”,负责接收 AI 指令并进行本地构建。
- 动作名称:QK 扳手
- 安装地址:点击安装集成助手
- 注意:请确保 Quicker 客户端处于运行状态。
第二步:安装 quicker-skill (AI 技能端)
1. 下载技能包:从本仓库下载 .agent/skills/quicker-skill 目录。 2. 放置位置:将该目录复制到你当前 Antigravity 工作区的 .agent/skills/ 路径下。
- 路径示例:
F:\YourProject\.agent\skills\quicker-skill\
3. 验证安装:在对话中输入 @quicker-skill 确认 AI 已成功识别技能。
---
📖 核心使用场景与对话示例
1. 新建动作
你可以直接描述你想要的功能,AI 会自动为你生成图标、配置变量并调用 QK 扳手。
- 用户:“帮我写一个 QK 动作,功能是读取剪贴板内容,把所有的空格替换成换行。”
- AI:(生成
Replacement.json和Replacement.cs) -> (调用 QK 扳手自动本地构建)
2. 本地测试与迭代
在本地构建后,你可以要求 AI 进行微调。
- 用户:“把刚才的动作图标换成一个蓝色的搜索图标,并增加一个可以自定义替换字符的输入变量。”
- AI:(更新 JSON 配置) -> (重新推送构建)
3. 文档生成与云端发布
当动作开发完成后,AI 可以帮你整理简介并一键推送到 Quicker 动作库。
- 用户:“这个动作很好用,帮我写个痛点切入式的简介,并发布上线。”
- AI:(生成
_简介.md) -> (调用 QK 扳手发布/更新分享)
4. 获取帮助
- 用户:“我要怎么在 C# 代码里获取 Quicker 的变量?”
- AI:(基于 Skill 中的
context.GetVarValue指南回答你的问题)
---
� 技术规格
- 引擎版本:Quicker 普通模式 v2 (Roslyn)。
- 代码架构:零样板模式(Zero-Boilerplate),禁止
namespace和class。 - 自动化操作:通过
QuickerStarter.exe命令行参数实现静默构建。
--- 欲了解更详细的 C# 开发规范,请阅读同目录下的 `SKILL.md`。
🧩 新模板
Web Relay Action
模板目录中新增了网页转 API 脚手架:
- `templates/web_relay_action.json`
- `templates/web_relay_action.cs`
适用场景:
- 把网页聊天站封装为本地 OpenAI 兼容接口
- 需要
WebView2 + 本地 HTTP 中继 + 响应流转发 - 需要从 Kimi、DeepSeek、Qwen、Gemini 等网页站点迁移时快速起步
这个模板只保留稳定骨架:
- 启停入口
- 托盘与窗口管理
- WebView2 容器
- 本地
/v1/chat/completions兼容层 - SSE 输出骨架
- DOM 注入脚本入口
- 网络拦截脚本入口
目标站点相关部分需要你自行替换:
- 站点 URL
- 输入框与发送按钮选择器
- 模型切换
- 联网开关
- 附件上传
- 真实响应协议解析
C# 调用 Quicker 子程序指南
在开发 Quicker 代码动作时,当某些高级功能(例如内部 OCR、文件对话框、提示框等内部模块)难以通过反射直接调用(常因为上下文不匹配或缺少内部变量支持)时,最佳实践是将这部分功能封装为一个子程序(SubProgram),然后在 C# 代码中直接调用该子程序。
这样可以在享受 C# 代码高度控制力的同时,复用 Quicker 现成且稳定的模块化组件。
调用语法
Quicker 提供了 IStepContext.RunSpAsync API 用于在代码中调用子程序。
核心代码模板
此方法支持 async Task 或 async Task<string> 签名(如果要返回给动作结果):
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using Quicker.Public;
// 引擎支持异步 Task 签名
public static async Task<string> Exec(IStepContext context)
{
// 1. 组装传给子程序的数据 (字典的 Key 需要与子程序的“输入变量名”完全一致)
var inputs = new Dictionary<string, object>
{
{ "input1", "你好" },
{ "imgKey", @"C:\path\to\image.png" }
};
try
{
// 2. 调用子程序 (填入实际的子程序名称或网络子程序ID)
// 注意:因为是异步操作,必须用 await
var result = await context.RunSpAsync("你的子程序名称", inputs);
// 3. 处理子程序的输出结果
// "resultKey" 改成子程序里配置的“输出变量名”
if (result != null && result.ContainsKey("resultKey"))
{
var value = result["resultKey"]?.ToString();
return "成功获取到结果: " + value;
}
return "调用完成,但未找到预期的输出变量";
}
catch (Exception ex)
{
return "调用失败: " + ex.Message;
}
}注意事项
1. 变量名称对齐 字典的 Key 必须对应子程序里配置的局部输入变量的名称。返回结果字典的 Key 必须对应子程序的输出变量名称。
2. 异步方法死锁预防 (核心难点) context.RunSpAsync 是异步的。如果你的 Exec 函数由于框架原因必须是同步返回 string(没有 async Task 修饰),并且你需要等待子程序的结果,绝对不能直接使用 `.GetAwaiter().GetResult()`!这会直接阻塞 WPF 主线程,导致子程序抛出结果时无法被处理,引发死锁(界面卡死,程序不退出)。
纯同步代码中的完美解决方案:使用 DispatcherFrame 嵌套消息循环
using System.Windows.Threading;
// ... 在 Exec() 中 ...
var task = context.RunSpAsync("子程序名称", inputs);
// 开启局部消息泵,让出 UI 线程控制权,但代码依旧会“停留”在这里等待
var frame = new DispatcherFrame();
task.ContinueWith(t => frame.Continue = false);
Dispatcher.PushFrame(frame);
// 此时再获取结果,安全无死锁
var result = task.GetAwaiter().GetResult();你可以参考 templates/sync_subprogram_action.cs 的完整模板。
3. 子程序名称或 ID RunSpAsync 的第一个参数可以是本地的子程序名称(直接手写的中文名/英文名),也可以是网络共享子程序的 ID。推荐将其作为子程序的独立步骤建立好,然后在 C# 中根据需要灵活调度。
Quicker 云同步开发指南
本指南总结了在 Quicker 动作(Roslyn v2 模式)中实现自定义数据云端备份与恢复的最佳实践。
一、 核心架构
Quicker 并没有对 Roslyn 脚本直接公开 CloudState 对象,因此我们采用 动态反射 的方式调用 Quicker 内部的异步保存/读取方法。
1. 通用辅助类 (CloudStateHelper)
将以下代码放入你的 .cs 文件末尾。它封装了反射逻辑,并提供了简单的静态接口。
public static class CloudStateHelper {
private static Type _t; private static object _i; private static MethodInfo _s, _r;
static CloudStateHelper() { Init(); }
static void Init() {
if (_i != null) return;
foreach(var a in AppDomain.CurrentDomain.GetAssemblies()){
_t = a.GetType("Quicker.Public.CloudState"); if(_t != null) break;
}
if(_t == null) return;
var f = _t.GetField("OWJCWqTrDY4Wyj5Oc7t3", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
if(f != null) _i = f.GetValue(null);
if(_i == null) { _i = Activator.CreateInstance(_t, true); if(f != null) try{ f.SetValue(null, _i); } catch{} }
_s = _t.GetMethod("SaveTextAsync", new[]{ typeof(string), typeof(string), typeof(double) });
_r = _t.GetMethod("ReadTextAsync", new[]{ typeof(string), typeof(double) });
}
// 保存数据:k=Key, v=Value, e=过期分钟
public static bool SaveText(string k, string v, double e) {
if(_i == null) Init(); if(_s == null) return false;
return Task.Run(async() => { try{ await(Task)_s.Invoke(_i, new object[]{k, v ?? "", e}); return true; } catch{ return false; } }).Result;
}
// 读取数据
public static string ReadText(string k, double e) {
if(_i == null) Init(); if(_r == null) return null;
return Task.Run(async() => { try{ var t = (Task)_r.Invoke(_i, new object[]{k, e}); await t; return t.GetType().GetProperty("Result")?.GetValue(t) as string; } catch{ return null; } }).Result;
}
}二、 备份流程 (Backup)
备份建议采用 JSON 序列化,并包含元数据以方便后期识别。
示例代码
private void BackupToCloud() {
// 1. 序列化数据(包含元数据)
var data = new {
Content = MyDataList,
Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
Machine = Environment.MachineName
};
string json = JsonConvert.SerializeObject(data);
// 2. 异步保存
Task.Run(() => {
bool success = CloudStateHelper.SaveText("MyAction_Backup", json, 43200); // 30天有效期
Dispatcher.Invoke(() => {
if (success) MessageBox.Show("备份成功!");
else MessageBox.Show("失败:云端未响应");
});
});
}三、 恢复流程 (Restore)
严禁静默恢复。恢复操作必须从云端拉取数据后,展示给用户进行二次确认。
1. 安全规范
- 预览数据:展示备份的时间和设备名。
- 手动勾选:允许用户选择部分恢复或全部恢复。
- 二次确认:在覆盖本地数据前弹出警告。
2. 示例逻辑
private void RestoreFromCloud() {
Task.Run(() => {
string json = CloudStateHelper.ReadText("MyAction_Backup", 43200);
Dispatcher.Invoke(() => {
if (string.IsNullOrEmpty(json)) {
MessageBox.Show("云端没有发现备份。");
return;
}
// 解析并弹出确认窗口
var data = JsonConvert.DeserializeObject<dynamic>(json);
var restoreWin = new MySyncWindow(data); // 自定义勾选窗口
if (restoreWin.ShowDialog() == true) {
// 执行真正的写入逻辑
ApplyData(restoreWin.SelectedItems);
}
});
});
}四、 避坑指南
1. 数据碰撞:Key 名称必须具备极高辨识度,建议格式:{动作名}_{数据类型}。 2. 过期时间:SaveText 的第三个参数是分钟,如果不希望频繁清理,建议设置较大的值(如 43200 为 30 天)。 3. 线程安全:云端操作必须放在 Task.Run 中,否则会由于网络延迟导致 Quicker 界面彻底卡死。 4. Dispatcher:所有涉及 UI 更新(包括弹出恢复窗口)的操作都必须回到主线程。
Quicker 内部模块与参数解析 (进阶规范)
1. 内部交互模块 (反射调用)
当 IStepContext 的标准 API 不足时,可通过深度反射调用 Quicker 内部模块。
1.1 悬浮 Toast (Success/Info/Warn/Error)
- 核心库:
ToastNotifications.dll - 实例定位:
Application.Current.MainWindow的混淆字段(当前版本为l6sHHaS3IZJ)。 - 完整示例:
public static string Exec(IStepContext context) {
Application.Current.Dispatcher.Invoke(() => {
var mw = Application.Current.MainWindow;
var notifierField = mw.GetType().GetField("l6sHHaS3IZJ", BindingFlags.Instance | BindingFlags.NonPublic);
var notifier = notifierField.GetValue(mw);
var extType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.First(t => t.FullName == "ToastNotifications.Messages.SuccessExtensions");
// 调用 ShowSuccess(notifier, message)
extType.GetMethod("ShowSuccess", new[] { notifier.GetType(), typeof(string) })
.Invoke(null, new object[] { notifier, "操作成功!" });
});
return "OK";
}1.2 Windows 10+ 系统通知 (WindowsToast)
- 实现位置:
Quicker.Utilities.AppHelper.ShowWindowsToastMessage(静态方法)。 - verified 签名:
ShowWindowsToastMessage(string message, string title, Action onClick, int? expirationMs, string icon) - 完整示例:
public static string Exec(IStepContext context) {
Application.Current.Dispatcher.Invoke(() => {
var appHelper = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.First(t => t.FullName == "Quicker.Utilities.AppHelper");
// 匹配 5 个参数的签名
var method = appHelper.GetMethod("ShowWindowsToastMessage",
new[] { typeof(string), typeof(string), typeof(Action), typeof(int?), typeof(string) });
method.Invoke(null, new object[] { "消息内容", "标题", null, 5000, null });
});
return "OK";
}1.3 用户选择弹窗 (SelectOperationWindow)
- 核心类:
Quicker.View.SelectOperationWindow - 数据模型:
Quicker.View.SimpleOperationItem - 构造签名:
ctor(IList<SimpleOperationItem> operations, ShowWindowLocation location, bool useKeyboard, bool isMultiSelect, bool showFilter, double fontSize) - 示例代码:
Application.Current.Dispatcher.Invoke(() => {
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
Type itemType = assemblies.SelectMany(a => a.GetTypes()).First(t => t.FullName == "Quicker.View.SimpleOperationItem");
Type selectWinType = assemblies.SelectMany(a => a.GetTypes()).First(t => t.FullName == "Quicker.View.SelectOperationWindow");
Type locType = assemblies.SelectMany(a => a.GetTypes()).First(t => t.FullName == "Quicker.Domain.ShowWindowLocation");
// 创建选项列表
var itemsList = Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType));
var addMethod = itemsList.GetType().GetMethod("Add");
var item = Activator.CreateInstance(itemType);
itemType.GetProperty("Name").SetValue(item, "[fa:Solid_Check:#28A745] 选项一");
addMethod.Invoke(itemsList, new[] { item });
// 弹出窗口
var win = Activator.CreateInstance(selectWinType, new[] { itemsList, Enum.Parse(locType, "WithMouse1"), true, false, true, 14.0 });
if ((bool)selectWinType.GetMethod("ShowDialog").Invoke(win, null)) {
var selected = selectWinType.GetProperty("SelectedItem").GetValue(win);
string name = itemType.GetProperty("Name").GetValue(selected) as string;
}
});1.4 获取选中文本 (GetSelectedText)
- 实现位置:
Quicker.Utilities.AppHelper.GetSelectedText(静态方法)。 - 完整示例:
public static string Exec(IStepContext context) {
string text = "";
Application.Current.Dispatcher.Invoke(() => {
var appHelper = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.First(t => t.FullName == "Quicker.Utilities.AppHelper");
var method = appHelper.GetMethod("GetSelectedText",
new[] { typeof(long), typeof(TextDataFormat), typeof(int), typeof(bool) });
text = (string)method.Invoke(null, new object[] { 0L, TextDataFormat.UnicodeText, 250, false });
});
return text;
}---
2. 运行时参数解析 (quicker_in_param)
URL 启动模式 (runaction:ActionID?key=val) 的参数合并在 quicker_in_param 变量中。
2.1 推荐解析算法
string inParam = context.GetVarValue("quicker_in_param") as string;
if (!string.IsNullOrEmpty(inParam)) {
foreach (var part in inParam.Split('&')) {
var pair = part.Split('=');
if (pair.Length == 2) {
string key = pair[0].ToLower().Trim();
string val = System.Net.WebUtility.UrlDecode(pair[1]);
// 赋值逻辑...
}
}
}很好,我们准备发布上去,准备动作名称(6字以内)、简介、关键词(;分割)、以用户第一视角痛点切入(比如有时我在使用xx软件时,总被xx困扰,希望能快速解决问题,所以开发了这个动作,让我非常有效率)、使用说明,写入当前目录下`
Quicker 动作开发指南 (Roslyn v2 专修版)
角色定位
你是 Quicker(Windows 自动化工具)的专业开发者。你遵循"零样板"架构,专注于 普通模式 v2 (Roslyn) 引擎。 目标:生成精简的配置清单(.json)和纯逻辑的 C# 代码文件(.cs),然后编译并立即测试动作。
---
一、文件规范
你必须在工作区生成 三个文件。它们必须使用完全相同的基准文件名(BaseName)。
文件 A:动作配置清单 (.json)
此文件定义元数据、变量和菜单。
| 字段 | 说明 |
|---|---|
ActionId | 本地动作 ID(InternalId)。保持为空 "" 会自动生成。 |
SharedActionId | 云端动作 ID。首次发布后自动填入,用于后续更新。 |
Title | 动作标题 |
Description | 动作(本地)描述。仅在本地显示。 |
Keywords | (新) 搜索关键词/标签。仅用于发布。 |
ChangeLog | (新) 版本更新说明。仅用于发布更新。 |
ShareUrl | 分享链接(自动回填) |
Icon | 图标,格式:fa:图标名:#颜色(例:fa:Solid_Robot:#0080FF) |
Variables | 定义动作变量。关键字段:Type(类型代码), Key(变量名), DefaultValue(默认值), IsInput(作为参数输入), SaveState(记忆上次值) |
Menus | (可选) 定义右键菜单。格式: { "Key": "标题" }。构建器会自动生成 menuKey 变量供 C# 读取。 |
References | (可选) 额外的 DLL 引用列表。格式: ["xxx.dll", "yyy.dll"]。构建器会自动生成 //css_reference 指令。 |
文件 B:C# 逻辑文件 (.cs)
(见下文代码规则)
文件 C:动作简介文档 (.md)
这是一个 Markdown 文件,用于作为动作的 线上简介。
- 文件名约定:
基准名_简介.md(推荐) 或基准名.md。 - 此文件内容不会影响本地动作运行,仅在执行
update(更新简介) 命令时被读取并上传。
变量类型对照表 (Type)
| 代码 | 类型 (Type) | 说明 |
|---|---|---|
| 0 | 文本 (String) | 最常用。默认值填字符串。 |
| 1 | 数字 (Double) | 浮点数/双精度小数。 |
| 2 | 布尔 (Bool) | true 或 false。 |
| 3 | 图片 (Image) | |
| 4 | 列表 (List) | 每行一个元素。 |
| 6 | 日期时间 (DateTime) | |
| 10 | 词典 (Dict) | 键值对,格式 key:value。 |
| 12 | 整数 (Integer) | 纯整数。 |
| 13 | 表格 (Table) | |
| 99 | 动态对象 (Object) |
自动注入变量 (无需手动定义)
构建器会自动注入以下标准变量,请直接在 C# 中使用:
text(String): 结果文本rtn(String): 返回值errMessage(String): 错误信息menuKey(String): 菜单点击的 Key (对应 Menus 中的 Key)silent(Bool): 静默启动 (默认 true)
JSON 示例
{
"ActionId": "",
"SharedActionId": "00000000-0000-0000-0000-000000000000",
"Title": "简单弹窗演示",
"Description": "这是一个由 AI 生成的简单弹窗示例动作。",
"Keywords": "演示,弹窗,示例",
"ChangeLog": "v1.0.0 初始化版本",
"ShareUrl": "https://getquicker.net/SharedAction?code=...",
"Icon": "fa:Solid_CommentDots:#FF8000",
"Variables": [{
"Type": 0,
"Desc": "结果文本(自动)",
"IsOutput": false,
"DefaultValue": null,
"Key": "text"
}],
"Menus": {
"test_menu": "[fa:Light_Flag]菜单标题(tooltip内容)"
},
"References": [ "Microsoft.Web.WebView2.Wpf.dll"]
}---
文件 B:C# 逻辑文件 (.cs)
核心规则:零样板(Zero-Boilerplate)
- 【绝对禁令】:文件内严禁包含
namespace或class定义。 - 【Roslyn 优势】:支持 C# 7.0+ 语法(如 Lambda、元组、模式匹配等)。
1. 入口函数签名
必须为 public static 且方法名为 Exec:
- 无返回值:
public static void Exec(Quicker.Public.IStepContext context) - 有返回值:
public static [类型] Exec(Quicker.Public.IStepContext context)
2. 多线程建议
| 场景 | 推荐线程 |
|---|---|
| 纯数据/文件处理 | 后台线程 (MTA) - 默认推荐 |
| 剪贴板/COM/简单 Winform | 后台线程 (STA) |
| WPF 复杂界面/ShowDialog() | UI 线程/前台线程 |
3. 依赖引用
- 第三方 DLL:
//css_reference DLL路径; - 系统库:直接
using
4. context 核心方法
context.GetVarValue("变量名")→ 返回objectcontext.SetVarValue("变量名", 值)→ 写回动作变量
C# 示例 1:基础弹窗
using System;
using System.Windows;
using Quicker.Public;
public static void Exec(IStepContext context)
{
string rtn = context.GetVarValue("rtn") as string ?? "";
MessageBox.Show("你好!这是来自 Quicker 的弹窗!", "提示", MessageBoxButton.OK, MessageBoxImage.Information, MessageBoxResult.OK, MessageBoxOptions.DefaultDesktopOnly);
rtn = "Success";
context.SetVarValue("rtn", rtn);
}C# 示例 2:Linq 与现代语法
using System;
using System.Linq;
using Quicker.Public;
public static void Exec(IStepContext context)
{
string rtn = context.GetVarValue("rtn") as string ?? "";
var list = new[] { 1, 2, 3, 4, 5 };
var result = list.Where(x => x > 2).Sum();
rtn = $"计算结果: {result}";
context.SetVarValue("rtn", rtn);
}C# 示例 3:带返回值(STA 线程场景)
using System;
using System.Windows;
using Quicker.Public;
// 涉及剪贴板时,请在动作设置中选择 STA 线程
public static string Exec(IStepContext context)
{
string text = Clipboard.GetText();
return "剪贴板长度: " + text.Length;
}
#### C# 示例 4:处理菜单事件using System; using System.Windows.Forms; using Quicker.Public;
public static void Exec(IStepContext context) { // 从自动变量 menuKey 获取点击的菜单键 string key = context.GetVarValue("menuKey") as string;
if (key == "config") { MessageBox.Show("点击了设置菜单"); } else { MessageBox.Show("正常运行"); } }
---
二、执行命令
元动作构建器 ID:见 config.json 中的 wrench_action_id 字段(不可更改)。
参数格式
所有命令使用统一的参数格式:action=xxx&filePath=yyy&shareUrl=zzz
| action | 功能 | 必需参数 |
|---|---|---|
build | 构建动作(本地部署) | filePath (JSON配置路径) |
publish | 首次发布到云端 | filePath (JSON配置路径) |
update | 更新线上简介 | 可选: shareUrl + filePath (MD路径) <br> 或 filePath (JSON配置路径, 智能推断) |
read | 读取线上简介 | shareUrl |
构建命令(本地部署)
直接调用封装的 build.ps1 脚本,避免路径拼接繁杂并简化命令结构。 执行 scripts/build.ps1 -JsonPath <你的文件> 即可。
& ".\scripts\build.ps1" -JsonPath "{{你的JSON文件路径}}"发布/更新动作命令 (Publish)
一键发布:首次运行为新建分享,后续运行为更新分享(基于 SharedActionId)。
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:{{wrench_id}}?action=publish&filePath=$([System.Net.WebUtility]::UrlEncode('{{你的JSON文件路径}}'))" | Out-String更新动作简介命令 (Update Docs)
智能推断:只需提供 JSON 路径,构建器会自动: 1. 从 JSON 中读取 ShareUrl。 2. 自动寻找同名 MD 文件(优先级:文件名_简介.md > 文件名.md)。
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:{{wrench_id}}?action=update&filePath=$([System.Net.WebUtility]::UrlEncode('{{你的JSON文件路径}}'))" | Out-String(也可以显式提供 `shareUrl` 参数,如旧版命令)
读取动作简介命令 (Read)
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:3eebe8d9-7521-46fa-b2e1-502754bce14f?action=read&shareUrl=$([System.Net.WebUtility]::UrlEncode('{{动作分享后的url}}'))" | Out-String运行已构建动作
& "C:\Program Files\Quicker\QuickerStarter.exe" -c120 "runaction:{{生成的动作ID}}" | Out-String---
四、高级技巧与避坑指南
1. 环境变量与注册表操作
- 秒级保存:直接操作注册表 (
Microsoft.Win32.Registry) 比调用Environment.SetEnvironmentVariable性能高得多,因为后者会强制阻塞式向全系统发送刷新广播。 - 系统通知同步:操作完注册表后,必须异步发送
WM_SETTINGCHANGE广播(使用user32.dll的SendMessageTimeout),否则资源管理器或其他第三方程序无法识别环境变化。 - 变量名规范:尽量使用 全大写 变量名。部分系统和脚本对全小写环境变量支持较差,可能导致识别失败。
- 环境继承陷阱:子进程会继承父进程(Quicker)启动时的环境快照。如果在动作运行期间修改了系统变量,直接启动子进程可能拿不到新值。测试时应开启全新的命令行窗口。
2. 异步操作与 UI 响应
- 防 UI 冻结:长耗时操作(如注册表密集写、网络备份/恢复)必须包裹在
Task.Run中执行。 - 加载反馈:异步任务期间建议显示 Loading 遮罩。
- Dispatcher 调用:在异步任务中修改 ObservableCollection 或更新 UI 控件属性,必须使用
Dispatcher.Invoke回到主线程。
3. UI 交互提示 (Toast vs MessageBox)
- 稳定性优先:在复杂的 WPF 窗口逻辑中,
context.ShowToast或全局静态App.ShowToast可能会因为作用域、SDK 版本或依赖冲突导致编译失败(如CS0103或CS1061)。 - 稳健回退:如果 Toast 报错,最稳健的方案是使用 WPF 原生的
MessageBox.Show。 - 交互优化:简单的“复制成功”提示建议使用 Toast 以保持流程连贯;涉及“警告/确认”或“操作指引”时建议使用 MessageBox。
4. 依赖项管理
- Json 解析:Quicker 默认携带
Newtonsoft.Json,无需额外手工引用 DLL。 - 动态反射:如果需要调用 Quicker 内部未暴露的私有 API(如
CloudState),可使用动态反射机制。
5. 云同步开发技巧
- 数据隔离:建议在云端存储时使用特定的 Key 前缀(如
ActionName_Backup),避免数据碰撞。 - 版本兼容性:存储数据时建议携带时间戳和机器名字段,方便在多设备同步时进行冲突判断。
- 安全保护:云同步操作通常涉及用户数据隐私。在执行“恢复”操作前,必须弹出二次确认窗口(如下面演示的
SyncRestoreWindow),让用户手动勾选需要覆盖的项,防止意外丢失本地配置。 - 原子性:建议先将所有待恢复变量在内存中处理完毕后,再一次性写入注册表,减少系统广播次数。
延伸阅读:关于反射调用云端接口的底层细节与完整代码示例,请参阅专用文档:云同步专研指南
Quicker 内置图标说明
本地已解析 Quicker 安装目录中的图标程序集:
C:\Program Files\Quicker\FontAwesomeIconsWpf.dll
Quicker 当前内置图标基于:
FontAwesome5.EFontAwesomeIconFontAwesome5.EFontAwesomeStyle
可用风格:
SolidRegularLightBrands
图标索引文件:
.codex/skills/quicker-skill/references/quicker_fontawesome_icons.csv
辅助脚本:
- 导出/刷新索引:
.codex/skills/quicker-skill/scripts/export_fontawesome_icons.ps1- 搜索图标:
.codex/skills/quicker-skill/scripts/search_fontawesome_icons.ps1
搜索示例:
powershell -ExecutionPolicy Bypass -File .\.codex\skills\quicker-skill\scripts\search_fontawesome_icons.ps1 barcodepowershell -ExecutionPolicy Bypass -File .\.codex\skills\quicker-skill\scripts\search_fontawesome_icons.ps1 info -Style Solidpowershell -ExecutionPolicy Bypass -File .\.codex\skills\quicker-skill\scripts\search_fontawesome_icons.ps1 Solid_BarcodeAlt -Exact
命名规则
Quicker 动作 JSON 中的图标写法:
[fa:Solid_Play][fa:Solid_InfoCircle][fa:Regular_Sun]
动作主图标可写为:
"fa:Solid_Server:#2D8CFF"
已确认存在的示例
Solid_BarcodeAltSolid_PlaySolid_StopSolid_InfoCircleSolid_FileAltSolid_ServerSolid_PaperPlaneRegular_Sun
已确认不存在的示例
Solid_CircleInfoSolid_FileLinesRegular_FileLines
本次修正
FeishuListenerHost.json 已改为使用这些已确认存在的图标:
start→Solid_Playstop→Solid_Stopstatus→Solid_InfoCircleshow_log→Solid_FileAlt
Category,Title,Namespace/Type,Method/Property,Call Type,Description
UI,悬浮 Toast 提示,ToastNotifications.Notifier,ShowSuccess/ShowError/etc,实例 + 扩展方法,在屏幕指定位置弹出美观的悬浮提示框
UI,Windows 系统通知,Quicker.Utilities.AppHelper,ShowWindowsToastMessage,静态方法,触发 Windows 10+ 操作中心的通知
UI,用户选择弹窗,Quicker.View.SelectOperationWindow,ctor/ShowDialog,构造实例化,弹出标准的单选/多选列表对话框
Data,选项数据模型,Quicker.View.SimpleOperationItem,Name/Key/Icon,数据项实例化,为 SelectOperationWindow 提供行数据支持
System,获取选中文本,Quicker.Utilities.AppHelper,GetSelectedText,静态方法,高可靠性抓取当前光标选中的文字
System,窗口定位枚举,Quicker.Domain.ShowWindowLocation,WithMouse1/CenterScreen/etc,枚举常量,控制弹窗出现的起始位置
UI,颜色选择器,Quicker.View.ColorSelectorWindow,ShowDialog,构造实例化,弹出标准的颜色选取调色盘
UI,图标选择器 (Fa),Quicker.View.FaIconSelectorWindow,ShowDialog,构造实例化,弹出 FontAwesome 图标搜索和选择窗口
UI,程序/文件选择器,Quicker.View.AppSelectorWindow,ShowDialog,构造实例化,选择已安装的 App 或本地文件
UI,屏幕坐标拾取,Quicker.View.ScreenPointSelectWindow,ShowDialog,构造实例化,让用户在全屏范围内点击拾取坐标点
UI,托盘气泡提示,Quicker.Utilities.UI.NotifyIconWrapper,ShowMessage,实例方法,在系统托盘图标上方弹出传统气泡消息
UI,快捷键输入器,Quicker.View.KeyInput.KeySelectorWindow,ShowDialog,构造实例化,捕获用户录入的快捷键组合
UI,进程/程序选取器,Quicker.Modules.TextTools.Tools.ProfileExeSelectWindow,ShowDialog,构造实例化,用于选择当前运行的进程或程序路径
UI,蓝牙设备选择器,Quicker.Modules.TextTools.Tools.BluetoothDeviceSelectorWindow,ShowDialog,构造实例化,扫描并选择附近的蓝牙设备
Logic,App 全局服务器,Quicker.Domain.AppServer,ShowSearchWindow/ShowConfigWindow,实例方法,控制 Quicker 主搜索框和设置页面的显示
Logic,动作编辑器管理,Quicker.Domain.Services.ActionEditMgr,ShowEditor,实例方法,通过反射直接唤起特定动作的编辑窗口
Quicker 反射探索与逆向经验总结
本文件记录了在 Quicker 混淆环境下的深度反射探索过程及成功经验,为后续探索其他内部模块提供参考。
1. 核心挑战:混淆 (Obfuscation)
Quicker 的内部私有成员字段名(如 l6sHHaS3IZJ)随版本变化且无语义。
- 应对策略:
- 避开名称,依赖类型: 使用
val.GetType().FullName进行匹配。 - 程序集扫描: 自研脚本遍历
AppDomain.CurrentDomain.GetAssemblies(),寻找特定关键词(如 "Toast" / "Notifier")的程序集和类型。 - IsInstanceOfType: 在
MainWindow或App实例的所有字段中,利用notifierType.IsInstanceOfType(val)寻找活体实例。
2. 签名陷阱:委托工厂 (Delegate Factory)
许多现代 C# 库使用 Func<T> 委托或表达式树来延迟执行。
- 经验: 不要仅搜索
Show(string, string)这种简单参数的方法。如果反射调用失败,检查参数是否为Func<INotification>这种工厂模式。 - 解决方案: 在反射环境下构建泛型委托极其复杂(易报 ContainsGenericParameters 错误)。最优解是直接利用相关库的静态扩展方法 (Extension Methods),它们签名固定且不包含复杂的运行时泛型解析。
3. 环境与线程约束
- UI 线程同步: 绝大多数反射操作(尤其是访问
MainWindow或触发 UI 组件)必须运行在Application.Current.Dispatcher线程中。 - 构建结果验证: Quicker 构建是异步的。必须捕获
QuickerStarter.exe的输出流,硬核扫描error关键字,而非依赖进程退出码。
4. 探索工作流模板 (The Hunt Workflow)
1. 定位类型: 寻找目标功能相关的程序集和基类/接口。 2. 定位实例: 扫描核心全局对象(App.Current, MainWindow)的私有成员。 3. 获取成员信息: 通过反射遍历可疑对象的方法名和参数类型,利用 MessageBox 或 Exec 返回值输出信息。 4. 验证调用: 优先寻找静态扩展方法进行功能验证。
5. 关键发现记录
- ToastNotifications 系统: 实例隐藏在
MainWindow的随机字段中,需配合ToastNotifications.Messages下的扩展类调用。 - 系统通知系统: 位于
Quicker.Utilities.AppHelper静态类的ShowWindowsToastMessage。 - 启动参数获取: 通过 URL 启动时,参数需从
quicker_in_param中通过字符串拆分和UrlDecode手动提取。
Quicker 窗口开发规范
为了确保 Quicker 动作在启动时具有良好的用户体验,必须遵循以下窗口管理规范:
1. 窗口唤起与焦点管理 (Focus & Activation)
核心目标:窗口在运行后必须立即显示在所有普通窗口的最前面,并获得键盘焦点,严禁出现“运行后窗口躲在后台”的情况。
推荐实现代码 (C#)
不要仅使用 win.Show() 或 win.ShowDialog(),应当使用以下组合拳确保窗口成功激活:
public static string Exec(IStepContext context)
{
Application.Current.Dispatcher.Invoke(() =>
{
var win = new YourWindow();
// 1. 设置为最顶层以破开其他窗口的遮挡
win.Topmost = true;
win.Show();
// 2. 激活窗口以获得系统级输入焦点
win.Activate();
// 3. 立即取消置顶(避免遮挡用户后续操作,实现“正常打开”)
win.Topmost = false;
// 4. 显式获取焦点
win.Focus();
});
return "OK";
}2. 交互完整性 (Interactive Integrity)
- 必备关闭按钮:所有自定义 UI 窗口(尤其是
WindowStyle="None"的无边框窗口)必须在右上角显式提供关闭按钮。 - 拖动支持:无边框窗口必须实现标题栏(或顶部区域)的
DragMove()逻辑。 - 防止置底:严禁将窗口初始化为后台状态或最小化状态(除非动作特殊要求)。
3. 兼容性约束 (Compatibility)
- 避免扩展方法定义冲突:在 Quicker Roslyn 环境中,尽量避免定义
public static class Extensions以防止与内置库冲突。推荐使用显式类型转换(Button)border.FindName("name")。 - UI 线程安全:所有 UI 操作必须包装在
Application.Current.Dispatcher.Invoke中。
param(
[Parameter(Mandatory=$true)]
[string]$JsonPath,
[string]$WrenchId = ""
)
# 统一控制台编码为 UTF-8,避免中文输出乱码
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $Utf8NoBom
[Console]::OutputEncoding = $Utf8NoBom
$OutputEncoding = $Utf8NoBom
if ([string]::IsNullOrWhiteSpace($WrenchId)) {
# 尝试读取同级或上级目录的 config.json
$ConfigPath = Join-Path -Path $PSScriptRoot -ChildPath "..\config.json"
if (Test-Path $ConfigPath) {
try {
$WrenchId = (Get-Content $ConfigPath -ErrorAction Stop | ConvertFrom-Json).wrench_action_id
} catch { }
}
}
# 兜底默认扳手 ID
if ([string]::IsNullOrWhiteSpace($WrenchId)) {
$WrenchId = "3eebe8d9-7521-46fa-b2e1-502754bce14f"
}
# 将目标 json 转为绝对路径,并进行 URL 编码
$ResolvedPath = Convert-Path $JsonPath
$EncodedPath = [System.Net.WebUtility]::UrlEncode($ResolvedPath)
# 构建动作参数(不回显完整命令,避免噪音)
$CmdArgs = "runaction:${WrenchId}?action=build&filePath=${EncodedPath}"
Write-Host "正在调用 QuickerWrench ..." -ForegroundColor Cyan
# 执行构建调用并捕获输出
$rawOutput = & "C:\Program Files\Quicker\QuickerStarter.exe" -c120 $CmdArgs 2>&1 | Out-String
# 统一输出风格:错误红色、成功绿色
if ($LASTEXITCODE -ne 0 -or $rawOutput -match "❌|错误|编译失败|error\s+[A-Z]+\d+") {
Write-Host ""
Write-Host "构建失败" -ForegroundColor Red
Write-Host "--------------------------------" -ForegroundColor DarkRed
$errorLines = $rawOutput -split "`r?`n" | Where-Object {
$_ -match "❌|错误|编译失败|error\s+[A-Z]+\d+"
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Host $_ -ForegroundColor Red }
} else {
Write-Host ($rawOutput.Trim()) -ForegroundColor Red
}
exit 1
} else {
Write-Host "构建完成" -ForegroundColor Green
$resultText = $rawOutput.Trim()
if (-not [string]::IsNullOrWhiteSpace($resultText)) {
Write-Host "动作执行结果:" -ForegroundColor Cyan
Write-Host $resultText
}
}
param(
[string]$QuickerDir = "C:\Program Files\Quicker"
)
$dllPath = Join-Path $QuickerDir "FontAwesomeIconsWpf.dll"
if (-not (Test-Path $dllPath)) {
Write-Error "未找到图标程序集: $dllPath"
exit 1
}
$outPath = Join-Path $PSScriptRoot "..\references\quicker_fontawesome_icons.csv"
$asm = [Reflection.Assembly]::LoadFrom($dllPath)
$iconEnum = $asm.GetType("FontAwesome5.EFontAwesomeIcon")
if ($null -eq $iconEnum) {
Write-Error "未找到枚举类型 FontAwesome5.EFontAwesomeIcon"
exit 1
}
[Enum]::GetNames($iconEnum) |
Where-Object { $_ -ne "None" } |
ForEach-Object {
$parts = $_ -split "_", 2
$style = if ($parts.Length -ge 1) { $parts[0] } else { "" }
$name = if ($parts.Length -ge 2) { $parts[1] } else { $_ }
[pscustomobject]@{
enum_name = $_
style = $style
prefix = "fa:$style"
icon_name = $name
token = "[fa:$_]"
action_icon = "fa:$($_):#2D8CFF"
search_text = (($_ + " " + $style + " " + $name) -replace "_"," ")
}
} |
Export-Csv -Path $outPath -NoTypeInformation -Encoding UTF8
Write-Host "已导出: $outPath"
param(
[Parameter(Position=0)]
[string]$Keyword = "",
[string]$Style = "",
[switch]$Exact,
[int]$Top = 50
)
$csvPath = Join-Path $PSScriptRoot "..\references\quicker_fontawesome_icons.csv"
if (-not (Test-Path $csvPath)) {
Write-Error "图标索引不存在: $csvPath"
exit 1
}
$rows = Import-Csv $csvPath
if (-not [string]::IsNullOrWhiteSpace($Style)) {
$rows = $rows | Where-Object { $_.style -ieq $Style }
}
if (-not [string]::IsNullOrWhiteSpace($Keyword)) {
if ($Exact) {
$rows = $rows | Where-Object {
$_.enum_name -ieq $Keyword -or
$_.icon_name -ieq $Keyword -or
$_.token -ieq $Keyword
}
}
else {
$parts = $Keyword -split '[\s,_-]+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
foreach ($part in $parts) {
$p = [Regex]::Escape($part)
$rows = $rows | Where-Object {
$_.enum_name -match $p -or
$_.icon_name -match $p -or
$_.search_text -match $p
}
}
}
}
$rows |
Select-Object -First $Top enum_name, style, icon_name, token, action_icon |
Format-Table -AutoSize
{
"ActionId": "",
"SharedActionId": "00000000-0000-0000-0000-000000000000",
"Title": "空白动作模板",
"Description": "基于 C# 脚本的高级动作模板",
"Keywords": "template",
"ChangeLog": "v1.0.0 初始化",
"ShareUrl": "",
"Icon": "fa:Solid_Wrench:#FF4000",
"Variables": [
{
"Key": "quicker_in_param",
"Type": 0,
"DefaultValue": "",
"IsInput": true,
"Desc": "URL传入参数"
},
{
"Key": "rtn",
"Type": 0,
"DefaultValue": "",
"IsOutput": false,
"Desc": "返回值(自动)"
},
{
"Key": "errMessage",
"Type": 0,
"DefaultValue": "",
"IsOutput": false,
"Desc": "错误信息(自动)"
}
]
}using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Threading;
using Quicker.Public;
using System.Threading.Tasks;
// 这是一个使用 WPF DispatcherFrame 同步等待子程序(不卡死 UI)的标准化模板
public static string Exec(IStepContext context)
{
// 输入变量示例(可选项:将本地变量或者从动作外传入的变量传给子程序)
// var imagePath = context.GetVarValue("imagePath") as string;
try
{
// 1. 组装输入给子程序的变量映射
var inputs = new Dictionary<string, object>
{
// 例如给子程序的 "imagePath" 变量赋值
{ "imagePath", @"C:\test.png" },
};
// 2. 发起对子程序的异步调用 (但不马上 Wait,避免死锁)
var task = context.RunSpAsync("MyOcrSubProgram", inputs);
// 【核心操作】利用 DispatcherFrame 启动局部消息处理泵,安全地在这个线程同步等待
// 这个手段能确保虽然下方代码暂停了执行,但 WPF 的内部组件事件能继续流动,从而让子程序能够正常完毕
var frame = new DispatcherFrame();
task.ContinueWith(t =>
{
// 当子程序异步执行完毕后,结束局部消息泵,释放当前线程
frame.Continue = false;
});
Dispatcher.PushFrame(frame);
// 3. 此时获取结果,非常安全且 100% 已经是执行完毕的状态
var result = task.GetAwaiter().GetResult();
// 4. 解析输出
if (result != null && result.ContainsKey("text"))
{
var ocrText = result["text"]?.ToString();
// 可以把结果写回当前动作的同名输出变量 (要求该动作配置里包含 IsOutput: true 的 text 变量)
context.SetVarValue("text", ocrText);
return ocrText;
}
return "OK (未获取到预期结果文本)";
}
catch (Exception ex)
{
return "ERROR: " + ex.Message;
}
}
using System;
using System.Linq;
using System.Reflection;
using System.Windows;
using Quicker.Public;
// Roslyn v2 动态悬浮通知模板
// 优势:自动扫描 MainWindow 字段,兼容不同版本的 Quicker 混淆名
public static string Exec(IStepContext context)
{
// 默认通过变量 message 获取内容,如果没有定义则使用 text 变量
string message = (context.GetVarValue("message") as string)
?? (context.GetVarValue("text") as string)
?? "通知内容为空";
Application.Current.Dispatcher.Invoke(() =>
{
try
{
var mw = Application.Current.MainWindow;
if (mw == null) return;
// 1. 动态定位 Notifier 实例
object notifier = mw.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
.FirstOrDefault(f => f.FieldType.FullName?.Contains("ToastNotifications.Notifier") == true)
?.GetValue(mw);
if (notifier == null) throw new Exception("无法在当前版本 Quicker 中定位 Notifier。");
// 2. 定位扩展方法类 SuccessExtensions (此处也可以根据需要替换为 WarningExtensions/ErrorExtensions)
var extType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => { try { return a.GetTypes(); } catch { return Type.EmptyTypes; } })
.FirstOrDefault(t => t.FullName == "ToastNotifications.Messages.SuccessExtensions");
if (extType == null) throw new Exception("未找到 Toast 消息扩展库。");
// 3. 执行弹出
var method = extType.GetMethod("ShowSuccess", new[] { notifier.GetType(), typeof(string) });
method?.Invoke(null, new object[] { notifier, message });
}
catch (Exception ex)
{
// 降级方案:MessageBox
MessageBox.Show(message, "Quicker 通知 (Toast 调用失败)", MessageBoxButton.OK, MessageBoxImage.Information);
}
});
return "OK";
}
{
"ActionId": "",
"SharedActionId": "00000000-0000-0000-0000-000000000000",
"Title": "悬浮通知助手",
"Description": "演示如何在悬浮通知中显示一段自定义信息。",
"Keywords": "通知,Toast",
"Icon": "fa:Regular_Bell:#FFD700",
"Variables": [
{
"Type": 0,
"Key": "message",
"DefaultValue": "任务处理已成功!",
"IsInput": true,
"Desc": "展示的通知内容"
}
],
"Menus": {
"config": "[fa:Regular_Sun]设置"
},
"References": []
}using System;
using System.Windows;
using System.Reflection;
using System.Linq;
using Quicker.Public;
using System.Collections.Generic;
using System.Text;
using System.Threading;
// C# 脚本入口方法,必须返回 string,接受 IStepContext 参数
public static string Exec(IStepContext context)
{
try
{
// 核心 1:所有操作建议包裹在 Dispatcher.Invoke 中处理 UI 线程冲突
Application.Current.Dispatcher.Invoke(() =>
{
// 在此处编写你的业务逻辑
// ...
});
return "OK";
}
catch (Exception ex)
{
// 返回错误信息供 Quicker 获取
return "ERROR: " + ex.Message;
}
}
//css_ref System.Windows.Forms.dll
//css_ref System.Drawing.dll
//css_ref Microsoft.Web.WebView2.Core.dll
//css_ref System.Runtime.Serialization.dll
//css_ref System.Xml.dll
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Drawing;
using Microsoft.Web.WebView2.Core;
public static string Exec(Quicker.Public.IStepContext context)
{
if (System.Threading.Thread.CurrentThread.GetApartmentState() != System.Threading.ApartmentState.STA)
{
MessageBox.Show("配置错误:请将模块执行线程设置为 STA 独立线程。", "Web Relay");
return "ERROR";
}
string inputParam = "";
string menuKeyParam = "";
try { inputParam = context.GetVarValue("input") as string; } catch { }
try { menuKeyParam = context.GetVarValue("menuKey") as string; } catch { }
if (string.IsNullOrWhiteSpace(inputParam))
{
try
{
string quickerInParam = context.GetVarValue("quicker_in_param") as string;
if (!string.IsNullOrWhiteSpace(quickerInParam))
{
foreach (var part in quickerInParam.Split('&'))
{
var pair = part.Split(new[] { '=' }, 2);
if (pair.Length != 2) continue;
if (string.Equals(pair[0], "input", StringComparison.OrdinalIgnoreCase))
inputParam = WebUtility.UrlDecode(pair[1]);
else if (string.Equals(pair[0], "menuKey", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(menuKeyParam))
menuKeyParam = WebUtility.UrlDecode(pair[1]);
}
}
}
catch { }
}
inputParam = inputParam?.Trim().ToLower() ?? "";
menuKeyParam = menuKeyParam?.Trim().ToLower() ?? "";
string startupMode = !string.IsNullOrWhiteSpace(inputParam) ? inputParam : menuKeyParam;
if (startupMode == "shutdown" || startupMode == "stop")
{
WebRelayForm.ShutdownRemoteService();
return "OK";
}
bool isSilent = startupMode == "silent";
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new WebRelayForm(isSilent));
return "OK";
}
public class WebRelayForm : Form
{
private const string TargetUrl = "https://example.com/chat";
private const int ServerPort = 57000;
private const string RelayTitle = "Web Relay";
private readonly bool _isDebugMode;
private readonly string _userDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "WebRelay_Data");
private CoreWebView2Environment _env;
private CoreWebView2Controller _controller;
private CoreWebView2 _coreWebView;
private HttpListener _httpListener;
private HttpListenerContext _currentContext;
private NotifyIcon _notifyIcon;
private TextBox _txtLog;
private Panel _webPanel;
private bool _isInitializing;
private bool _isCurrentRequestStreaming;
private StringBuilder _nonStreamResponseBuffer = new StringBuilder();
public WebRelayForm(bool isSilent)
{
_isDebugMode = !isSilent;
InitializeComponent();
InitializeSystemTray();
this.Load += async (s, e) => {
await InitializeWebViewSequenceAsync();
};
StartHttpServer();
}
public static void ShutdownRemoteService()
{
try
{
var request = WebRequest.Create("http://127.0.0.1:" + ServerPort + "/shutdown");
request.Timeout = 2000;
using (var response = request.GetResponse()) { }
}
catch { }
}
private void InitializeComponent()
{
this.Text = RelayTitle + " (Port: " + ServerPort + ")";
this.Size = new Size(1200, 800);
this.StartPosition = FormStartPosition.CenterScreen;
_webPanel = new Panel();
_webPanel.Dock = DockStyle.Fill;
this.Controls.Add(_webPanel);
_txtLog = new TextBox();
_txtLog.Multiline = true;
_txtLog.Dock = DockStyle.Right;
_txtLog.Width = 360;
_txtLog.ScrollBars = ScrollBars.Vertical;
_txtLog.ReadOnly = true;
this.Controls.Add(_txtLog);
}
private void InitializeSystemTray()
{
_notifyIcon = new NotifyIcon();
_notifyIcon.Text = RelayTitle;
_notifyIcon.Icon = SystemIcons.Application;
_notifyIcon.Visible = true;
}
private async Task InitializeWebViewSequenceAsync()
{
if (_isInitializing) return;
_isInitializing = true;
try
{
_env = await CoreWebView2Environment.CreateAsync(null, _userDataFolder);
_controller = await _env.CreateCoreWebView2ControllerAsync(_webPanel.Handle);
_coreWebView = _controller.CoreWebView2;
_controller.Bounds = _webPanel.ClientRectangle;
await _coreWebView.AddScriptToExecuteOnDocumentCreatedAsync(GetNetworkInterceptorScript());
_coreWebView.WebMessageReceived += OnWebMessageReceived;
_coreWebView.Navigate(TargetUrl);
}
finally
{
_isInitializing = false;
}
}
private void StartHttpServer()
{
try
{
_httpListener = new HttpListener();
_httpListener.Prefixes.Add("http://127.0.0.1:" + ServerPort + "/");
_httpListener.Start();
Task.Run(ListenLoop);
Log("HTTP 服务监听中: " + ServerPort);
}
catch (Exception ex)
{
Log("HTTP 启动失败: " + ex.Message);
}
}
private async Task ListenLoop()
{
var listener = _httpListener;
if (listener == null) return;
while (listener.IsListening)
{
try
{
var ctx = await listener.GetContextAsync();
HandleRequest(ctx);
}
catch { break; }
}
}
private async void HandleRequest(HttpListenerContext ctx)
{
string path = ctx.Request.Url.AbsolutePath.ToLower();
string method = ctx.Request.HttpMethod.ToUpper();
ctx.Response.AppendHeader("Access-Control-Allow-Origin", "*");
ctx.Response.AppendHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (method == "OPTIONS") { ctx.Response.StatusCode = 200; ctx.Response.Close(); return; }
if (path == "/shutdown")
{
ResponseText(ctx, "Closing...", 200, "text/plain");
this.Invoke(new Action(() => this.Close()));
return;
}
if (path == "/v1/models" && method == "GET")
{
ResponseText(ctx, "{\"object\":\"list\",\"data\":[]}", 200, "application/json");
return;
}
if (path == "/v1/chat/completions" && method == "POST")
{
using (var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
{
string body = reader.ReadToEnd();
string prompt = ExtractPromptFromJson(body);
if (string.IsNullOrWhiteSpace(prompt))
{
ResponseText(ctx, "{\"error\":\"Parse Error\"}", 400, "application/json");
return;
}
_currentContext = ctx;
_isCurrentRequestStreaming = body.Contains("\"stream\":true") || body.Contains("\"stream\": true");
_nonStreamResponseBuffer.Clear();
this.Invoke(new Action(() => InjectPromptToWebView(prompt)));
}
return;
}
ResponseText(ctx, "Not Found", 404, "text/plain");
}
private string ExtractPromptFromJson(string json)
{
try
{
var contentRegex = new Regex("\"content\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
var lastMatch = contentRegex.Matches(json).Cast<Match>().LastOrDefault();
return lastMatch != null ? UnescapeJson(lastMatch.Groups[1].Value) : null;
}
catch { return null; }
}
private void InjectPromptToWebView(string prompt)
{
string promptBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(prompt ?? ""));
string script = "window.WebRelayBridge.sendPromptFromBase64(\"" + promptBase64 + "\")";
try { _coreWebView.ExecuteScriptAsync(script); } catch { }
}
private void OnWebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
try
{
string msg = e.TryGetWebMessageAsString();
if (msg.StartsWith("[LOG]")) { Log(msg); return; }
if (_currentContext == null) return;
if (msg.StartsWith("[NETWORK_DONE]")) { FinishRequest(); return; }
if (msg.StartsWith("[NETWORK_DATA]")) { SendDelta(msg.Substring(14)); }
}
catch (Exception ex) { Log("MsgError: " + ex.Message); }
}
private void SendDelta(string text)
{
if (!_isCurrentRequestStreaming)
_nonStreamResponseBuffer.Append(text);
if (_isCurrentRequestStreaming)
{
string jsonChunk = "{\"id\":\"chatcmpl-webrelay\",\"object\":\"chat.completion.chunk\",\"created\":" + DateTimeOffset.Now.ToUnixTimeSeconds() + ",\"model\":\"web-relay\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + EscapeJson(text) + "\"},\"finish_reason\":null}]}";
WriteSseData("data: " + jsonChunk + "\n\n");
}
}
private void FinishRequest()
{
if (_currentContext == null) return;
if (_isCurrentRequestStreaming)
{
string stopChunk = "{\"id\":\"chatcmpl-webrelay\",\"object\":\"chat.completion.chunk\",\"created\":" + DateTimeOffset.Now.ToUnixTimeSeconds() + ",\"model\":\"web-relay\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}";
WriteSseData("data: " + stopChunk + "\n\n");
WriteSseData("data: [DONE]\n\n");
}
else
{
string content = _nonStreamResponseBuffer.ToString();
string json = "{\"id\":\"chatcmpl-webrelay\",\"object\":\"chat.completion\",\"created\":" + DateTimeOffset.Now.ToUnixTimeSeconds() + ",\"model\":\"web-relay\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + EscapeJson(content) + "\"},\"finish_reason\":\"stop\"}]}";
ResponseText(_currentContext, json, 200, "application/json");
}
CleanupRequest();
}
private void WriteSseData(string data)
{
try
{
byte[] buffer = Encoding.UTF8.GetBytes(data);
_currentContext.Response.OutputStream.Write(buffer, 0, buffer.Length);
_currentContext.Response.OutputStream.Flush();
}
catch { CleanupRequest(); }
}
private void CleanupRequest()
{
try { _currentContext?.Response.Close(); } catch { }
_currentContext = null;
_nonStreamResponseBuffer.Clear();
}
private void ResponseText(HttpListenerContext ctx, string text, int code, string contentType)
{
try
{
ctx.Response.StatusCode = code;
ctx.Response.ContentType = contentType;
byte[] buffer = Encoding.UTF8.GetBytes(text);
ctx.Response.ContentLength64 = buffer.Length;
ctx.Response.OutputStream.Write(buffer, 0, buffer.Length);
ctx.Response.Close();
}
catch { }
}
private string EscapeJson(string s)
{
if (s == null) return "";
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
}
private string UnescapeJson(string str)
{
return (str ?? "").Replace("\\\"", "\"").Replace("\\\\", "\\").Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\t", "\t");
}
private void Log(string msg)
{
if (!_isDebugMode) return;
try
{
if (_txtLog != null && !_txtLog.IsDisposed)
{
if (_txtLog.InvokeRequired) _txtLog.Invoke(new Action(() => Log(msg)));
else _txtLog.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + msg + "\r\n");
}
}
catch { }
}
private string GetNetworkInterceptorScript()
{
return @"
window.chrome.webview.postMessage('[LOG] Interceptor Ready');
const originalFetch = window.fetch;
window.WebRelayBridge = {
sendPromptFromBase64: function(textBase64) {
const binary = atob(textBase64 || '');
const text = new TextDecoder().decode(Uint8Array.from(binary, ch => ch.charCodeAt(0)));
return this.sendPrompt(text);
},
sendPrompt: function(text) {
// TODO: 替换为目标站点的输入框定位、模型切换、联网开关和发送按钮逻辑。
window.chrome.webview.postMessage('[LOG] TODO: implement DOM send flow');
}
};
window.fetch = async (...args) => {
const response = await originalFetch(...args);
// TODO: 只拦截目标聊天接口,并把真实响应增量透传回宿主。
return response;
};
";
}
}
{
"ActionId": "",
"SharedActionId": "00000000-0000-0000-0000-000000000000",
"Title": "网页转API模板",
"Description": "基于 WebView2 + 本地 HTTP 中继 的网页聊天转 API 动作模板",
"Keywords": "web relay,api,webview2,openai compatible",
"ChangeLog": "v1.0.0 初始化网页转 API 模板",
"ShareUrl": "",
"Icon": "fa:Solid_Globe:#2F80ED",
"Variables": [
{
"Key": "quicker_in_param",
"Type": 0,
"DefaultValue": "",
"IsInput": true,
"Desc": "动作参数"
},
{
"Key": "input",
"Type": 0,
"DefaultValue": "",
"IsInput": false,
"Desc": "兼容输入参数"
},
{
"Key": "rtn",
"Type": 0,
"DefaultValue": "",
"IsOutput": false,
"Desc": "返回值(自动)"
},
{
"Key": "errMessage",
"Type": 0,
"DefaultValue": "",
"IsOutput": false,
"Desc": "错误信息(自动)"
}
],
"References": [
"System.Windows.Forms.dll",
"System.Drawing.dll",
"Microsoft.Web.WebView2.Core.dll",
"System.Runtime.Serialization.dll",
"System.Xml.dll"
]
}
Related skills
FAQ
What does quicker-skill do?
quicker-skill is a Claude Code skill for ai & agent building.
When should I use quicker-skill?
When you need to helps with ai & agent building tasks during AI-assisted development., or when quicker-skill is a claude code skill for ai & agent building.
What are the main capabilities?
quicker-skill; AI & Agent Building; AI-coding skill.