@qingtui/openclaw-plugin 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,99 @@
1
- # Temporary Holding Version
1
+ # OpenClaw 轻推渠道插件
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 将轻推(Qingtui / Nudge)机器人接入 OpenClaw,支持私聊、群聊、多机器人、动态白名单、文本与媒体发送、流式回复。入站通过 Socket 长连接接收,不需要部署 HTTP webhook。
4
+
5
+ ## 文档导航
6
+
7
+ - [开发指南](docs/开发指南.md):依赖与工具版本、源码结构、构建、本地链接开发、现有脚本的适用范围。
8
+ - [调试指南](docs/调试指南.md):隔离环境冒烟、运行日志、真实消息验收、连接与权限故障定位。
9
+ - [发布指南](docs/发布指南.md):版本同步、打包检查、安装验收、npm / ClawHub 发布、升级与回滚。
10
+
11
+ 文档以仓库的 `package.json`、`pnpm-lock.yaml`、`pnpm-workspace.yaml`、`tsconfig.json`、`openclaw.plugin.json` 和当前源码为准。
12
+
13
+ ## 三个名称不要混用
14
+
15
+ | 用途 | 标识 | 使用位置 |
16
+ | --- | --- | --- |
17
+ | npm 包名 | `@qingtui/openclaw-plugin` | 安装依赖、npm 发布 |
18
+ | 插件 ID | `openclaw-nudge` | `openclaw plugins inspect/enable`、`plugins.entries` |
19
+ | 渠道 ID | `qingtui` | `channels.qingtui`、机器人账号配置 |
20
+
21
+ ## 从源码开始
22
+
23
+ 要求 OpenClaw `>=2026.10.8`;本项目按 `2026.10.8` 构建。该宿主要求 Node.js `>=24.22.1 <25 || >=26.12.1`。依赖版本和 pnpm 使用说明见[开发指南](docs/开发指南.md#环境与依赖基线)。
24
+
25
+ 在仓库根目录安装并构建:
26
+
27
+ ```bash
28
+ pnpm install --frozen-lockfile
29
+ pnpm run build
30
+ ```
31
+
32
+ 准备**安装在本仓库之外**的 OpenClaw 宿主,并确认当前 shell 的 `openclaw` 指向它。接下来的命令会修改当前 OpenClaw 配置;仅验证加载时,请改用[隔离调试流程](docs/调试指南.md)。
33
+
34
+ ```bash
35
+ command -v openclaw
36
+ openclaw --version
37
+ openclaw plugins install --link .
38
+ openclaw plugins inspect openclaw-nudge --json
39
+ ```
40
+
41
+ `--link` 只登记路径,不复制文件;不能与 `--force` 同用。若安装策略阻止操作,先审查提示与宿主策略,不要绕过安全检查。需要启用插件或确认其声明能力时,先审查插件,再手动执行:
42
+
43
+ ```bash
44
+ openclaw plugins enable openclaw-nudge --accept-capabilities
45
+ ```
46
+
47
+ 发布入口是 `dist/index.js`,配置向导入口是 `dist/setup-entry.js`。修改源码后需要重新 `pnpm run build`,再用外部宿主执行 `openclaw gateway restart`。不要用本仓库的 `pnpm exec openclaw gateway run` 加载本仓库:OpenClaw `2026.10.x` 在这种目录布局下存在源码捕获问题,详见调试指南。
48
+
49
+ ## 配置机器人
50
+
51
+ 推荐交互配置,避免把密钥写进 shell 历史:
52
+
53
+ ```bash
54
+ openclaw channels add
55
+ ```
56
+
57
+ 在向导中选择轻推渠道,选择或新建账号,依次输入 `appid`、`appsecret` 和轻推服务地址(缺省 `https://open.qingtui.com`,QA 环境填 `https://qaqtopen.qingtui.com`)。录入后回到渠道列表,必须选择 **Finished** 并继续到 **Channels updated.** 才会保存;中途取消不会写盘。外部渠道不要使用 `channels add --channel qingtui`。
58
+
59
+ `appsecret` 以掩码方式录入,重跑向导时只询问是否保留、不回显已存密钥。`appid` 仍为明文输入。
60
+
61
+ 配置位于默认的 `~/.openclaw/openclaw.json`,也可由 `OPENCLAW_CONFIG_PATH` 指定。以下仅展示需要合并到现有配置的分节,不能用它覆盖整个配置文件;占位值必须换成真实凭证:
62
+
63
+ ```json5
64
+ {
65
+ channels: {
66
+ qingtui: {
67
+ streaming: true,
68
+ serverUrl: "https://open.qingtui.com",
69
+ accounts: {
70
+ default: {
71
+ enabled: true,
72
+ appid: "替换为机器人ID",
73
+ appsecret: "替换为机器人密钥",
74
+ },
75
+ // 多机器人时继续增加账号,例如 bot2: { appid: "...", appsecret: "..." }
76
+ },
77
+ },
78
+ },
79
+ }
80
+ ```
81
+
82
+ - `appid` / `appsecret` 必须是字符串,且只能位于 `accounts.<账号ID>`,包括 `accounts.default`;不能放到渠道顶层。
83
+ - `streaming` 缺省开启,设为 `false` 使用普通回复。
84
+ - `serverUrl` 是轻推服务(Socket 代理)地址,缺省 `https://open.qingtui.com`;QA 环境为 `https://qaqtopen.qingtui.com`。所有账号共用。
85
+ - `openUrl` 是 token、白名单、媒体上传等 **REST API** 的基地址,可选;未配置时与 `serverUrl` 相同,只有两者不同域时才需要单独设置。
86
+ - **从 1.0.x 升级**:旧版 Socket 固定连 QA(`qaqtopen`),新版缺省连生产。仍要连 QA 时需显式设置 `serverUrl: "https://qaqtopen.qingtui.com"`。
87
+ - 私聊与群聊都使用轻推平台同步的同一份动态白名单,按机器人账号隔离;没有可配置的 `dmPolicy`、`groupPolicy`、`allowFrom` 或 `groupAllowFrom`。
88
+ - 临时禁用某个机器人时设置 `accounts.<账号ID>.enabled: false`;禁用整个渠道时设置 `channels.qingtui.enabled: false`。两者都需要重启网关后生效。
89
+
90
+ 保存后验证并重启:
91
+
92
+ ```bash
93
+ openclaw config validate
94
+ openclaw gateway restart
95
+ openclaw plugins inspect openclaw-nudge --runtime --json
96
+ openclaw gateway health
97
+ ```
98
+
99
+ `inspect` 成功只证明插件能加载;`health` 中的 `qingtui: not-running` 也不能直接判定 Socket 故障。真实账号应结合“socket 认证成功”“已连接轻推消息代理”“动态白名单已同步”日志,并实际收发一条消息验收,详见[调试指南](docs/调试指南.md)。
package/dist/index.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * openclaw-nudge —— OpenClaw 外部渠道插件:把「轻推」(nudge)接入 OpenClaw。
3
+ *
4
+ * 【给不熟悉 OpenClaw 的读者】渠道插件 = 一组约定的导出,告诉 OpenClaw:
5
+ * 1. 渠道怎么配置(配置在 ~/.openclaw/openclaw.json 的
6
+ * channels["qingtui"] 里;一个账号 = 一个轻推机器人)
7
+ * 2. 消息怎么进来(本插件用 WebSocket 长连接,轻推服务器主动推送,
8
+ * 不暴露 HTTP webhook,无需公网地址)
9
+ * 3. OpenClaw 的回复怎么发回去(outbound)
10
+ *
11
+ * 文件导览(建议阅读顺序):
12
+ * src/config.ts 配置长什么样、怎么读(账号/凭证/服务地址模型)
13
+ * src/setup-wizard.ts 交互式配置向导(openclaw channels add 的提问流程)
14
+ * src/channel.ts 渠道插件组装:能力声明、私聊安全策略、出站发送
15
+ * src/client.ts 轻推连接层:WebSocket 连接池、动态白名单缓存、发送 API
16
+ * src/service.ts 后台服务:逐账号建连、定时同步白名单
17
+ * src/event.ts 推送负载归一化与按账号去重
18
+ * src/inbound.ts 入站流水线:推送如何变成 agent 的一轮对话
19
+ * src/stream.ts 单聊流式回复:快照转增量、节流、保活与收尾
20
+ * src/target.ts 出站目标 user:/channel: 编解码
21
+ *
22
+ * 消息流向:
23
+ * 入站 轻推服务器 --WebSocket--> service.ts --> inbound.ts --> OpenClaw agent
24
+ * 出站 agent 回复 --> channel.ts(outbound) --> client.ts --> 轻推服务器
25
+ *
26
+ * 本文件只做三件事:setRuntime 把 PluginRuntime 句柄存入 runtime store(入站派发要用)、
27
+ * registerCliMetadata 注册 CLI 帮助描述、registerFull 注册后台 service
28
+ * (实现在 src/service.ts)。
29
+ */
30
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
31
+ import { createPluginRuntimeStore, } from "openclaw/plugin-sdk/runtime-store";
32
+ import { nudgeChatPlugin } from "./src/channel.js";
33
+ import { createNudgeInboundService } from "./src/service.js";
34
+ // 宿主运行时句柄:按插件 ID 存在全局命名槽位,SDK 模块被重复加载时也共享同一份
35
+ const runtimeStore = createPluginRuntimeStore({
36
+ pluginId: "openclaw-nudge",
37
+ errorMessage: "openclaw-nudge: plugin runtime is not initialized",
38
+ });
39
+ export default defineChannelPluginEntry({
40
+ id: "openclaw-nudge",
41
+ name: "Openclaw Nudge",
42
+ description: "connect nudge to OpenClaw",
43
+ plugin: nudgeChatPlugin,
44
+ setRuntime: runtimeStore.setRuntime,
45
+ registerCliMetadata(api) {
46
+ api.registerCli(({ program }) => {
47
+ program.command("openclaw-nudge").description("Nudge Chat management");
48
+ }, {
49
+ descriptors: [
50
+ {
51
+ name: "openclaw-nudge",
52
+ description: "Nudge Chat management",
53
+ hasSubcommands: false,
54
+ },
55
+ ],
56
+ });
57
+ },
58
+ registerFull(api) {
59
+ // registerFull 在 "tool-discovery" 模式也会被调用,只有 full 模式
60
+ // 才允许建长连接这类重副作用(docs/plugins/sdk-entrypoints.md)。
61
+ if (api.registrationMode !== "full")
62
+ return;
63
+ api.registerService(createNudgeInboundService(() => runtimeStore.tryGetRuntime()));
64
+ },
65
+ });
@@ -0,0 +1,5 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ // setup-only 入口:`openclaw channels add` 等配置类命令只加载本文件
3
+ // (不建连、不起 service);package.json 的 openclaw.setupEntry 指向这里。
4
+ import { nudgeChatPlugin } from "./src/channel.js";
5
+ export default defineSetupPluginEntry(nudgeChatPlugin);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 渠道插件组装:把配置、向导、安全策略、出站发送拼成 OpenClaw 渠道插件对象。
3
+ *
4
+ * 【新手须知】createChatChannelPlugin 是 OpenClaw 聊天渠道的标准工厂:
5
+ * base.meta 渠道在 CLI/UI 里的展示信息
6
+ * base.setupWizard 交互式配置向导(实现在 src/setup-wizard.ts)
7
+ * base.capabilities 支持的会话类型(direct 单聊 / group 群聊)
8
+ * base.config 账号枚举与解析(实现在 src/config.ts)
9
+ * base.setup onboarding 时的账号命名/写盘回调
10
+ * security.dm 私聊准入:固定 allowlist,白名单 = 轻推服务器同步的
11
+ * 运行时缓存(src/client.ts),无静态配置项、不走 pairing
12
+ * threading 回复引用模式
13
+ * outbound 出站:agent 回复经 src/client.ts 的 nudgeChatApi 发回轻推
14
+ */
15
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
16
+ import { applyAccountNameToChannelSection } from "openclaw/plugin-sdk/setup";
17
+ import { getDynamicAllowFrom, nudgeChatApi } from "./client.js";
18
+ import { isNudgeAccountConfigured, isNudgeChannelEnabled, listNudgeAccountIds, NUDGE_CHANNEL_ID, patchNudgeAccountConfig, resolveAccount, resolveDefaultNudgeAccountId, } from "./config.js";
19
+ import { nudgeSetupWizard } from "./setup-wizard.js";
20
+ export const nudgeChatPlugin = createChatChannelPlugin({
21
+ // 注意:不用 createChannelPluginBase —— 它把 capabilities 标成可选,
22
+ // 而 createChatChannelPlugin 要求必填,直接过类型检查会报
23
+ // TS2322(SDK 声明缺陷)。内置渠道同样直接传对象字面量。
24
+ base: {
25
+ id: NUDGE_CHANNEL_ID,
26
+ // 外部插件不在内置渠道目录里,meta 必须显式给全
27
+ // (resolveSdkChatChannelMeta 只认 bundled channel id)。
28
+ meta: {
29
+ id: NUDGE_CHANNEL_ID,
30
+ label: "qingtui",
31
+ selectionLabel: "qingtui (轻推)",
32
+ docsPath: "/channels/qingtui",
33
+ blurb: "connect qingtui to OpenClaw",
34
+ },
35
+ setupWizard: nudgeSetupWizard,
36
+ capabilities: { chatTypes: ["direct", "group"] },
37
+ // 账号解析/检查放 config,不放 setup(setup 只管 onboarding 写回)
38
+ config: {
39
+ listAccountIds: listNudgeAccountIds,
40
+ defaultAccountId: resolveDefaultNudgeAccountId,
41
+ resolveAccount,
42
+ inspectAccount(cfg, accountId) {
43
+ const configured = isNudgeAccountConfigured(cfg, accountId);
44
+ return {
45
+ // 渠道总开关关闭时 service 不建连,账号视为未启用
46
+ enabled: configured && isNudgeChannelEnabled(cfg),
47
+ configured,
48
+ tokenStatus: configured ? "available" : "missing",
49
+ };
50
+ },
51
+ },
52
+ setup: {
53
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
54
+ cfg,
55
+ channelKey: NUDGE_CHANNEL_ID,
56
+ accountId,
57
+ name,
58
+ alwaysUseAccounts: true,
59
+ }),
60
+ applyAccountConfig: ({ cfg, accountId, input }) => patchNudgeAccountConfig({ cfg, accountId, patch: input }),
61
+ },
62
+ },
63
+ // DM 安全:固定 allowlist(白名单来自开放平台可见范围同步,不走 pairing)
64
+ security: {
65
+ dm: {
66
+ channelKey: NUDGE_CHANNEL_ID,
67
+ resolvePolicy: () => "allowlist",
68
+ // 生效白名单由 service 按账号后台定时刷新,见 client.ts getDynamicAllowFrom
69
+ resolveAllowFrom: (account) => [
70
+ ...getDynamicAllowFrom(account.accountId),
71
+ ],
72
+ },
73
+ },
74
+ // 回复携带 replyToId;群聊引用消息由 nudgeChatApi.sendMessage 按
75
+ // sendType 升级为 type="ref",单聊忽略 replyToId 发普通文本
76
+ threading: { topLevelReplyToMode: "reply" },
77
+ // 出站:OpenClaw 回复 -> nudge
78
+ outbound: {
79
+ attachedResults: {
80
+ channel: NUDGE_CHANNEL_ID,
81
+ sendText: async (ctx) => {
82
+ const result = await nudgeChatApi.sendMessage({
83
+ accountId: ctx.accountId,
84
+ to: ctx.to,
85
+ text: ctx.text,
86
+ replyToId: ctx.replyToId,
87
+ });
88
+ return { messageId: result.id, chatId: result.chatId };
89
+ },
90
+ // 媒体读取交给宿主加载器并带上宿主授予的读取能力:远程 URL 受
91
+ // SSRF 策略约束,本地路径只允许授权根目录
92
+ sendMedia: async (ctx) => {
93
+ const result = await nudgeChatApi.sendFile({
94
+ accountId: ctx.accountId,
95
+ to: ctx.to,
96
+ mediaUrl: ctx.mediaUrl ?? "",
97
+ text: ctx.text,
98
+ mediaAccess: {
99
+ localRoots: ctx.mediaAccess?.localRoots ?? ctx.mediaLocalRoots,
100
+ readFile: ctx.mediaAccess?.readFile ?? ctx.mediaReadFile,
101
+ workspaceDir: ctx.mediaAccess?.workspaceDir,
102
+ },
103
+ });
104
+ return { messageId: result.id, chatId: result.chatId };
105
+ },
106
+ },
107
+ // base 里放通用出站策略;deliveryMode 是必填项
108
+ base: {
109
+ deliveryMode: "direct",
110
+ },
111
+ },
112
+ });