快速开始 — 跟着搭一个节点
不克隆一个现成项目照着跑——那样手没动、决策也不是你的。这篇带你以 消息提醒 为例,跟着逻辑把一个真实节点搭出来:读懂 5 块通信代码 → 填功能清单 → 填身份清单 → 提 PR。搭完,你的程序就接入了 KnotLink。
本文用的模板就是 KNodeIndex 新手训练营。拷一个语言项目、跟着填、自检绿了提 PR,你的名字就挂在节点索引里 → 新手训练营 practice/。
流程
拿模板 → 读懂 5 块代码 → 填 FuncList → 填 manifest → 提 PR
第 0 步:拿到模板
模板在训练营里,每种语言是一个自带 SDK、能直接编译运行的完整项目:
git clone https://github.com/KnotLink-Protocol/KNodeIndex
cd KNodeIndex/practice/_exercise/python # 或 javascript / java / csharp / cpp
一个语言项目长这样(以 Python 为例):
python/
├── main.py # 通信代码(本文第 2 步)
├── plugin_manifest.json # 身份清单(第 4 步)
├── FuncList.json # 功能清单(第 3 步)
├── README.md # 项目说明
├── logo.png # 节点图标
├── requirements.txt # 依赖
└── knotlink/ # 打包好的 SDK,无需安装
第 1 步:想清楚要提供什么,定 AppID
动手前先想:这个节点对外提供什么?消息提醒很简单——
接口
show:别人调用它、传一条消息,它打印出来并回OK; 信号messageShown:打印完广播一条「已显示」,带上消息内容和时间。
AppID 用倒置域名、把你的名字嵌进去:
com.github.你的用户名.msgreminder
这样它在整个 KnotLink 网络里天然唯一,也是你的署名。一旦发布就别改了。
早期项目用 0x 开头的 8 位十六进制(如 0x00000014),KnotLink 继续支持。新项目推荐倒置域名,可读性更好。详见 核心概念 — 标识符命名规范。
第 2 步:读懂通信代码(5 块,KLUDF 标准化)
main.* 里的代码已经按节点的执行逻辑分成 5 块、完整写好并带注释。你只要读懂每块,然后把 ① 里的 APPID 换成你自己。
| 块 | 干什么 |
|---|---|
| ① 上线 | 声明身份(AppID + 接口 + 信号),当「回复者」连上 KnotLink |
| ② 解析 | 被调用时,用 KLUDF(KLKVMap)把 key=value 请求解析出参数 |
| ③ 处理 | 你的业务:把消息打印出来 |
| ④ 报告 | 用 KLUDF 打包 content + time,广播 messageShown 信号 |
| ⑤ 回复 | 告诉调用方成功(OK) |
- Python
- JavaScript
- Java
- C#
- C++
from knotlink import OpenSocketResponser, SignalSender, KLKVMap
import time
# ① 上线:声明身份,作为「回复者」连上 KnotLink
APPID = "com.github.你的用户名.msgreminder" # ← 换成你自己
OPENSOCKET_ID = "show"
SIGNAL_ID = "messageShown"
responser = OpenSocketResponser(APPID, OPENSOCKET_ID)
sender = SignalSender(APPID, SIGNAL_ID)
def handle_request(data: str) -> str:
# ② 用 KLUDF 解析请求,按名取参数
req = KLKVMap()
req.deserialize(data)
message = req.get("msgContext", "默认消息")
# ③ 处理:打印消息
print(f"收到:{message}")
# ④ 用 KLUDF 打包 content + time,广播信号
out = KLKVMap()
out["content"] = message
out["time"] = time.strftime("%Y-%m-%d %H:%M:%S")
sender.emitt(out.serialize())
# ⑤ 回复
return "OK"
responser.set_RecvFunc(handle_request) # 回调返回值自动回传
input("按回车退出。\n")
const OpenSocketResponser = require('./knotlink/OpenSocketResponser');
const SignalSender = require('./knotlink/SignalSender');
const KLKVMap = require('./knotlink/KLKVMap');
// ① 上线
const APPID = "com.github.你的用户名.msgreminder"; // ← 换成你自己
const OPENSOCKET_ID = "show";
const SIGNAL_ID = "messageShown";
const responser = new OpenSocketResponser(APPID, OPENSOCKET_ID);
const sender = new SignalSender(APPID, SIGNAL_ID);
responser.on('request', (questionID, data) => {
// ② 用 KLUDF 解析请求
const req = new KLKVMap();
req.deserialize(data);
const message = req.get("msgContext", "默认消息");
// ③ 处理:打印消息
console.log(`收到:${message}`);
// ④ 用 KLUDF 打包 content + time,广播信号
const out = new KLKVMap();
out.set("content", message);
out.set("time", new Date().toISOString());
sender.emitt(out.serialize());
// ⑤ 回复(JS 显式 sendBack)
responser.sendBack("OK", questionID);
});
public class Main {
// ① 上线
static final String APPID = "com.github.你的用户名.msgreminder"; // ← 换成你自己
static final String OPENSOCKET_ID = "show";
static final String SIGNAL_ID = "messageShown";
public static void main(String[] args) throws Exception {
OpenSocketResponser responser = new OpenSocketResponser(APPID, OPENSOCKET_ID);
SignalSender sender = new SignalSender(APPID, SIGNAL_ID);
responser.setDataListener((data, key) -> {
// ② 用 KLUDF 解析请求
KLKVMap req = new KLKVMap();
req.deserialize(data);
String message = req.get("msgContext", "默认消息");
// ③ 处理:打印消息
System.out.println("收到:" + message);
// ④ 用 KLUDF 打包 content + time,广播信号
KLKVMap out = new KLKVMap();
out.put("content", message);
out.put("time", java.time.LocalDateTime.now().toString());
sender.emitt(out.serialize());
// ⑤ 回复(Java 显式 sendBack)
responser.sendBack("OK", key);
});
new java.util.Scanner(System.in).nextLine();
}
}
using System;
using System.Threading.Tasks;
using KnotLink;
class Program
{
// ① 上线
const string APPID = "com.github.你的用户名.msgreminder"; // ← 换成你自己
const string OPENSOCKET_ID = "show";
const string SIGNAL_ID = "messageShown";
static async Task Main()
{
var responser = new OpenSocketResponser(APPID, OPENSOCKET_ID);
var sender = new SignalSender(APPID, SIGNAL_ID);
responser.OnQuestionAsync = async (data) =>
{
// ② 用 KLUDF 解析请求
var req = new KLKVMap();
req.Deserialize(data);
string message = req.Get("msgContext", "默认消息");
// ③ 处理:打印消息
Console.WriteLine($"收到:{message}");
// ④ 用 KLUDF 打包 content + time,广播信号
var payload = new KLKVMap();
payload["content"] = message;
payload["time"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
await sender.EmitAsync(payload.Serialize());
// ⑤ 回复(返回值自动回传)
return "OK";
};
Console.ReadLine();
}
}
#include <iostream>
#include <string>
#include <ctime>
#include "OpenSocketResponser.hpp"
#include "SignalSender.hpp"
#include "KLUDF.hpp"
using namespace knotlink;
// ① 上线
const std::string APPID = "com.github.你的用户名.msgreminder"; // ← 换成你自己
const std::string OPENSOCKET_ID = "show";
const std::string SIGNAL_ID = "messageShown";
int main() {
OpenSocketResponser responser(APPID, OPENSOCKET_ID);
SignalSender sender(APPID, SIGNAL_ID);
responser.setQuestionHandler([&](const std::string& data) -> std::string {
// ② 用 KLUDF 解析请求
KLKVMap req;
req.deserialize(data);
std::string message = req.get("msgContext", "默认消息");
// ③ 处理:打印消息
std::cout << "收到:" << message << std::endl;
// ④ 用 KLUDF 打包 content + time,广播信号
KLKVMap out;
out["content"] = message;
char tbuf[32];
std::time_t now = std::time(nullptr);
std::strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", std::localtime(&now));
out["time"] = tbuf;
sender.emitt(out.serialize());
// ⑤ 回复(返回值自动回传)
return "OK";
});
std::cin.get();
return 0;
}
请求和信号数据都是 key1=value1;key2=value2 格式,用 KLKVMap 标准化处理:deserialize 解析、get 取参数、serialize 打包。各语言 API 一致(Python/C++/C# 回调返回值自动回传,Java/JS 要显式 sendBack)。
JS / Java 的官方 SDK 暂未内置
KLKVMap,训练营已在打包 SDK 里补上(knotlink/KLKVMap.js、knotlink-sdk/KLKVMap.java)。八种语言的完整示例见 编写代码。
第 3 步:填 FuncList.json
告诉工具链你的节点有哪些接口和信号——appID 与代码、manifest 三处保持一致:
{
"appName": "消息提醒",
"specVersion": "1.0",
"manifestVersion": "1.0.0",
"openSocket": {
"ShowMsg": {
"appID": "com.github.你的用户名.msgreminder",
"openSocketID": "show",
"description": "打印收到的消息并回复 OK",
"args": {
"msgContext": {
"type": "input",
"description": "消息内容",
"defaultVal": "测试消息"
}
},
"returns": []
}
},
"signal": {
"messageShown": {
"appID": "com.github.你的用户名.msgreminder",
"signalID": "messageShown",
"description": "消息显示后广播",
"returns": {
"content": { "description": "已显示的消息内容" },
"time": { "description": "消息显示的时间" }
}
}
}
}
openSocketID/signalID要和代码里一致(show/messageShown)。args三种参数类型:input(运行时输入)、optional(下拉选项)、static(固定值)。- 注意
signal.returns是对象格式{ "字段": { "description": "..." } },不是数组。
不想手写?用 FLEditor 可视化编辑。详见 填写功能清单。
第 4 步:填 plugin_manifest.json
告诉平台你是谁、怎么启动:
{
"app_id": "com.github.你的用户名.msgreminder",
"plugin_name": "消息提醒",
"author": "你的用户名",
"version": "v1.0.0",
"description": "打印收到的消息",
"download_url": "https://github.com/你的用户名/msgreminder/releases/latest",
"auto_start": "true",
"exe_path": "MsgNotification.exe"
}
| 字段 | 填什么 |
|---|---|
app_id | 和第 1 步、代码、FuncList 一致 |
plugin_name | 显示名称,和 FuncList 的 appName 一致 |
author | 你的名字 |
download_url | 下载地址(GitHub Releases 等) |
exe_path | 可执行文件名(源码项目可填源码入口文件) |
节点类型由目录决定:插入式放
plugin/(用plugin_manifest.json),独立式放standalone/(用standalone_manifest.json)。详见 打包与分发。
第 5 步:提 PR,署名收录
练手(训练营) → 提交到 practice/:
- Fork KNodeIndex
- 把你填好的语言项目复制成
practice/<你的GitHub用户名>/ - 本地自检:
node validate-practice.js(全绿再提) - 提 PR,合并后你的练习就留档在
practice/里 🎉
真实节点 → 提交到 plugin/ 或 standalone/:
- 复制
plugin/_template/→ 重命名为节点名-作者名 - 放入:
FuncList.json+plugin_manifest.json+logo.png+README.md - 本地自检:
node validate-nodes.js - 提 PR,合并后你的名字挂在节点索引里
还想看现成的?
官方案例集有四种标准模式的完整清单和代码:官方案例集