NotificationTool - Typecho 登录和评论通知插件
用 Claude AI 生成了一个 Typecho V1.3.0 的插件,功能是在登录时或有新评论时发送消息通知给管理员,支持邮件、钉钉、企业微信和飞书机器人。
时代真的变了,我一行代码也没有编写,完全是口述需求让 AI 来生成和调试,已测试过邮件、钉钉和企业微信均使用正常。
使用方法:将解开后的目录复制到网站的 /usr/Plugins/ 目录即可。
下载:NotificationTool.rar

用 Claude AI 生成了一个 Typecho V1.3.0 的插件,功能是在登录时或有新评论时发送消息通知给管理员,支持邮件、钉钉、企业微信和飞书机器人。
时代真的变了,我一行代码也没有编写,完全是口述需求让 AI 来生成和调试,已测试过邮件、钉钉和企业微信均使用正常。
使用方法:将解开后的目录复制到网站的 /usr/Plugins/ 目录即可。
下载:NotificationTool.rar

Delphi 没有支持 toml 格式解析的单元,从 github 上找到了来自悉尼的 Iwan Kelaiah 写的一个 TOML Parser for Free Pascal 单元。简单修改了一下,让其支持 delphi。
以下说明比较简陋,详细文档参见压缩包。
新增加的用法:
// 从文件加载
Config := LoadToml('config.toml');
// 或:
Config := NewTable;
Config.LoadFromFile('config.toml');
// 基本类型
Config.GetStr(Key, Default); // 字符串
Config.GetInt(Key, Default); // 整数
Config.GetFloat(Key, Default); // 浮点数
Config.GetBool(Key, Default); // 布尔值
Config.GetDateTime(Key, Default); // 日期时间
Config.GetDateTimeValue(Key, Default); // 日期时间原始精度字符串
Config.TryGetStr(Key, Value); // 字符串
Config.TryGetInt(Key, Value); // 整数
Config.TryGetFloat(Key, Value); // 浮点数
Config.TryGetBool(Key, Value); // 布尔值
Config.TryetDateTime(Key, Value); // 日期时间
Config.TryGetDateTimeValue(Key, Value); // 日期时间原始精度字符串
// 复杂类型
Config.GetArray(Key); // 数组(返回nil表示不存在)
Config.TryGetArray(Key, value); // 数组
Config.GetTable(Key); // 表(返回nil表示不存在)
Config.TryGetTable(Key, value); // 表
// 数组方法
Array.GetStr(Index, Default);
Array.GetInt(Index, Default);
Array.GetTable(Index);
Array.ForEachTable(Procedure); // 遍历数组写入
// Set类,默认覆盖原值
Config.SetStr(key, value, [Overwrite]);
Config.SetInt(key, value, [Overwrite]);
Config.SetFloat(key, value, [Overwrite]);
Config.SetBool(key, value, [Overwrite]);
Config.SetDateTime(key, value, [Overwrite]);
Config.SetArray(key, value, [Overwrite]);
Config.SetTable(key, value, [Overwrite]);
// Put方法,支持重载,自动识别类型,默认覆盖原值
Config := NewTable
.Put(key1, value1, [Overwrite])
.Put(key2, value2, [Overwrite])
.Put(key3, value3, [Overwrite]);
// 数组的 Add 方法
Tags := NewArray
.AddStr('value1')
.AddStr('value2')
.AddStr('value3');
Ports := NewArray
.AddInt(8080)
.AddInt(8081)
.AddInt(8082);
// 另外也支持:AddFloat、AddBool、AddDateTime、AddTable
// 按索引删除数组中数据
Array.RemoveAt(idx);
// 清空数组
Array.Clear;
// 更新或创建函数(功能重复,已删除)
// 创建表或数组
Config := NewTable;
Tags := NewArray;
// 或更短的别名
Config := Table;
Tags := Arr;
// 其它工具方法
Config.ToString; // 转换为字符串
Config.Count; // 获取键数量
Config.HasKey(Key); // 检查键是否存在
Config.GetKeys(List, Recursive); // 获取所有键名
Config.REmove('key'); // 删除键示例:
//打开 toml 文件
Config := LoadToml('config.toml');
// 或:
Config := NewTable;
Config.LoadFromFile('config.toml');
// 读取
width := Config.GetInt('width', 800);
title := Config.GetStr('title');
debug := Config.GetBool('debug',False);
name := Config.GetStr('server.host', 'localhost'); // 支持点分隔路径
//创建
Config := NewTable;
Tags := NewArray;
//创建同时赋值:
Config := NewTable
.SetStr('app_name', 'My Application')
.SetStr('version', '1.0.0')
.SetInt('port', 8080)
.SetBool('debug', False);
Tags := NewArray
.AddStr('pascal')
.AddStr('delphi')
.AddStr('toml');
//创建嵌套结构:
Config := NewTable
.Put('server',
NewTable
.Put('host', 'localhost')
.Put('port', 8080)
)
.Put('database',
NewTable
.Put('host', 'localhost')
.Put('port', 5432)
.Put('pool_size', 10)
);
// 写入(默认覆盖原值)
Config.SetStr('title', 'My App');
Config.SetInt('width', 1920, True);
Config.SetBool('debug', False);
Config.SetFloat('version', 1.1, False);
// 自动识别类型写入(默认覆盖原值)
Config.Put('width', 1920)
.Put('height', 1080)
.Put('title', 'My App', False);
// 保存文件
Config.SaveToFile('config.toml');
//创建表数组
Servers := NewArray
.AddTable(
NewTable
.Put('name', 'web-01')
.Put('ip', '192.168.1.10')
)
.AddTable(
NewTable
.Put('name', 'db-01')
.Put('ip', '192.168.1.20')
);
Config.SetArray('servers', Servers);
// 遍历数组方式一
parameters.ForEachTable(
procedure(param: TTOMLTable)
begin
showmessage(param.GetStr('name'));
end
);
// 遍历数组方式二
procedure ProcessParameter(param: TTOMLTable);
begin
showmessage(param.GetStr('name'));
end;
parameters.ForEachTable(ProcessParameter); // 调用以下内容为翻译的一些原始用法:
config.toml 示例:
# 注释:config.toml
revision = "1.2.1af"
[project]
name = "My Amazing Project"
version = "1.0.0"
基本用法
program BasicParseTOML;
uses
TOML;
var
Config: TTOMLTable;
RevisionValue, ProjectValue, ProjectName: TTOMLValue;
ProjectTable: TTOMLTable;
begin
// 打开 TOML 文件
Config := ParseTOMLFromFile('config.toml');
try
// 访问字符串类型数据
if (Config.TryGetValue('revision', RevisionValue)) then
WriteLn('''revision'' 的值为:', RevisionValue.AsString);
// 安全地访问数据
if Config.TryGetValue('project', ProjectValue) and
(ProjectValue is TTOMLTable) then
begin
ProjectTable := TTOMLTable(ProjectValue);
if ProjectTable.TryGetValue('name', ProjectName) then
WriteLn('Project Name: ', ProjectName.AsString)
else
WriteLn('Project name 未找到。');
end
else
WriteLn('Project 配置未找到。');
finally
Config.Free;
end;
end. program BasicSerializeTOML;
uses
TOML;
var
Config: TTOMLTable;
Database: TTOMLTable;
begin
Config := TOMLTable;
try
Database := TOMLTable;
// 添加数据
Database.Add('host', TOMLString('localhost'));
Database.Add('port', TOMLInteger(5432));
// 修改数据
Database.Items.AddOrSetValue('enable',TOMLBoolean(True));
Config.Add('database', Database);
if SerializeTOMLToFile(Config, 'config.toml') then
WriteLn('配置保存成功。')
else
WriteLn('保存配置出错。');
finally
Config.Free;
end;
end.常见用法
var
Config: TTOMLTable;
Tags: TTOMLArray;
begin
Config := TOMLTable;
try
Tags := TOMLArray;
Tags.Add(TOMLString('pascal'));
Tags.Add(TOMLString('toml'));
Config.Add('tags', Tags);
WriteLn(SerializeTOML(Config));
finally
Config.Free;
end;
end. var
Config: TTOMLTable;
Database: TTOMLTable;
begin
Config := TOMLTable;
try
Database := TOMLTable;
Database.Add('host', TOMLString('localhost'));
Database.Add('port', TOMLInteger(5432));
Config.Add('database', Database);
WriteLn(SerializeTOML(Config));
finally
Config.Free;
end;
end. program BasicSerializeTOML;
uses
TOML, SysUtils;
var
Config, ServerConfig: TTOMLTable;
Ports: TTOMLArray;
SerializedTOML: string;
begin
Config := TOMLTable;
try
// 创建嵌套表
ServerConfig := TOMLTable;
ServerConfig.Add('host', TOMLString('127.0.0.1'));
ServerConfig.Add('enabled', TOMLBoolean(True));
// 创建和填充数组
Ports := TOMLArray;
Ports.Add(TOMLInteger(80));
Ports.Add(TOMLInteger(443));
ServerConfig.Add('ports', Ports);
// 添加 server 配置到主配置
Config.Add('server', ServerConfig);
// 添加一些基础元数据
Config.Add('version', TOMLFloat(1.0));
Config.Add('last_updated', TOMLDateTime(Now));
// 序列化为 TOML 格式
SerializedTOML := SerializeTOML(Config);
WriteLn('生成 TOML:');
WriteLn(SerializedTOML);
// 保存文件
if SerializeTOMLToFile(Config, 'config.toml') then
WriteLn('保存成功。');
finally
Config.Free;
end;
end.上述代码将生成 TOML 文件,内容如下:
version = 1.0
last_updated = 2024-03-20T15:30:45Z
[server]
host = "127.0.0.1"
enabled = true
ports = [ 80, 443 ]说明:所有数据都经过适当的类型检查和内存管理。该单元可确保:
注意:
创建/释放表:
var
Config: TTOMLTable;
Database: TTOMLTable;
begin
Config := TOMLTable;
Database := TOMLTable;
//使用 Add 方法将嵌套表或值插入父表中。
Config.Add('database', Database);
// ... 添加其它嵌套表及数据...
Config.Free; // 仅释放顶级所有者即可自动释放所有嵌套的表和值。
end.避免显式释放嵌套对象:
• 不要手动释放嵌套表或值,以防止出现内存管理问题。
API 参考
类型
用于创建 TOML 值的辅助函数
创建 TOML 字符串值。
TOMLString
function TOMLString(const AValue: string): TTOMLString;创建 TOML 整数值。
TOMLInteger
function TOMLInteger(const AValue: Int64): TTOMLInteger;创建 TOML 浮点值。
TOMLFloat
function TOMLFloat(const AValue: Double): TTOMLFloat;创建 TOML 布尔值。
TOMLBoolean
function TOMLBoolean(const AValue: Boolean): TTOMLBoolean;创建 TOML 时间日期格式。
TOMLDateTime
function TOMLDateTime(const AValue: TDateTime): TTOMLDateTime;创建 TOML 数组。
TOMLArray
function TOMLArray: TTOMLArray;创建 TOML 表。
TOMLTable
function TOMLTable: TTOMLTable;解析函数
function ParseTOML(const ATOML: string): TTOMLTable;
begin
Result := TOML.Parser.ParseTOMLString(ATOML);
end;• ParseTOMLFromFile
将 TOML 文件解析为 TTOMLTable 对象。
function ParseTOMLFromFile(const AFileName: string): TTOMLTable;
var
FileStream: TFileStream;
StringStream: TStringStream;
begin
FileStream := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite);
try
StringStream := TStringStream.Create('');
try
StringStream.CopyFrom(FileStream, 0);
Result := ParseTOMLString(StringStream.DataString);
finally
StringStream.Free;
end;
finally
FileStream.Free;
end;
end;序列化函数
SerializeTOML
将 TTOMLValue 序列化为 TOML 格式的字符串。
function SerializeTOML(const AValue: TTOMLValue): string;
begin
Result := TOML.Serializer.SerializeTOML(AValue);
end;• SerializeTOMLToFile
序列化 TTOMLValue 并将其保存为文件(BOM:是否为带签名的 UTF-8 编码格式,默认有签名)。
function SerializeTOMLToFile(const AValue: TTOMLValue; const AFileName: string; BOM: Boolean = True): Boolean;
begin
Result := TOML.Serializer.SerializeTOMLToFile(AValue, AFileName; BOM: Boolean = True);
end; 以前用了插件,有人留言时会发邮件给我,最近发现开始不发邮件了。懒得仔细研究,就换了个 Robotpush 插件,是可以发钉钉、微信等机器人消息的。结果消息我是收到了,人家的留言却发不出去。
偷懒不得,看了一下源码,在第 199 行:
self::sendRobotMessages($robotTypes, $dingtalkWebhookUrl, $feishuWebhookUrl, $wecomWebhookUrl, $message);之后加入一行:
return $comment;测试成功。
另一处同样代码后加了一行 return $user;
2025年9月1日修订:改进时间戳算法;消息可以正确处理引号等特殊字符串。
钉钉的机器人群消息很好用,也不用申请特别的权限,免费的消息条数也很多,做个监控类的消息通知很合适。一般都是用 python、JAVA 等语言来编写调用代码,简单使用也可以直接命令行调 curl。
这几天想给一个用 delphi 编写的运维工具加上钉钉消息功能。尝试编写了一下,用自定义关键字方式发送消息很简单,一次就通过了;然而加签方式却死活通不过,总是返回加签错误。仔细阅读了N遍官方文档,就是个很常用的 HMAC-SHA256 + Base64 加签算法。蹊跷的是,我换了三四种不同的代码去实现,每一种算法得到的结果和网上的在线计算器的结果都一模一样,然而就是和官方的 Python 语言例程的结果不一样。
晚饭后出门散步时我继续思考这个问题:既然我的代码和在线计算器的一致,说明算法本身没有错误,那么只能是输入参数不一致;这时我突然领悟到,Python 和 JAVA 语言都会自动处理 \n 这样的转义字符串而 Delphi 不会,官方文档里要求在加签字符串中添加了一个 \n,肯定就是这里导致的错误。后来在代码中用 #10 来代替 \n,果然验签通过。
那么就分享一段完整的用 delphi 发送钉钉机器人消息的代码吧:
unit main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
System.JSON, Vcl.StdCtrls, Hash, System.Net.URLClient, DateUtils,
System.Net.HttpClient, System.Net.HttpClientComponent, NetEncoding;
type
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
NetHTTPClient1: TNetHTTPClient;
function dingtalk(content: string): boolean;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
dingtalk('test,测试。[关键字]');
end;
function TForm1.dingtalk(content: string): boolean;
//钉钉机器人消息发送函数
//URL 格式:https://oapi.dingtalk.com/robot/send?access_token=XXXXXX×tamp=XXX&sign=XXX
var
url, keywords, token, secret, timestamp, sign, str: string;
s: TStringStream;
json, TextObj: TJSONObject;
begin
//result := false;
begin
NetHTTPClient1.ContentType := 'application/json';
NetHTTPClient1.AcceptCharSet := 'utf-8';
NetHTTPClient1.AcceptLanguage := 'zh-CN';
token := 'xxxxxxxx';
keywords := '';
secret := 'SECxxxxxxxx';
url := 'https://oapi.dingtalk.com/robot/send?access_token=' + token;
(*
发送消息有两种安全策略,自定义关键字和加签,至少选择一种,也可同时使用。
secret 为加签密钥,不为空时表示需要加签。
timestamp 为毫秒级时间戳。
加签算法是常用的 HmacSHA256 + Base64:
把 timestamp+"\n"+secret 当做签名字符串,使用HmacSHA256算法计算签名,
然后进行 Base64 编码,将得到的结果再进行 urlEncode 编码,从而得到最终签名数据 sign。
*)
if secret <> '' then
begin
//获取毫秒级时间戳。2025/9/1 修订,避免时区原因导致错误
timestamp :=(MilliSecondsBetween(TTimeZone.Local.ToUniversalTime(Now),EncodeDateTime(1970, 1, 1, 0, 0, 0, 0))).ToString;
//加签
sign := TNetEncoding.Base64.EncodeBytesToString(THashSHA2.GetHMACAsBytes(timestamp + #10 + secret, secret, SHA256));
//URLEncode 编码
sign := TNetEncoding.url.Encode(sign);
//拼接 url
url := url + '×tamp=' + timestamp + '&sign=' + sign;
end;
//keywords 为自定义关键字,若为空时则必须加签,即 secret 不能为空。
//生成 json 格式的消息内容字符串。
//2025/9/1 修订,可以正确处理消息内容包含引号等特殊字符的情况。
json:=TJSONObject.Create;
json.AddPair('msgtype', 'text');
TextObj := TJSONObject.Create;
//如有自定义关键字 keywords,可插入到 content 中的任意位置。
TextObj.AddPair('content', TJSONString.Create(content+keywords));
json.AddPair('text', TextObj);
s := TStringStream.Create(json.ToString, TEncoding.UTF8);TEncoding.UTF8);
//发送 post 请求
try
str := NetHTTPClient1.Post(url, s).ContentAsString(nil);
except
on E: Exception do
memo1.Lines.Add('发送消息时出错:' + e.Message);
end;
//解析返回结果
try
json := TJSONObject.ParseJSONValue(str, true, true) as TJSONObject;
if json.GetValue<integer>('errcode') = 0 then
begin
result := true;
memo1.Lines.Add('消息发送成功。');
end
else
begin
result := false;
memo1.Lines.Add('消息发送失败,错误代码:' + json.GetValue<string>('errcode') + json.GetValue<string>('errmsg'));
end;
s.Free;
json.Free;
except
on E: Exception do
begin
result := false;
memo1.Lines.Add('发送消息时出错:' + e.Message);
end;
end;
end;
end;
end.
前言:
RouterOS 路由器单纯的使用脚本无法实现阿里云动态域名功能(无法给请求数据加签),需要借助其它代码写的转发代理。以前是在一个 Windows 版的工具里增加了相关功能代码,后来服务器要关掉,准备找个能部署到网站上的版本。网上找了一圈发现很多代码都是基于阿里云 SDK 的,需要在服务器上安装,而我用的那个网站空间只能上传文件不支持安装。于是自己写了一个基于阿里云 OpenAPI V3 版签名机制的 PHP 版本,只有一个 PHP 文件和一个 RouterOS 脚本,实测使用正常。顺带感慨:AI 辅助编程真的很方便。
支持阿里云域名动态解析的 RouterOS V7 脚本及对应的转发代理。
说明:
刚注意到我的一台路由器的 DDNS 功能失灵了,看了一下用的是一个从 github 上找的支持 cloudflare DNS 的脚本,现在一执行就报错。本想改改继续用,觉得这个脚本不够优雅,还要生成临时文件。于是就自己写了一个,特点是支持多路 PPPOE 拨号,不生成临时文件,也不定义临时的全局变量,并支持发送钉钉消息(如不需要把 dingtalk 值留空或相关语句屏蔽即可)。
脚本实际上分为两个,第一个脚本放在 /ppp/Profiles 那里,打开 pppoe 拨号使用的那个 profile 条目,在 scripts 标签页下的 On Up 编辑框里填写如下内容:
# 定义要调用的 DDNS 脚本名称
:local scrName "CloudFlare_DDNS"
# 获取拨号接口名称
:local ifname [/interface get $interface name];
# 获取公网IP
:local currentIP $"local-address";
# 定义要传输的变量
:local var1 "ifname=$ifname"
:local var2 "currentIP=$currentIP"
# 调用 DDNS 脚本并注入变量
:execute script="[[:parse \"[:parse [/system script get $scrName source]] $var1 $var2\"]]"第二个脚本在 /system/scripts 处添加,命名为“CloudFlare_DDNS”,内容如下:
# =========================================================
# --- Cloudflare DDNS 全自动脚本 for RouterOS v7 ---
# --- 自动从On Up事件获取IP和接口 ---
# =========================================================
# ----------------------------------------------------
# --- 参数配置 ---
# ----------------------------------------------------
# CloudFlare Token(DNS 权限)
:local cfToken "你的 Token 数据"
# CloudFlare 区域 ID
:local cfZoneId "你的区域 ID"
# --- 接口 -> 域名 映射关系 (在这里扩展) ---
# 如果只有一条拨号线路,只须将域名赋值给 cfDomain,将运营商名字赋值给 ISP;
# 后面两行 if 语句屏蔽即可。
:local cfDomain "lt.yourdomain.com"
:local ISP "联通"
# 多条线路时使用
# 如果有多条拨号线路,上面的 cfDomain 和 ISP 两个值留空(不可删除);
# 取消下面两条 if 语句前面的 #,并按实际情况填写 ifname(拨号接口名称)、cfDomain 和 ISP。
# if ($ifname = "pppoe-out-LT") do={:set cfDomain "lt.yourdomain.com";:set ISP "联通"}
# if ($ifname = "pppoe-out-YD") do={:set cfDomain "yd.yourdomain.com";:set ISP "移动"}
# 如果有第三个拨号,在这里继续添加。
# --- 调用钉钉发送通知(可选,关键字方式)---
# 不需要钉钉时可以将 url 变量留空,或者全部删除
:global dingtalk do={
:local keyword "关键字";
:local url "https://oapi.dingtalk.com/robot/send?access_token=你的钉钉token"
:local now ([/system/clock get date] ." ".[/system/clock get time])
:local data "{\"msgtype\": \"text\", \"text\": {\"content\": \"$now $1 $keyword\"}}"
:local header "Content-Type:application/json";
:log info "data: $data"
if ([:len $url] > 0) do={
do {
/tool/fetch http-method=post mode=https http-header-field="$header" http-data="$data" url="$url"
} on-error={log error "发送钉钉消息失败。"}
}
}
# ----------------------------------------------------
# --- 合法性检查 ---
# 接口检查
if ([:len $ifname] = 0) do={
:log warning "未获取到 PPPOE 接口名称,脚本退出。"
:quit
}
# 域名检查
if ([:len $cfDomain] = 0) do={
:log warning "域名为空,脚本退出。"
:quit
}
# 公网 IP 检查
if ([:len $currentIP] = 0) do={
:log warning "未获取到公网 IP 地址,脚本退出。"
:quit
} else={:log info "$ISP 接口名称:$ifname,公网 IP 地址:$currentIP,匹配域名:$cfDomain"}
# --- 查询Cloudflare上的DNS记录信息 ---
:local recordId ""
:local dnsIP ""
:local recordQueryUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records?name=$cfDomain&type=A"
:local queryResponse [/tool fetch url=$recordQueryUrl http-header-field="Authorization: Bearer $cfToken" as-value output=user]
if ([:find ($queryResponse->"data") "\"success\":true"] != nil) do={
:local responseData ($queryResponse->"data")
:local idStart [:find $responseData "\"id\":\""]
if ($idStart != nil) do={
:set idStart ($idStart + 6); :local idEnd [:find $responseData "\"" $idStart]; :set recordId [:pick $responseData $idStart $idEnd]
:local contentStart ([:find $responseData "\"content\":\""] + 11); :local contentEnd [:find $responseData "\"" $contentStart]; :set dnsIP [:pick $responseData $contentStart $contentEnd]
:log info "$ISP接口:域名 $cfDomain 当前解析记录为: $dnsIP"
} else={ :log error "$ISP接口:API调用成功,但未找到域名 $cfDomain 的 A 类型记录。"; :error "未找到DNS记录" }
} else={ :log error "$ISP接口:查询 DNS 信息失败。"; :error "查询DNS信息失败" }
# --- 对比IP并决定是否更新 ---
if ([:len $recordId] > 0) do={
if ($currentIP = $dnsIP) do={
:log info "$ISP接口:IP地址 ($currentIP) 未变更,无需更新。"
} else={
:log warning "$ISP接口:IP地址已变更 ($dnsIP -> $currentIP)。准备更新..."
# --- 执行更新操作 ---
:local cfApiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$recordId"
:local jsonData "{\"type\":\"A\",\"name\":\"$cfDomain\",\"content\":\"$currentIP\",\"ttl\":120,\"proxied\":false}"
:local updateResponse [/tool fetch url=$cfApiUrl http-method=put http-header-field="Authorization: Bearer $cfToken,Content-Type: application/json" http-data=$jsonData as-value output=user]
if ([:find ($updateResponse->"data") "\"success\":true"] != nil) do={
:local msg "$ISP接口:公网 IP 变更为 $currentIP,域名 $cfDomain 的DNS记录更新成功。"
:log info $msg
# ---发送钉钉消息,可选 ---
[$dingtalk $msg]
} else={ :log error "$ISP接口:DNS记录更新失败。" }
}
}
CloudFlare 的 Token 和 ZoneId 数据需要自行在 CloudFlare 官网上去设置或查找。
和其它脚本需要手工指定拨号接口、从 /ip/address 处查询 IP、甚至需要生成全局变量或临时文件不同,这个脚本的有趣之处在利用 On Up 事件时系统返回的端口 ID 号和 IP 地址,并巧妙地传递给了修改域名解析的脚本(RouterOS 脚本并不支持命令行方式传入参数)。
按照阿里云 OpenAPIV3 版本的签名机制,实现直接使用 HTTP 请求调用阿里云 OpenAPI 的 Delphi 单元,参考文档:https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature?spm=a2c4g.11186623.help-menu-262060.d_0_4_2.47934b19LiYUSI#3856b031d40ht
虽然现在 Delphi 没多少人用了,但仍是个很好用的工具。本单元在 Delphi 10.4.2 下开发,不依赖第三方库。用法比较简单,示例里都有。
目录说明:
备注:使用示例时须将 AliyunAPIClient.pas 复制到同一文件夹或添加到搜索路径。
此代码已上传至 github
第一次安装Appsmith,按照官方文档给出的 docker-compose.yml 创建了一个文件:
version: "3"
services:
appsmith:
image: index.docker.io/appsmith/appsmith-ee
container_name: appsmith
ports:
- "80:80"
- "443:443"
volumes:
- ./stacks:/appsmith-stacks
restart: unless-stopped然后执行:docker-compose up -d
安装一路顺利,完成后用浏览器正常打开了程序界面。填写安装向导,创建账户,然后就跳转到了登录界面。问题出现,用刚刚创建的账户密码无法登录,提示无效。但用随意输入的用户名和密码则会提示用户名密码不对之类的错误,说明系统已经验证通过了刚才的账户,是之后出了问题。
下面是我做的各种尝试:
尝试从 docker-compose.yml 文件中加入如下内容,然后重新创建容器,无效。
environment:
APPSMITH_ENCRYPTION_PASSWORD: abcd
APPSMITH_ENCRYPTION_SALT: abcd最后去翻看官方文档,在本地开发设置相关内容下提到,使用其项目包里如下文件创建容器:/deploy/docker/docker-compose.yml
# Use for running an Appsmith, during development.
version: "3"
services:
appsmith:
image: index.docker.io/appsmith/appsmith-ce:release
container_name: appsmith
ports:
- "8080:80"
environment:
APPSMITH_ENCRYPTION_PASSWORD: abcd
APPSMITH_ENCRYPTION_SALT: abcd
volumes:
- ./stacks:/appsmith-stacks尝试了一下,安装向导在创建完账户后是来到了数据库向导界面,然后就直接进入了程序开发界面。安装成功。
究其原因,是最初的配置文件里没有为镜像指定标签,被默认打上了 latest 标签,从而下载了不同的版本。
键盘映射工具 KeybMap V2.2 64位版本
功能:通过修改注册表来进行键盘键的重新定义。
文件大小:4.54M
适用系统:Windows VISTA/7/8/10/11 64位版本
界面:简体中文/英文
说明:本键盘映射工具仅修改注册表,不驻留内存。通过本工具,你可以将键盘上一些不常用的键定义为其它功能或者将其禁用,如将 ScrollLock 定义为静音、Wake Up 定义为减小音量、Sleep 定义为增大音量等等。
注意:
1、Power、WWW Home、Mute 等部分多媒体键(非 104 标准键)的映射只对 PS/2 键盘有效。若使用 USB 键盘则无法将其映射为其它键,除非使用 USB -> PS/2 转换器。
2、Pause 和 Fn 键不允许映射,程序已将其排除在外。
3、本版本只支持 64 位操作系统,使用时需要管理员权限。
4、32位版本的 KeybMap 在 64 位系统上可以运行,但映射功能无效,故不再提供。
更新:
2.2 版本修订了导入配置后界面不显示导入数据的 bug。
2.1 版本完善了导入导出流程,去除了当前用户的键盘映射功能(Windows7 之后操作系统不支持。但多媒体键重新定义动作是支持当前用户的,且优先级高于所有用户)。有人说看着键定义列表里有加号而没有等号感觉别扭,本来是写代码时为了绕过一个可能会出现的问题而故意这么做的,现在我还是改回来吧。
2.0 版本使用 Delphi 重写了一遍,优化了大量代码,修正了一处键名错误,补充了在非中文系统中遗漏的几处翻译。增加了几个键定义,屏蔽了 Windows 已经废弃的 RegisteredApp 功能。
1.9 版本修订了网友提出的在按键捕捉时的 bug,另外优化了一些相关流程。
1.8 版本暂时只发布了 64 位版本,因改用 Lazarus 编译,文件大小增加很多。高级功能里将当前用户和全局用户的定义分开排列,修正了类型为 Shellexecute 时指定的程序包含路径时导出为注册表文件时的一个问题。
1.7.3 版修正自定义键值无法保存问题,修正列表中动作状态刷新问题。
1.7.2 版增加对映射键适用范围的选择(当前用户/所有用户选择);允许自行输入键值进行映射;改进导入功能的兼容性。
1.7.1 版修正导入功能的一些问题。
1.7 版增加捕捉键值功能,可以识别大部分键盘的非标准扩展键位(注意:如果 PS/2 键盘使用了 USB 转换器,一些非标准扩展键位将会失效)。去掉程序的重启功能,改由用户自行操作。增加了键值显示。修正一个子窗口显示字体过大问题。
1.6 版恢复导入导出配置功能。
1.5 版增加对一些多媒体键定义修改功能。暂时屏蔽导入导出功能。
1.4 版增强对 VISTA 的兼容性,支持 Windows 7。
1.3 版修订由 1.2 版引起的键无法正确禁用问题。
1.2 版增补遗漏的 Z 键。
1.1 版修正多余两个键名问题。
Silence
下载:keybmap.rar
最近偶然需要用一下 Delphi 下的 Raize,使用时报 rzborder.pas 错误。记忆中多少年前就知道这是个编码识别的问题。放狗一搜,居然现在有人说是RZ的源码有问题,不能指向RZ的源码;还有人说是源码缺失},辛辛苦苦地去一行行修改,把类似下面这种:
( { Col0: $00; Col1: $7E; Col2: $14; Col3: $08; Col4: $00 ),改为:
( {} Col0: $00; Col1: $7E; Col2: $14; Col3: $08; Col4: $00 ),实际上人家的源码没有任何错误,只是在中文系统下 Delphi 使用默认的GB2312编码格式把特殊字符+}错误地识别成了汉字而已。
解决方法很简单:用 Emeditor 以西欧 Windows-1252 编码页打开 rzborder.pas 文件,然后另存为带签名的 utf-8 编码格式即可。你会发现上面那行代码其实是这样的:
( {þ} Col0: $00; Col1: $7E; Col2: $14; Col3: $08; Col4: $00 ),那个特殊字符 þ 的 16 进制编码是 FE,它后面的 } 是 7D,而汉字“”的 GB2312 编码恰好就是 16 进制的 FE7D。