Compare commits

...

4 Commits

Author SHA1 Message Date
Copilot
66700ebe1a
Add OpeniLink notification channel (#2988)
* Initial plan

* Add OpeniLink notification channel support

Agent-Logs-Url: https://github.com/whyour/qinglong/sessions/c80b4882-1bd7-4ffe-9180-cd3220da5986

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Add context_token and hub_url to OpeniLink notification channel

Agent-Logs-Url: https://github.com/whyour/qinglong/sessions/a5e66f5a-dab8-4a65-96ca-960d35fa9d50

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2026-04-25 16:01:36 +08:00
Copilot
07bf0c705b
fix: respect QlPort env var in Docker health check (#2963)
* Initial plan

* fix: use QlPort env variable in health check with fallback to default 5700

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2026-03-11 20:43:56 +08:00
whyour
fd516977e3 chore: upgrade nodemailer 2026-03-07 22:35:18 +08:00
whyour
c39f4ef846 chore: 更新 multer,解决 cve 漏洞 2026-03-07 21:31:24 +08:00
10 changed files with 741 additions and 264 deletions

View File

@ -20,6 +20,7 @@ export enum NotificationMode {
'chronocat' = 'Chronocat',
'ntfy' = 'ntfy',
'wxPusherBot' = 'wxPusherBot',
'openiLink' = 'openiLink',
}
abstract class NotificationBaseInfo {
@ -161,6 +162,12 @@ export class WxPusherBotNotification extends NotificationBaseInfo {
public wxPusherBotUids = '';
}
export class OpeniLinkNotification extends NotificationBaseInfo {
public openiLinkAppToken = '';
public openiLinkHubUrl = '';
public openiLinkContextToken = '';
}
export interface NotificationInfo
extends GoCqHttpBotNotification,
GotifyNotification,
@ -182,4 +189,5 @@ export interface NotificationInfo
ChronocatNotification,
LarkNotification,
NtfyNotification,
WxPusherBotNotification {}
WxPusherBotNotification,
OpeniLinkNotification {}

View File

@ -34,6 +34,7 @@ export default class NotificationService {
['chronocat', this.chronocat],
['ntfy', this.ntfy],
['wxPusherBot', this.wxPusherBot],
['openiLink', this.openiLink],
]);
private title = '';
@ -858,4 +859,35 @@ export default class NotificationService {
}
return {};
}
private async openiLink() {
const { openiLinkAppToken, openiLinkHubUrl, openiLinkContextToken } =
this.params;
const baseUrl = openiLinkHubUrl?.replace(/\/$/, '') || 'https://hub.openilink.com';
const url = `${baseUrl}/bot/v1/message/send`;
const body: Record<string, string> = {
type: 'text',
content: `${this.title}\n\n${this.content}`,
};
if (openiLinkContextToken) {
body.context_token = openiLinkContextToken;
}
try {
const res = await httpClient.post(url, {
...this.gotOption,
json: body,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${openiLinkAppToken}`,
},
});
if (res.ok) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
}

View File

@ -84,6 +84,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1
CMD curl -sf --noproxy '*' http://127.0.0.1:${QlPort:-5700}/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]

View File

@ -84,6 +84,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1
CMD curl -sf --noproxy '*' http://127.0.0.1:${QlPort:-5700}/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]

View File

@ -77,9 +77,9 @@
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"multer": "1.4.5-lts.1",
"multer": "2.1.1",
"node-schedule": "^2.1.0",
"nodemailer": "^6.9.16",
"nodemailer": "^8.0.1",
"p-queue-cjs": "7.3.4",
"@bufbuild/protobuf": "^2.10.0",
"ps-tree": "^1.2.0",

File diff suppressed because it is too large Load Diff

View File

@ -259,4 +259,13 @@ export WEBHOOK_METHOD=""
## 支持 text/plain、application/json、multipart/form-data、application/x-www-form-urlencoded
export WEBHOOK_CONTENT_TYPE=""
## 23. OpeniLink
## 官方文档: https://openilink.com/docs/hub/apps
## 在 OpeniLink Hub 后台安装 App 后获取 app_token
export OPENILINK_APP_TOKEN=""
## OpeniLink Hub 地址,默认为 https://hub.openilink.com自建 Hub 时填写自己的地址
export OPENILINK_HUB_URL=""
## OpeniLink 的 context_token用于标识消息会话上下文可从消息事件中获取
export OPENILINK_CONTEXT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可

View File

@ -151,6 +151,11 @@ const push_config = {
WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken
WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
WXPUSHER_UIDS: '', // wxpusher 的 用户ID多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
// 官方文档: https://openilink.com/docs/hub/apps
OPENILINK_APP_TOKEN: '', // OpeniLink 的 app_token在 OpeniLink Hub 后台安装 App 后获取
OPENILINK_HUB_URL: '', // OpeniLink Hub 地址,默认为 https://hub.openilink.com自建 Hub 时填写自己的地址
OPENILINK_CONTEXT_TOKEN: '', // OpeniLink 的 context_token用于标识消息会话上下文可从消息事件中获取
};
for (const key in push_config) {
@ -1408,6 +1413,54 @@ function wxPusherNotify(text, desp) {
});
}
function openiLinkNotify(text, desp) {
return new Promise((resolve) => {
const { OPENILINK_APP_TOKEN, OPENILINK_HUB_URL, OPENILINK_CONTEXT_TOKEN } =
push_config;
if (OPENILINK_APP_TOKEN) {
const baseUrl = OPENILINK_HUB_URL
? OPENILINK_HUB_URL.replace(/\/$/, '')
: 'https://hub.openilink.com';
const body = {
type: 'text',
content: `${text}\n\n${desp}`,
};
if (OPENILINK_CONTEXT_TOKEN) {
body.context_token = OPENILINK_CONTEXT_TOKEN;
}
const options = {
url: `${baseUrl}/bot/v1/message/send`,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENILINK_APP_TOKEN}`,
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('OpeniLink 发送通知消息失败!\n', err);
} else {
if (data.ok) {
console.log('OpeniLink 发送通知消息成功!');
} else {
console.log(`OpeniLink 发送通知消息异常:${data.error}`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function parseString(input, valueFormatFn) {
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
const matches = {};
@ -1538,6 +1591,7 @@ async function sendNotify(text, desp, params = {}) {
qmsgNotify(text, desp), // 自定义通知
ntfyNotify(text, desp), // Ntfy
wxPusherNotify(text, desp), // wxpusher
openiLinkNotify(text, desp), // OpeniLink
]);
}

View File

@ -135,6 +135,10 @@ push_config = {
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
'OPENILINK_APP_TOKEN': '', # OpeniLink 的 app_token在 OpeniLink Hub 后台安装 App 后获取 官方文档: https://openilink.com/docs/hub/apps
'OPENILINK_HUB_URL': '', # OpeniLink Hub 地址,默认为 https://hub.openilink.com自建 Hub 时填写自己的地址
'OPENILINK_CONTEXT_TOKEN': '', # OpeniLink 的 context_token用于标识消息会话上下文可从消息事件中获取
}
# fmt: on
@ -898,6 +902,43 @@ def wxpusher_bot(title: str, content: str) -> None:
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
def openilink(title: str, content: str) -> None:
"""
通过 OpeniLink 推送消息
支持的环境变量:
- OPENILINK_APP_TOKEN: OpeniLink Hub 后台安装 App 后获取的 app_token
- OPENILINK_HUB_URL: OpeniLink Hub 地址默认为 https://hub.openilink.com
- OPENILINK_CONTEXT_TOKEN: 消息会话上下文 token可从消息事件中获取
"""
if not push_config.get("OPENILINK_APP_TOKEN"):
return
print("OpeniLink 服务启动")
base_url = (
push_config.get("OPENILINK_HUB_URL", "").rstrip("/")
or "https://hub.openilink.com"
)
url = f"{base_url}/bot/v1/message/send"
headers = {
"Content-Type": "application/json",
"Authorization": f'Bearer {push_config.get("OPENILINK_APP_TOKEN")}',
}
data = {
"type": "text",
"content": f"{title}\n\n{content}",
}
if push_config.get("OPENILINK_CONTEXT_TOKEN"):
data["context_token"] = push_config.get("OPENILINK_CONTEXT_TOKEN")
response = requests.post(url=url, json=data, headers=headers).json()
if response.get("ok"):
print("OpeniLink 推送成功!")
else:
print(f'OpeniLink 推送失败!错误信息:{response.get("error")}')
def parse_headers(headers):
if not headers:
return {}
@ -1063,6 +1104,8 @@ def add_notify_function():
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
):
notify_function.append(wxpusher_bot)
if push_config.get("OPENILINK_APP_TOKEN"):
notify_function.append(openilink)
if not notify_function:
print(f"无推送渠道,请检查通知变量是否正确")
return notify_function

View File

@ -98,6 +98,7 @@ export default {
{ value: 'pushPlus', label: 'PushPlus' },
{ value: 'wePlusBot', label: intl.get('微加机器人') },
{ value: 'wxPusherBot', label: 'wxPusher' },
{ value: 'openiLink', label: 'OpeniLink' },
{ value: 'chat', label: intl.get('群晖chat') },
{ value: 'email', label: intl.get('邮箱') },
{ value: 'lark', label: intl.get('飞书机器人') },
@ -387,6 +388,27 @@ export default {
required: false,
},
],
openiLink: [
{
label: 'openiLinkAppToken',
tip: intl.get(
'OpeniLink的app_token在OpeniLink Hub后台安装App后获取参考 https://openilink.com/docs/hub/apps',
),
required: true,
},
{
label: 'openiLinkHubUrl',
tip: intl.get(
'OpeniLink Hub地址默认为 https://hub.openilink.com自建Hub时填写自己的地址',
),
},
{
label: 'openiLinkContextToken',
tip: intl.get(
'OpeniLink的context_token用于标识消息会话上下文可从消息事件中获取',
),
},
],
lark: [
{
label: 'larkKey',