AI LOAD INSTRUCTION: yak scripts are not isolated command-line programs — when running in the Yakit engine, yakit.* library functions push structured messages to the frontend through the gRPC ExecResult bidirectional stream, and the frontend
useHoldGRPCStreamroutes them by message type / log level to different UI components (log panel, progress bar, table, chart, risk list, website tree...). This page explains this complete链路 in full and provides an API quick reference and runnable examples for each UI output function. When writing a plugin and wanting "the interface to display X", look it up here.
0. Related routes
- Main entry:yak
- Yakit basic usage and function area description:yakit-basic
- Database operations (db library):yaklang-database
- Native plugin + cli parameter form:yakit-native-plugin
- Right-click codec plugin:yakit-rightclick-plugin
- Validation toolchain:yaklang-toolchain
1. Core mental model: the complete chain from yak script to UI
flowchart LR
subgraph Script["yak 脚本 (引擎内执行)"]
Call["yakit.Info(...)<br/>yakit.SetProgress(...)<br/>yakit.EnableTable(...)<br/>yakit.Output(table)"]
end
subgraph Bridge["gRPC 桥接 (引擎 -> 前端)"]
Client["YakitClient.send()<br/>封装为 ExecResult"]
Stream["gRPC 双向流<br/>ExecResult.IsMessage=true<br/>Message=JSON(YakitMessage)"]
end
subgraph Frontend["Yakit 前端"]
Hook["useHoldGRPCStream<br/>解析 YakitMessage"]
Route{"type / level<br/>路由"}
UI["日志面板 / 进度条<br/>表格 / 图表 / 卡片<br/>风险列表 / 网站树"]
end
Call --> Client --> Stream --> Hook --> Route --> UI
Key understanding:
- yakit.* functions do not directly manipulate the DOM: they construct
YakitLog/YakitProgress/YakitFeatureand other structs, and emit them throughYakitClient.send(). - Two modes of send:
- VirtualYakitClient(command line
yak xxx.yak): the send callback printsExecResultto stdout (log.Info)。 - gRPC YakitClient(Yakit engine environment): send pushes
ExecResultto the frontend through the gRPC bidirectional stream.
- VirtualYakitClient(command line
- The frontend routes by type + level:
useHoldGRPCStreamparsesYakitMessageJSON, and bytype(log/progress/status-card) andlogData.level(json-feature/feature-table-data/json-risk/info/warn/...) dispatches to different UI components.
Source location:
- Engine-side export:
yaklang/common/yak/yaklib/yakit.go(YakitExports/GetExtYakitLibByClient)- Client implementation:
yaklang/common/yak/yaklib/yakit_client.go(YakitClient.Output/YakitLog/send)- Message serialization:
yaklang/common/yak/yaklib/yakit.go(MarshalYakitOutput/YakitMessageGenerator)- Frontend consumption:
yakit/app/renderer/src/main/src/hook/useHoldGRPCStream/useHoldGRPCStream.ts
2. Message protocol: YakitMessage structure
Every message from the engine to the frontend is ExecResult{IsMessage: true, Message: JSON}, where Message is the
JSON serialization of
YakitMessage :
// yaklang/common/yak/yaklib/yakit.go
type YakitMessage struct {
Type string `json:"type"` // "log" / "progress" / "status-card"
Content json.RawMessage `json:"content"` // YakitLog / YakitProgress / YakitStatusCard
}
type YakitLog struct {
Level string `json:"level"` // info/warn/error/success/debug/code/markdown/text/report/file/
// json-feature/feature-table-data/feature-text-data/
// json-risk/json-table/json-graph/json-httpflow/fingerprint/...
Data string `json:"data"` // 内容或 JSON 字符串
Timestamp int64 `json:"timestamp"`
}
Frontend useHoldGRPCStream consumption logic (simplified):
// yakit/app/renderer/src/main/src/hook/useHoldGRPCStream/useHoldGRPCStream.ts
if (obj.type === 'progress') { /* 进度条 */ }
if (obj.type === 'log' && logData.level === 'feature-status-card-data') { /* 状态卡片 */ }
if (obj.type === 'log' && logData.level === 'json-feature') { /* 启用表格/网站树/文本标签页 */ }
if (obj.type === 'log' && logData.level === 'feature-table-data') { /* 表格行数据 */ }
if (obj.type === 'log' && logData.level === 'feature-text-data') { /* 文本标签页数据 */ }
if (obj.type === 'log' && logData.level === 'json-risk') { /* 风险列表 */ }
// 其余 log -> 日志面板
3. UI output function category quick reference
3.1 Log output (log panel)
| Function | UI display | Description |
|---|---|---|
yakit.Info(format, args...) |
info-level log line | Supports printf formatting |
yakit.Warn(format, args...) |
warn-level log line (yellow) | Same as above |
yakit.Error(format, args...) |
error-level log line (red) | Same as above |
yakit.Success(msg) |
success-level log line (green) | Does not do printf; accepts an already-assembled string |
yakit.Debug(msg) |
debug-level log line | Same as above |
yakit.Text(msg) |
text block (entire block of text) | Multiline text, no formatting |
yakit.Code(msg) |
code block (monospace font) | Code/packet |
yakit.Markdown(md) |
Markdown rendering | Supports headings/lists/tables/bold |
yakit.Report(id) |
Report reference | References a report by ID |
Note:
yakit.Info/yakit.Warn/yakit.Errorsupports printf formatting (the first argument is the format string);yakit.Success/yakit.Text/yakit.Code/yakit.Markdowndoes not do printf; you need to first usesprintf/ f-string concatenation.log.infois printf-style; to print content containing%you need to use thelog.info("%s", x)placeholder and cannot directly concatenate strings.
3.2 Progress bar
| Function | UI display | Description |
|---|---|---|
yakit.SetProgress(f) |
Main progress bar | f is 0.0~1.0 |
yakit.SetProgressEx(id, f) |
Progress bar with the specified id | Multiple independent progress bars can be maintained at the same time |
3.3 Status card (small card for key metrics)
| Function | UI display | Description |
|---|---|---|
yakit.StatusCard(id, data, tags...) |
Status card | The same id updates in place; tags are used for grouping |
3.4 Static table (one-time output)
| Function | UI display | Description |
|---|---|---|
yakit.NewTable(head...) |
Create table object | head is the column name |
table.Append(row...) |
Append row | Each parameter corresponds to one column |
yakit.Output(table) |
Render table | Unified output channel |
yakit.NewTableis a static table that "collects everything before displaying it all at once". To output results while scanning, useEnableTable + TableData(see 3.5).
3.5 Dynamic table (real-time incremental updates)
| Function | UI display | Description |
|---|---|---|
yakit.EnableTable(name, columns) |
Declare a dynamic table | columns is the list of column names |
yakit.TableData(name, data) |
Write one row to the declared table | data is a map, keys correspond to column names; may include "uuid" to control row identity |
EnableTablesendsjson-feature(feature=fixed-table) to declare the table,TableDatasendsfeature-table-datawritten row by row. Writing again with the same uuid will "update" the same row.
3.6 Charts
| Function | UI display | Description |
|---|---|---|
yakit.NewLineGraph(name...) |
Line chart | Trend display |
yakit.NewBarGraph(name...) |
Bar chart | Categorical comparison |
yakit.NewPieGraph(name...) |
Pie chart | Proportion/composition |
yakit.NewWordCloud(name...) |
Word cloud | Keyword frequency |
graph.Add(key, value) |
Add data point | Common to all charts |
yakit.Output(graph) |
Render chart | Unified output channel |
The four chart constructors have exactly the same signature and differ only in display form. The chart object is sent via
yakit.Outputand the frontend renders byjson-graphlevel.
3.7 Website tree
| Function | UI display | Description |
|---|---|---|
yakit.EnableWebsiteTrees(targets) |
Website tree tab | targets is a comma-separated list of targets |
3.8 Text tab
| Function | UI display | Description |
|---|---|---|
yakit.EnableText(tabName) |
Text tab | Declare a text tab |
yakit.TextTabData(tabName, data) |
Text tab data | Append text to the declared tab |
3.9 DOT graph tab
| Function | UI display | Description |
|---|---|---|
yakit.EnableDotGraphTab(tabName) |
DOT graph tab | Declare tab |
yakit.OutputDotGraph(tabName, dotData) |
DOT graph data | Output a Graphviz DOT string |
3.10 Unified output channel
| Function | Description |
|---|---|
yakit.Output(obj) |
Automatically select the output channel based on the object type: Table/Graph/Risk/HTTPFlow/Fingerprint/StatusCard/TableData/... |
yakit.Outputis "intelligent routing" — pass in*YakitTableto go tojson-table, pass in*YakitGraphto go tojson-graph, pass in*schema.Riskto go tojson-risk, pass in*fp.MatchResultto go tofingerprint, pass in*YakitFixedTableDatato go tofeature-table-data, pass in*YakitStatusCardto go tofeature-status-card-data... This means the scanner's native result objects can be Output directly, and the frontend renders them into the corresponding cards/tables.
3.11 Risks and Files
| Function | UI Display | Description |
|---|---|---|
risk.NewRisk(url, risk.title(...), ...) |
Risk List | See the risk usage in each hotpatch skill for details |
yakit.NewHTTPFlowRisk(...) |
HTTP Traffic Risk | Risk object with request/response |
yakit.File(path, desc...) |
File Card | File information/operation records |
yakit.FileReadAction(...) and other series such as yakit.FileReadAction(...) |
File operation records | Read/Write/Create/Delete/Status/Permission/Find |
3.12 Streaming Output
| Function | UI Display | Description |
|---|---|---|
yakit.Stream(streamType, streamId, reader) |
Streaming Log | Read the reader character by character and push |
4. Complete Example: A Plugin That Outputs Multiple UI Components
// 关键词: yakit.Output, EnableTable, TableData, StatusCard, SetProgress, NewTable, NewBarGraph, Markdown
// 适用: yak 原生插件(在 Yakit 中执行时各 UI 组件实时展示)
func runPlugin() {
// 1. 进度条
yakit.SetProgress(0)
// 2. 状态卡片(关键指标)
yakit.StatusCard("Targets", "0/10", "progress")
// 3. 动态表格(边扫边出)
yakit.EnableTable("Port Scan Result", ["host", "port", "service"])
// 模拟扫描
results = [["10.0.0.1", "80", "http"], ["10.0.0.1", "443", "https"], ["10.0.0.2", "22", "ssh"]]
for i = 0; i < len(results); i++ {
r = results[i]
// 写动态表格
yakit.TableData("Port Scan Result", {"host": r[0], "port": r[1], "service": r[2]})
// 更新进度与卡片
yakit.SetProgress(float(i + 1) / float(len(results)))
yakit.StatusCard("Targets", sprintf("%d/%d", i + 1, len(results)), "progress")
yakit.Info("scanned %s:%s -> %s", r[0], r[1], r[2])
sleep(0.05)
}
yakit.SetProgress(1.0)
yakit.StatusCard("Targets", "10/10", "done")
// 4. 静态表格(汇总)
table = yakit.NewTable("Host", "Port", "Service")
for r in results { table.Append(r[0], r[1], r[2]) }
yakit.Output(table)
// 5. 柱状图
graph = yakit.NewBarGraph("port distribution")
graph.Add("80", 1)
graph.Add("443", 1)
graph.Add("22", 1)
yakit.Output(graph)
// 6. Markdown 报告
yakit.Markdown(sprintf("# Scan Report\n\n- hosts: 2\n- open ports: %d\n", len(results)))
yakit.Success("scan completed")
}
func runSelfTest() {
// 命令行自测: 调用纯函数验证逻辑,不依赖 UI
// yakit.* 在命令行环境走 VirtualYakitClient, 输出到 stdout
runPlugin()
assert true, "should complete without error"
}
if YAK_MAIN {
runSelfTest()
}
5. Behavior of yakit.* in the Command-Line Environment (Important)
When the script does not run in the Yakit engine (i.e., yak xxx.yak when run from the command line):
AutoInitYakit()Detected that there is no--yakit-webhookparameter → useemptyVirtualClient(NewVirtualYakitClient)。emptyVirtualClient's send callback to printExecResulttolog.Info(i.e., stdout).- Therefore
yakit.Info("hello")is equivalent tolog.Info("hello"),yakit.Output(table)will print the table's JSON.
This means: scripts containing yakit.* calls will not crash on the command line, and all output degrades to log printing. So
the safe debugging loop of "first self-test the logic on the command line, then paste it back into Yakit" still holds.
Source code:
yaklang/common/yak/yaklib/yakit.goinemptyVirtualClientandAutoInitYakit。
6. How the Engine Injects the yakit Global Variable
Before the engine starts the script, it uses SetEngineClient to inject a YakitClient instance as the script's yakit global variable:
// yaklang/common/yak/yaklib/yakit_client.go
func SetEngineClient(e *antlr4yak.Engine, client *YakitClient) {
e.OverrideRuntimeGlobalVariables(map[string]any{
"yakit": GetExtYakitLibByClient(client),
"risk": map[string]any{ /* risk 库, 内部用 client 做 risk 输出 */ },
})
InitYakit(client) // 设置全局默认客户端
}
GetExtYakitLibByClient(client) returns a map[string]interface{}, where each key is a
yakit.xxx function name that the script can call, and the value is the function bound to that client. Therefore the same script running under different clients:
- gRPC client → output to the frontend UI
- VirtualYakitClient → output to stdout
- empty client → output is discarded
7. Detailed Explanation of the Frontend useHoldGRPCStream Consumption Logic
The frontend useHoldGRPCStream is the core hook for displaying all plugin/scan results; it:
- Listens to the gRPC stream via
yakitStream.onData(token, callback). - Each time a
ExecResultis received, parsesYakitMessageJSON。 - and routes by
type+levelto different buffers:
type |
level |
Frontend Buffer | UI Component |
|---|---|---|---|
progress |
- | progressKVPair (Map) |
Progress Bar |
log |
feature-status-card-data |
cardKVPair (Map) |
Status Card |
log |
json-feature (feature=fixed-table) |
tabTable (Map) |
Dynamic Table Tab |
log |
json-feature (feature=website-trees) |
tabWebsite |
Website Tree Tab |
log |
json-feature (feature=text) |
tabsText (Map) |
Text Tab |
log |
feature-table-data |
Update tabTable the corresponding table in |
Dynamic table row |
log |
feature-text-data |
Update tabsText |
Text tab content |
log |
json-risk |
riskMessages (Array) |
Risk list |
log |
info/warn/error/success/debug/text/code/markdown |
messages (Array) |
Log panel |
log |
json-table |
messages (Array) |
Log panel (static table JSON) |
log |
json-graph |
messages (Array) |
Log panel (chart JSON) |
- Periodically (default 500ms) snapshot the buffer to React state to trigger a UI re-render.
After understanding this chain, you know "what the frontend is doing when writing
yakit.EnableTable"—it is waiting forjson-featuremessages to create the table Tab, then waits forfeature-table-datamessages to fill row by row.
8. Pitfalls and Notes
| Pitfall | Wrong approach | Correct approach |
|---|---|---|
yakit.Info contains % direct concatenation |
yakit.Info("progress: 50%") |
yakit.Info("progress: 50%%") or yakit.Info("%s", "progress: 50%") |
yakit.Success used as printf |
yakit.Success("found %d", n) |
yakit.Success(sprintf("found %d", n)) |
Dynamic table TableData without first EnableTable |
directly TableData |
first EnableTable declaration, then TableData write rows |
| Table data contains nested objects/arrays | yakit.TableData("t", {"info": {"a": 1}}) |
Flatten to primitive types, frontend filters out rows containing objects |
| Status card id is not fixed | Random id each time | Use a fixed id for in-place updates |
| Calling yakit.* in concurrent hooks | Multiple goroutines calling yakit.Info simultaneously | yakit.* itself is concurrency-safe (send is thread-safe), but avoid heavy yakit.Info in hooks to prevent performance impact |
9. Examples (examples/)
| File | Demo content | Validation |
|---|---|---|
| examples/ui-output-tour.yak | Log/progress bar/status card/static table/dynamic table/bar chart/Markdown full chain | yak <file> self-test |
10. Validation
cd /Users/v1ll4n/Projects/yaklang
go run common/yak/cmd/yak.go skills/yakit-ui-binding/examples/ui-output-tour.yak
When running on the command line,yakit.* goes through VirtualYakitClient, and all output degrades to stdout logs.
Running the same script in Yakit, each UI component displays in real time.
Pass criteria: completes within 10 seconds, no panic, log all in English, and ... self test passed。
Reference sources
- Engine-side yakit library export:
yaklang/common/yak/yaklib/yakit.go(YakitExports/GetExtYakitLibByClient) - Client implementation:
yaklang/common/yak/yaklib/yakit_client.go(YakitClient.Output/YakitLog/send) - Viewer extensions (EnableTable/StatusCard etc.):
yaklang/common/yak/yaklib/yakit_viewer.go - Message serialization:
yaklang/common/yak/yaklib/yakit.go(MarshalYakitOutput/YakitMessageGenerator) - Script execution:
yaklang/common/yak/yakscript/exec.go(ExecScriptWithParam/ExecScriptWithExecParam) - Frontend gRPC stream consumption:
yakit/app/renderer/src/main/src/hook/useHoldGRPCStream/useHoldGRPCStream.ts - Frontend type definitions:
yakit/app/renderer/src/main/src/hook/useHoldGRPCStream/useHoldGRPCStreamType.d.ts - Frontend result display:
yakit/app/renderer/src/main/src/pages/plugins/operator/pluginExecuteResult/PluginExecuteResult.tsx