@zhin.js/adapter-weixin-ilink 0.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 +96 -0
- package/client/LoginAssistPanel.tsx +200 -0
- package/client/WeixinIlinkManagement.tsx +139 -0
- package/client/index.tsx +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/api.ts +30 -0
- package/dist/index.js +29 -0
- package/lib/adapter.d.ts +11 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +14 -0
- package/lib/adapter.js.map +1 -0
- package/lib/aes-ecb.d.ts +7 -0
- package/lib/aes-ecb.d.ts.map +1 -0
- package/lib/aes-ecb.js +19 -0
- package/lib/aes-ecb.js.map +1 -0
- package/lib/bot.d.ts +37 -0
- package/lib/bot.d.ts.map +1 -0
- package/lib/bot.js +383 -0
- package/lib/bot.js.map +1 -0
- package/lib/cdn-upload.d.ts +18 -0
- package/lib/cdn-upload.d.ts.map +1 -0
- package/lib/cdn-upload.js +73 -0
- package/lib/cdn-upload.js.map +1 -0
- package/lib/cdn-url.d.ts +14 -0
- package/lib/cdn-url.d.ts.map +1 -0
- package/lib/cdn-url.js +14 -0
- package/lib/cdn-url.js.map +1 -0
- package/lib/context-store.d.ts +2 -0
- package/lib/context-store.d.ts.map +1 -0
- package/lib/context-store.js +2 -0
- package/lib/context-store.js.map +1 -0
- package/lib/credentials.d.ts +15 -0
- package/lib/credentials.d.ts.map +1 -0
- package/lib/credentials.js +49 -0
- package/lib/credentials.js.map +1 -0
- package/lib/ilink-api.d.ts +81 -0
- package/lib/ilink-api.d.ts.map +1 -0
- package/lib/ilink-api.js +273 -0
- package/lib/ilink-api.js.map +1 -0
- package/lib/ilink-config-cache.d.ts +19 -0
- package/lib/ilink-config-cache.d.ts.map +1 -0
- package/lib/ilink-config-cache.js +64 -0
- package/lib/ilink-config-cache.js.map +1 -0
- package/lib/ilink-logger.d.ts +10 -0
- package/lib/ilink-logger.d.ts.map +1 -0
- package/lib/ilink-logger.js +27 -0
- package/lib/ilink-logger.js.map +1 -0
- package/lib/ilink-meta.d.ts +13 -0
- package/lib/ilink-meta.d.ts.map +1 -0
- package/lib/ilink-meta.js +57 -0
- package/lib/ilink-meta.js.map +1 -0
- package/lib/ilink-random.d.ts +11 -0
- package/lib/ilink-random.d.ts.map +1 -0
- package/lib/ilink-random.js +16 -0
- package/lib/ilink-random.js.map +1 -0
- package/lib/ilink-redact.d.ts +21 -0
- package/lib/ilink-redact.d.ts.map +1 -0
- package/lib/ilink-redact.js +54 -0
- package/lib/ilink-redact.js.map +1 -0
- package/lib/ilink-session-guard.d.ts +16 -0
- package/lib/ilink-session-guard.d.ts.map +1 -0
- package/lib/ilink-session-guard.js +49 -0
- package/lib/ilink-session-guard.js.map +1 -0
- package/lib/ilink-types.d.ts +233 -0
- package/lib/ilink-types.d.ts.map +1 -0
- package/lib/ilink-types.js +35 -0
- package/lib/ilink-types.js.map +1 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +38 -0
- package/lib/index.js.map +1 -0
- package/lib/login-assist-routes.d.ts +7 -0
- package/lib/login-assist-routes.d.ts.map +1 -0
- package/lib/login-assist-routes.js +29 -0
- package/lib/login-assist-routes.js.map +1 -0
- package/lib/login.d.ts +6 -0
- package/lib/login.d.ts.map +1 -0
- package/lib/login.js +121 -0
- package/lib/login.js.map +1 -0
- package/lib/media-download.d.ts +19 -0
- package/lib/media-download.d.ts.map +1 -0
- package/lib/media-download.js +95 -0
- package/lib/media-download.js.map +1 -0
- package/lib/mime.d.ts +9 -0
- package/lib/mime.d.ts.map +1 -0
- package/lib/mime.js +99 -0
- package/lib/mime.js.map +1 -0
- package/lib/outbound-media.d.ts +5 -0
- package/lib/outbound-media.d.ts.map +1 -0
- package/lib/outbound-media.js +122 -0
- package/lib/outbound-media.js.map +1 -0
- package/lib/pic-decrypt.d.ts +10 -0
- package/lib/pic-decrypt.d.ts.map +1 -0
- package/lib/pic-decrypt.js +89 -0
- package/lib/pic-decrypt.js.map +1 -0
- package/lib/routes.d.ts +8 -0
- package/lib/routes.d.ts.map +1 -0
- package/lib/routes.js +29 -0
- package/lib/routes.js.map +1 -0
- package/lib/silk-transcode.d.ts +9 -0
- package/lib/silk-transcode.d.ts.map +1 -0
- package/lib/silk-transcode.js +64 -0
- package/lib/silk-transcode.js.map +1 -0
- package/lib/types.d.ts +26 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/typing-indicator.d.ts +32 -0
- package/lib/typing-indicator.d.ts.map +1 -0
- package/lib/typing-indicator.js +95 -0
- package/lib/typing-indicator.js.map +1 -0
- package/lib/upload.d.ts +43 -0
- package/lib/upload.d.ts.map +1 -0
- package/lib/upload.js +106 -0
- package/lib/upload.js.map +1 -0
- package/lib/weixin-inbound.d.ts +71 -0
- package/lib/weixin-inbound.d.ts.map +1 -0
- package/lib/weixin-inbound.js +201 -0
- package/lib/weixin-inbound.js.map +1 -0
- package/lib/weixin-send-media.d.ts +22 -0
- package/lib/weixin-send-media.d.ts.map +1 -0
- package/lib/weixin-send-media.js +54 -0
- package/lib/weixin-send-media.js.map +1 -0
- package/lib/weixin-send.d.ts +65 -0
- package/lib/weixin-send.d.ts.map +1 -0
- package/lib/weixin-send.js +181 -0
- package/lib/weixin-send.js.map +1 -0
- package/package.json +91 -0
- package/src/adapter.ts +16 -0
- package/src/aes-ecb.ts +21 -0
- package/src/bot.ts +483 -0
- package/src/cdn-upload.test.ts +177 -0
- package/src/cdn-upload.ts +87 -0
- package/src/cdn-url.ts +20 -0
- package/src/context-store.ts +8 -0
- package/src/credentials.ts +64 -0
- package/src/ilink-api.ts +349 -0
- package/src/ilink-config-cache.ts +79 -0
- package/src/ilink-logger.ts +36 -0
- package/src/ilink-meta.ts +60 -0
- package/src/ilink-random.ts +17 -0
- package/src/ilink-redact.ts +54 -0
- package/src/ilink-session-guard.ts +58 -0
- package/src/ilink-types.ts +261 -0
- package/src/index.ts +60 -0
- package/src/login-assist-routes.ts +36 -0
- package/src/login.ts +166 -0
- package/src/media-download.ts +149 -0
- package/src/mime.test.ts +74 -0
- package/src/mime.ts +105 -0
- package/src/outbound-media.ts +141 -0
- package/src/pic-decrypt.ts +101 -0
- package/src/routes.ts +39 -0
- package/src/silk-transcode.test.ts +58 -0
- package/src/silk-transcode.ts +74 -0
- package/src/silk-wasm.d.ts +6 -0
- package/src/types.ts +26 -0
- package/src/typing-indicator.ts +139 -0
- package/src/upload.test.ts +305 -0
- package/src/upload.ts +157 -0
- package/src/weixin-inbound.ts +257 -0
- package/src/weixin-send-media.ts +72 -0
- package/src/weixin-send.ts +237 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @zhin.js/adapter-weixin-ilink
|
|
2
|
+
|
|
3
|
+
通过微信 **iLink Bot API**(ClawBot 灰度入口)接入**个人微信**,支持文本与全量媒体收发、输入状态提示。
|
|
4
|
+
|
|
5
|
+
协议实现移植自 [Tencent/openclaw-weixin](https://github.com/Tencent/openclaw-weixin)(MIT),已去除 OpenClaw 耦合,`bot_agent` 默认为 `Zhin.js/<version>`。
|
|
6
|
+
|
|
7
|
+
## 与 wechat-mp 的区别
|
|
8
|
+
|
|
9
|
+
| 项 | weixin-ilink | wechat-mp |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 账号类型 | 个人微信(ClawBot) | 微信公众号 |
|
|
12
|
+
| 入站 | 长轮询 `getupdates` | Webhook 被动回复 |
|
|
13
|
+
| 登录 | 扫码 + 侧车凭证文件 | AppId/Secret/Token |
|
|
14
|
+
| 群聊 | 不支持(仅私聊) | 支持 |
|
|
15
|
+
|
|
16
|
+
可与 `@zhin.js/adapter-wechat-mp` **同时启用**。
|
|
17
|
+
|
|
18
|
+
## 前置条件
|
|
19
|
+
|
|
20
|
+
- 微信客户端需已灰度 **ClawBot** 入口(最新版微信 + 灰度资格)
|
|
21
|
+
- Node.js ^20.19.0 或 >=22.12.0
|
|
22
|
+
- 可选:`@zhin.js/host-router` + `@zhin.js/host-api` 用于 Web 控制台扫码登录
|
|
23
|
+
|
|
24
|
+
## 最小配置
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
bots:
|
|
28
|
+
- context: weixin-ilink
|
|
29
|
+
name: my-wechat
|
|
30
|
+
# botAgent: "Zhin.js/1.0.0" # 可选,观测用
|
|
31
|
+
# longPollTimeoutMs: 35000 # 可选
|
|
32
|
+
# baseUrl: https://ilinkai.weixin.qq.com
|
|
33
|
+
|
|
34
|
+
plugins:
|
|
35
|
+
- "@zhin.js/adapter-weixin-ilink"
|
|
36
|
+
- "@zhin.js/host-router" # 可选,Console + login-assist API
|
|
37
|
+
- "@zhin.js/host-api" # 可选
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`zhin.config.yml` **只写 bot `name`**;`botToken` 保存在侧车文件(不进 git):
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
data/weixin-ilink/<bot-name>.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
也可通过环境变量 `WEIXIN_ILINK_TOKEN` 或配置项 `botToken` 覆盖(CI 场景)。
|
|
47
|
+
|
|
48
|
+
## 登录流程
|
|
49
|
+
|
|
50
|
+
1. 首次启动无凭证时,bot 调用 `get_bot_qrcode` 并在控制台展示二维码(`loginAssist`)
|
|
51
|
+
2. 用户用微信扫码确认
|
|
52
|
+
3. 后台轮询 `get_qrcode_status` 直至 `confirmed`,写入 `data/weixin-ilink/<name>.json`
|
|
53
|
+
4. 调用 `notifyStart` 后进入长轮询
|
|
54
|
+
|
|
55
|
+
与 Host 同时启用时:
|
|
56
|
+
|
|
57
|
+
- Console 页面:`/weixin-ilink`(概览 + 登录辅助)
|
|
58
|
+
- HTTP API:`GET /api/login-assist/pending`、`POST /api/login-assist/submit`、`POST /api/login-assist/cancel`
|
|
59
|
+
- Bot 状态:`GET /api/weixin-ilink/bots`
|
|
60
|
+
|
|
61
|
+
## Typing Indicator
|
|
62
|
+
|
|
63
|
+
私聊使用 iLink `sendTyping` API,在微信侧显示**正在输入**(非文本消息、非 reaction)。长任务期间每 5s 保活一次(`keepaliveIntervalMs` 可配)。
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
typingIndicator:
|
|
67
|
+
enabled: true
|
|
68
|
+
keepaliveIntervalMs: 5000
|
|
69
|
+
privateConfig:
|
|
70
|
+
type: typing
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 出站说明
|
|
74
|
+
|
|
75
|
+
回复必须携带入站时缓存的 `context_token`(按 `botId + peerUserId`)。若用户久未发消息导致 token 缺失,出站会拒绝并打 warn。
|
|
76
|
+
|
|
77
|
+
所有出站走标准链路:`Message.$reply` / `Adapter.sendMessage` → `before.sendMessage` → `bot.$sendMessage`。
|
|
78
|
+
|
|
79
|
+
**图文限制**:微信单条消息不支持图文混排。适配器会自动:
|
|
80
|
+
- **出站**:先单独发文本,再发纯媒体(不带 caption)
|
|
81
|
+
- **入站**:若同一条 iLink 消息同时含文字与媒体,拆成两条 `message.receive` 事件
|
|
82
|
+
|
|
83
|
+
## 故障排查
|
|
84
|
+
|
|
85
|
+
| 现象 | 可能原因 |
|
|
86
|
+
|---|---|
|
|
87
|
+
| 无法扫码 / 无 ClawBot 入口 | 未灰度;需最新微信 + 灰度资格 |
|
|
88
|
+
| 二维码过期 | 重启 bot 重新获取 |
|
|
89
|
+
| 发不出消息 | 缺少 `context_token`;让用户先发一条消息 |
|
|
90
|
+
| `errcode -14` | 会话过期,适配器自动暂停 1 小时后重试 |
|
|
91
|
+
| Console 无待办 | 确认 `host-router` 已加载且访问 `/weixin-ilink` |
|
|
92
|
+
|
|
93
|
+
## 文档
|
|
94
|
+
|
|
95
|
+
- 架构与计划:仓库内 weixin-ilink 实现计划
|
|
96
|
+
- 适配器索引:[`plugins/adapters/README.md`](../README.md)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
LogIn,
|
|
4
|
+
QrCode,
|
|
5
|
+
AlertCircle,
|
|
6
|
+
Loader2,
|
|
7
|
+
X,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
import { apiFetch } from "./utils/api";
|
|
10
|
+
|
|
11
|
+
interface PendingLoginTask {
|
|
12
|
+
id: string;
|
|
13
|
+
adapter: string;
|
|
14
|
+
botId: string;
|
|
15
|
+
type: string;
|
|
16
|
+
payload?: {
|
|
17
|
+
message?: string;
|
|
18
|
+
image?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
qrcode?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
createdAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const POLL_BASE_MS = 2_000;
|
|
27
|
+
const POLL_MAX_MS = 15_000;
|
|
28
|
+
const ADAPTER_FILTER = "weixin-ilink";
|
|
29
|
+
|
|
30
|
+
export default function LoginAssistPanel() {
|
|
31
|
+
const [pending, setPending] = useState<PendingLoginTask[]>([]);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const [submitting, setSubmitting] = useState<Record<string, boolean>>({});
|
|
35
|
+
|
|
36
|
+
const consecutiveEmptyRef = useRef(0);
|
|
37
|
+
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
38
|
+
|
|
39
|
+
const fetchPending = useCallback(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const res = await apiFetch("/api/login-assist/pending");
|
|
42
|
+
if (!res.ok) throw new Error("获取待办失败");
|
|
43
|
+
const data = (await res.json()) as unknown;
|
|
44
|
+
const list = Array.isArray(data) ? (data as PendingLoginTask[]) : [];
|
|
45
|
+
const filtered = list.filter((t) => t.adapter === ADAPTER_FILTER);
|
|
46
|
+
setPending(filtered);
|
|
47
|
+
setError(null);
|
|
48
|
+
consecutiveEmptyRef.current = filtered.length > 0 ? 0 : consecutiveEmptyRef.current + 1;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
setError((err as Error).message);
|
|
51
|
+
consecutiveEmptyRef.current++;
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const scheduleNext = useCallback(() => {
|
|
58
|
+
const delay = Math.min(
|
|
59
|
+
POLL_BASE_MS * Math.pow(1.5, consecutiveEmptyRef.current),
|
|
60
|
+
POLL_MAX_MS,
|
|
61
|
+
);
|
|
62
|
+
timerRef.current = setTimeout(async () => {
|
|
63
|
+
await fetchPending();
|
|
64
|
+
scheduleNext();
|
|
65
|
+
}, delay);
|
|
66
|
+
}, [fetchPending]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
void fetchPending().then(scheduleNext);
|
|
70
|
+
return () => clearTimeout(timerRef.current);
|
|
71
|
+
}, [fetchPending, scheduleNext]);
|
|
72
|
+
|
|
73
|
+
const handleSubmit = async (id: string, value: string | Record<string, unknown>) => {
|
|
74
|
+
setSubmitting((s) => ({ ...s, [id]: true }));
|
|
75
|
+
try {
|
|
76
|
+
const res = await apiFetch("/api/login-assist/submit", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ id, value }),
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) throw new Error("提交失败");
|
|
82
|
+
consecutiveEmptyRef.current = 0;
|
|
83
|
+
await fetchPending();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
setError((err as Error).message);
|
|
86
|
+
} finally {
|
|
87
|
+
setSubmitting((s) => ({ ...s, [id]: false }));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleCancel = async (id: string) => {
|
|
92
|
+
try {
|
|
93
|
+
const res = await apiFetch("/api/login-assist/cancel", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ id }),
|
|
97
|
+
});
|
|
98
|
+
if (res.ok) await fetchPending();
|
|
99
|
+
} catch {
|
|
100
|
+
/* ignore */
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (loading && pending.length === 0) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex justify-center py-12">
|
|
107
|
+
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="space-y-4">
|
|
114
|
+
<p className="text-sm text-muted-foreground">
|
|
115
|
+
首次启动无凭证时,请用微信扫描 ClawBot 二维码;刷新页面后仍可继续查看待办。
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
{error && (
|
|
119
|
+
<div className="flex items-center gap-2 p-3 rounded border border-red-200 bg-red-50 text-red-600 text-sm dark:bg-red-900/20 dark:text-red-400 dark:border-red-800">
|
|
120
|
+
<AlertCircle className="w-4 h-4 shrink-0" />
|
|
121
|
+
<span>{error}</span>
|
|
122
|
+
<button type="button" onClick={() => setError(null)} className="ml-auto">
|
|
123
|
+
<X className="w-3.5 h-3.5" />
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{pending.length === 0 ? (
|
|
129
|
+
<div className="border rounded-lg bg-card p-12 text-center">
|
|
130
|
+
<LogIn className="w-16 h-16 mx-auto mb-4 opacity-25" />
|
|
131
|
+
<h3 className="text-lg font-semibold mb-1">暂无待办</h3>
|
|
132
|
+
<p className="text-sm text-muted-foreground">需要扫码登录时,二维码将显示在此处</p>
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
<div className="space-y-4">
|
|
136
|
+
{pending.map((task) => (
|
|
137
|
+
<div key={task.id} className="border rounded-lg bg-card p-4 shadow-sm">
|
|
138
|
+
<div className="flex items-start justify-between flex-wrap gap-2 mb-2">
|
|
139
|
+
<div className="flex items-center gap-2 font-semibold">
|
|
140
|
+
<QrCode className="w-4 h-4" /> 扫码登录
|
|
141
|
+
</div>
|
|
142
|
+
<div className="flex gap-1 text-xs">
|
|
143
|
+
<span className="px-2 py-0.5 rounded border">{task.adapter}</span>
|
|
144
|
+
<span className="px-2 py-0.5 rounded bg-muted">{task.botId}</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{task.payload?.message && (
|
|
149
|
+
<p className="text-sm text-muted-foreground mb-3">{task.payload.message}</p>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{task.type === "qrcode" && task.payload?.image && (
|
|
153
|
+
<div className="flex justify-center p-4 bg-muted/30 rounded-lg mb-3">
|
|
154
|
+
<img
|
|
155
|
+
src={String(task.payload.image)}
|
|
156
|
+
alt="登录二维码"
|
|
157
|
+
className="max-w-[200px] w-full h-auto"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{task.type === "qrcode" && !task.payload?.image && task.payload?.url && (
|
|
163
|
+
<p className="text-sm break-all mb-3">
|
|
164
|
+
<a
|
|
165
|
+
href={String(task.payload.url)}
|
|
166
|
+
target="_blank"
|
|
167
|
+
rel="noopener noreferrer"
|
|
168
|
+
className="text-primary hover:underline"
|
|
169
|
+
>
|
|
170
|
+
打开二维码链接
|
|
171
|
+
</a>
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{task.type === "qrcode" && (
|
|
176
|
+
<div className="flex gap-2 mt-3">
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
className="h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50"
|
|
180
|
+
disabled={!!submitting[task.id]}
|
|
181
|
+
onClick={() => void handleSubmit(task.id, { done: true })}
|
|
182
|
+
>
|
|
183
|
+
{submitting[task.id] ? "提交中…" : "我已扫码"}
|
|
184
|
+
</button>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className="h-9 px-4 rounded-md border text-sm hover:bg-accent transition-colors"
|
|
188
|
+
onClick={() => void handleCancel(task.id)}
|
|
189
|
+
>
|
|
190
|
+
取消
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { Bot, LogIn, RefreshCw, Wifi, WifiOff, Loader2 } from "lucide-react";
|
|
3
|
+
import LoginAssistPanel from "./LoginAssistPanel";
|
|
4
|
+
import { apiFetch } from "./utils/api";
|
|
5
|
+
|
|
6
|
+
interface BotRow {
|
|
7
|
+
name: string;
|
|
8
|
+
connected: boolean;
|
|
9
|
+
status: string;
|
|
10
|
+
hasCredentials?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Tab = "overview" | "login";
|
|
14
|
+
|
|
15
|
+
export default function WeixinIlinkManagement() {
|
|
16
|
+
const [tab, setTab] = useState<Tab>("overview");
|
|
17
|
+
const [bots, setBots] = useState<BotRow[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [err, setErr] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
const loadBots = useCallback(async () => {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setErr(null);
|
|
24
|
+
try {
|
|
25
|
+
const res = await apiFetch("/api/weixin-ilink/bots");
|
|
26
|
+
const json = (await res.json()) as { success?: boolean; data?: BotRow[]; message?: string };
|
|
27
|
+
if (!res.ok || !json.success) throw new Error(json.message || "加载失败");
|
|
28
|
+
setBots(Array.isArray(json.data) ? json.data : []);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
setErr((e as Error).message);
|
|
31
|
+
setBots([]);
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (tab === "overview") void loadBots();
|
|
39
|
+
}, [tab, loadBots]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="p-6 max-w-5xl mx-auto space-y-5">
|
|
43
|
+
<div className="flex items-center justify-between">
|
|
44
|
+
<div>
|
|
45
|
+
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
46
|
+
<Bot className="w-6 h-6" /> 微信 iLink(ClawBot)
|
|
47
|
+
</h1>
|
|
48
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
49
|
+
个人微信长轮询适配器:概览与扫码登录辅助
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
{tab === "overview" && (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => void loadBots()}
|
|
56
|
+
disabled={loading}
|
|
57
|
+
className="flex items-center gap-1 px-3 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 text-sm"
|
|
58
|
+
>
|
|
59
|
+
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} /> 刷新
|
|
60
|
+
</button>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="flex gap-1 border-b">
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => setTab("overview")}
|
|
68
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
69
|
+
tab === "overview"
|
|
70
|
+
? "border-blue-500 text-blue-600"
|
|
71
|
+
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
72
|
+
}`}
|
|
73
|
+
>
|
|
74
|
+
<span className="flex items-center gap-1.5">
|
|
75
|
+
<Bot className="w-4 h-4" /> 概览
|
|
76
|
+
</span>
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => setTab("login")}
|
|
81
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
82
|
+
tab === "login"
|
|
83
|
+
? "border-blue-500 text-blue-600"
|
|
84
|
+
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
<span className="flex items-center gap-1.5">
|
|
88
|
+
<LogIn className="w-4 h-4" /> 登录辅助
|
|
89
|
+
</span>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{tab === "overview" && (
|
|
94
|
+
<div className="space-y-4">
|
|
95
|
+
{err && (
|
|
96
|
+
<p className="text-sm text-red-600">{err}</p>
|
|
97
|
+
)}
|
|
98
|
+
{loading ? (
|
|
99
|
+
<div className="flex justify-center py-12">
|
|
100
|
+
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
|
101
|
+
</div>
|
|
102
|
+
) : bots.length === 0 ? (
|
|
103
|
+
<div className="border rounded-lg p-8 text-center text-muted-foreground">
|
|
104
|
+
暂无 weixin-ilink 机器人实例
|
|
105
|
+
</div>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="grid gap-3">
|
|
108
|
+
{bots.map((bot) => (
|
|
109
|
+
<div key={bot.name} className="border rounded-lg p-4 flex items-center justify-between">
|
|
110
|
+
<div>
|
|
111
|
+
<div className="font-semibold">{bot.name}</div>
|
|
112
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
113
|
+
凭证:{bot.hasCredentials ? "已保存" : "未登录"}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="flex items-center gap-2 text-sm">
|
|
117
|
+
{bot.connected ? (
|
|
118
|
+
<>
|
|
119
|
+
<Wifi className="w-4 h-4 text-green-500" />
|
|
120
|
+
<span className="text-green-600">在线</span>
|
|
121
|
+
</>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
<WifiOff className="w-4 h-4 text-gray-400" />
|
|
125
|
+
<span className="text-gray-500">离线</span>
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{tab === "login" && <LoginAssistPanel />}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
package/client/index.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PluginRegisterHostApi } from "@zhin.js/contract";
|
|
2
|
+
import WeixinIlinkManagement from "./WeixinIlinkManagement";
|
|
3
|
+
|
|
4
|
+
export function register(api: PluginRegisterHostApi) {
|
|
5
|
+
api.addRoute({
|
|
6
|
+
path: "/console/weixin-ilink",
|
|
7
|
+
name: "微信 iLink",
|
|
8
|
+
element: api.React.createElement(WeixinIlinkManagement, { hostReact: api.React }),
|
|
9
|
+
});
|
|
10
|
+
api.addTool({ id: "weixin-ilink", name: "微信 iLink", path: "/console/weixin-ilink" });
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"isolatedModules": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["./**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const TOKEN_KEY = "zhin_api_token";
|
|
2
|
+
const API_BASE_KEY = "zhin_api_base";
|
|
3
|
+
|
|
4
|
+
export function getApiBase(): string {
|
|
5
|
+
const stored = localStorage.getItem(API_BASE_KEY)?.trim();
|
|
6
|
+
if (stored) return stored.replace(/\/$/, "");
|
|
7
|
+
if (typeof window !== "undefined") return window.location.origin;
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getToken(): string | null {
|
|
12
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
16
|
+
const token = getToken();
|
|
17
|
+
const headers = new Headers(init?.headers);
|
|
18
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
19
|
+
|
|
20
|
+
const base = getApiBase();
|
|
21
|
+
const url =
|
|
22
|
+
typeof input === "string" && input.startsWith("/") ? `${base}${input}` : input;
|
|
23
|
+
|
|
24
|
+
const res = await fetch(url, { ...init, headers });
|
|
25
|
+
if (res.status === 401) {
|
|
26
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
27
|
+
window.dispatchEvent(new CustomEvent("zhin:auth-required"));
|
|
28
|
+
}
|
|
29
|
+
return res;
|
|
30
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import{useCallback as he,useEffect as ge,useState as q}from"react";import{forwardRef as ae,createElement as te}from"react";var y=(...e)=>e.filter((t,l,f)=>!!t&&t.trim()!==""&&f.indexOf(t)===l).join(" ").trim();var W=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();var E=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,l,f)=>f?f.toUpperCase():l.toLowerCase());var U=e=>{let t=E(e);return t.charAt(0).toUpperCase()+t.slice(1)};import{forwardRef as ee,createElement as z}from"react";var R={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};var V=e=>{for(let t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0;return!1};import{createContext as j,useContext as Y,useMemo as De,createElement as ye}from"react";var $=j({});var N=()=>Y($);var X=ee(({color:e,size:t,strokeWidth:l,absoluteStrokeWidth:f,className:n="",children:r,iconNode:m,...x},p)=>{let{size:s=24,strokeWidth:i=2,absoluteStrokeWidth:P=!1,color:b="currentColor",className:v=""}=N()??{},a=f??P?Number(l??i)*24/Number(t??s):l??i;return z("svg",{ref:p,...R,width:t??s??R.width,height:t??s??R.height,stroke:e??b,strokeWidth:a,className:y("lucide",v,n),...!r&&!V(x)&&{"aria-hidden":"true"},...x},[...m.map(([g,I])=>z(g,I)),...Array.isArray(r)?r:[r]])});var d=(e,t)=>{let l=ae(({className:f,...n},r)=>te(X,{ref:r,iconNode:t,className:y(`lucide-${W(U(e))}`,`lucide-${e}`,f),...n}));return l.displayName=U(e),l};var oe=[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]],k=d("bot",oe);var ue=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],C=d("circle-alert",ue);var de=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],L=d("loader-circle",de);var le=[["path",{d:"m10 17 5-5-5-5",key:"1bsop3"}],["path",{d:"M15 12H3",key:"6jk70r"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4",key:"u53s6r"}]],S=d("log-in",le);var fe=[["rect",{width:"5",height:"5",x:"3",y:"3",rx:"1",key:"1tu5fj"}],["rect",{width:"5",height:"5",x:"16",y:"3",rx:"1",key:"1v8r4q"}],["rect",{width:"5",height:"5",x:"3",y:"16",rx:"1",key:"1x03jg"}],["path",{d:"M21 16h-3a2 2 0 0 0-2 2v3",key:"177gqh"}],["path",{d:"M21 21v.01",key:"ents32"}],["path",{d:"M12 7v3a2 2 0 0 1-2 2H7",key:"8crl2c"}],["path",{d:"M3 12h.01",key:"nlz23k"}],["path",{d:"M12 3h.01",key:"n36tog"}],["path",{d:"M12 16v.01",key:"133mhm"}],["path",{d:"M16 12h1",key:"1slzba"}],["path",{d:"M21 12v.01",key:"1lwtk9"}],["path",{d:"M12 21v-1",key:"1880an"}]],A=d("qr-code",fe);var re=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],B=d("refresh-cw",re);var se=[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}],["path",{d:"M5 12.859a10 10 0 0 1 5.17-2.69",key:"1dl1wf"}],["path",{d:"M19 12.859a10 10 0 0 0-2.007-1.523",key:"4k23kn"}],["path",{d:"M2 8.82a15 15 0 0 1 4.177-2.643",key:"1grhjp"}],["path",{d:"M22 8.82a15 15 0 0 0-11.288-3.764",key:"z3jwby"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]],M=d("wifi-off",se);var ie=[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]],F=d("wifi",ie);var ce=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],D=d("x",ce);import{useCallback as Z,useEffect as Le,useRef as Q,useState as T}from"react";var K="zhin_api_token",ne="zhin_api_base";function pe(){let e=localStorage.getItem(ne)?.trim();return e?e.replace(/\/$/,""):typeof window<"u"?window.location.origin:""}function me(){return localStorage.getItem(K)}async function w(e,t){let l=me(),f=new Headers(t?.headers);l&&f.set("Authorization",`Bearer ${l}`);let n=pe(),r=typeof e=="string"&&e.startsWith("/")?`${n}${e}`:e,m=await fetch(r,{...t,headers:f});return m.status===401&&(localStorage.removeItem(K),window.dispatchEvent(new CustomEvent("zhin:auth-required"))),m}import{jsx as o,jsxs as h}from"react/jsx-runtime";var Ie=2e3,xe=15e3,Ce="weixin-ilink";function O(){let[e,t]=T([]),[l,f]=T(!0),[n,r]=T(null),[m,x]=T({}),p=Q(0),s=Q(),i=Z(async()=>{try{let a=await w("/api/login-assist/pending");if(!a.ok)throw new Error("\u83B7\u53D6\u5F85\u529E\u5931\u8D25");let g=await a.json(),G=(Array.isArray(g)?g:[]).filter(_=>_.adapter===Ce);t(G),r(null),p.current=G.length>0?0:p.current+1}catch(a){r(a.message),p.current++}finally{f(!1)}},[]),P=Z(()=>{let a=Math.min(Ie*Math.pow(1.5,p.current),xe);s.current=setTimeout(async()=>{await i(),P()},a)},[i]);Le(()=>(i().then(P),()=>clearTimeout(s.current)),[i,P]);let b=async(a,g)=>{x(I=>({...I,[a]:!0}));try{if(!(await w("/api/login-assist/submit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:a,value:g})})).ok)throw new Error("\u63D0\u4EA4\u5931\u8D25");p.current=0,await i()}catch(I){r(I.message)}finally{x(I=>({...I,[a]:!1}))}},v=async a=>{try{(await w("/api/login-assist/cancel",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:a})})).ok&&await i()}catch{}};return l&&e.length===0?o("div",{className:"flex justify-center py-12",children:o(L,{className:"w-6 h-6 animate-spin text-blue-500"})}):h("div",{className:"space-y-4",children:[o("p",{className:"text-sm text-muted-foreground",children:"\u9996\u6B21\u542F\u52A8\u65E0\u51ED\u8BC1\u65F6\uFF0C\u8BF7\u7528\u5FAE\u4FE1\u626B\u63CF ClawBot \u4E8C\u7EF4\u7801\uFF1B\u5237\u65B0\u9875\u9762\u540E\u4ECD\u53EF\u7EE7\u7EED\u67E5\u770B\u5F85\u529E\u3002"}),n&&h("div",{className:"flex items-center gap-2 p-3 rounded border border-red-200 bg-red-50 text-red-600 text-sm dark:bg-red-900/20 dark:text-red-400 dark:border-red-800",children:[o(C,{className:"w-4 h-4 shrink-0"}),o("span",{children:n}),o("button",{type:"button",onClick:()=>r(null),className:"ml-auto",children:o(D,{className:"w-3.5 h-3.5"})})]}),e.length===0?h("div",{className:"border rounded-lg bg-card p-12 text-center",children:[o(S,{className:"w-16 h-16 mx-auto mb-4 opacity-25"}),o("h3",{className:"text-lg font-semibold mb-1",children:"\u6682\u65E0\u5F85\u529E"}),o("p",{className:"text-sm text-muted-foreground",children:"\u9700\u8981\u626B\u7801\u767B\u5F55\u65F6\uFF0C\u4E8C\u7EF4\u7801\u5C06\u663E\u793A\u5728\u6B64\u5904"})]}):o("div",{className:"space-y-4",children:e.map(a=>h("div",{className:"border rounded-lg bg-card p-4 shadow-sm",children:[h("div",{className:"flex items-start justify-between flex-wrap gap-2 mb-2",children:[h("div",{className:"flex items-center gap-2 font-semibold",children:[o(A,{className:"w-4 h-4"})," \u626B\u7801\u767B\u5F55"]}),h("div",{className:"flex gap-1 text-xs",children:[o("span",{className:"px-2 py-0.5 rounded border",children:a.adapter}),o("span",{className:"px-2 py-0.5 rounded bg-muted",children:a.botId})]})]}),a.payload?.message&&o("p",{className:"text-sm text-muted-foreground mb-3",children:a.payload.message}),a.type==="qrcode"&&a.payload?.image&&o("div",{className:"flex justify-center p-4 bg-muted/30 rounded-lg mb-3",children:o("img",{src:String(a.payload.image),alt:"\u767B\u5F55\u4E8C\u7EF4\u7801",className:"max-w-[200px] w-full h-auto"})}),a.type==="qrcode"&&!a.payload?.image&&a.payload?.url&&o("p",{className:"text-sm break-all mb-3",children:o("a",{href:String(a.payload.url),target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:"\u6253\u5F00\u4E8C\u7EF4\u7801\u94FE\u63A5"})}),a.type==="qrcode"&&h("div",{className:"flex gap-2 mt-3",children:[o("button",{type:"button",className:"h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50",disabled:!!m[a.id],onClick:()=>{b(a.id,{done:!0})},children:m[a.id]?"\u63D0\u4EA4\u4E2D\u2026":"\u6211\u5DF2\u626B\u7801"}),o("button",{type:"button",className:"h-9 px-4 rounded-md border text-sm hover:bg-accent transition-colors",onClick:()=>{v(a.id)},children:"\u53D6\u6D88"})]})]},a.id))})]})}import{Fragment as J,jsx as u,jsxs as c}from"react/jsx-runtime";function H(){let[e,t]=q("overview"),[l,f]=q([]),[n,r]=q(!0),[m,x]=q(null),p=he(async()=>{r(!0),x(null);try{let s=await w("/api/weixin-ilink/bots"),i=await s.json();if(!s.ok||!i.success)throw new Error(i.message||"\u52A0\u8F7D\u5931\u8D25");f(Array.isArray(i.data)?i.data:[])}catch(s){x(s.message),f([])}finally{r(!1)}},[]);return ge(()=>{e==="overview"&&p()},[e,p]),c("div",{className:"p-6 max-w-5xl mx-auto space-y-5",children:[c("div",{className:"flex items-center justify-between",children:[c("div",{children:[c("h1",{className:"text-2xl font-bold flex items-center gap-2",children:[u(k,{className:"w-6 h-6"})," \u5FAE\u4FE1 iLink\uFF08ClawBot\uFF09"]}),u("p",{className:"text-sm text-muted-foreground mt-1",children:"\u4E2A\u4EBA\u5FAE\u4FE1\u957F\u8F6E\u8BE2\u9002\u914D\u5668\uFF1A\u6982\u89C8\u4E0E\u626B\u7801\u767B\u5F55\u8F85\u52A9"})]}),e==="overview"&&c("button",{type:"button",onClick:()=>{p()},disabled:n,className:"flex items-center gap-1 px-3 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 text-sm",children:[u(B,{className:`w-4 h-4 ${n?"animate-spin":""}`})," \u5237\u65B0"]})]}),c("div",{className:"flex gap-1 border-b",children:[u("button",{type:"button",onClick:()=>t("overview"),className:`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${e==="overview"?"border-blue-500 text-blue-600":"border-transparent text-gray-500 hover:text-gray-700"}`,children:c("span",{className:"flex items-center gap-1.5",children:[u(k,{className:"w-4 h-4"})," \u6982\u89C8"]})}),u("button",{type:"button",onClick:()=>t("login"),className:`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${e==="login"?"border-blue-500 text-blue-600":"border-transparent text-gray-500 hover:text-gray-700"}`,children:c("span",{className:"flex items-center gap-1.5",children:[u(S,{className:"w-4 h-4"})," \u767B\u5F55\u8F85\u52A9"]})})]}),e==="overview"&&c("div",{className:"space-y-4",children:[m&&u("p",{className:"text-sm text-red-600",children:m}),n?u("div",{className:"flex justify-center py-12",children:u(L,{className:"w-6 h-6 animate-spin text-blue-500"})}):l.length===0?u("div",{className:"border rounded-lg p-8 text-center text-muted-foreground",children:"\u6682\u65E0 weixin-ilink \u673A\u5668\u4EBA\u5B9E\u4F8B"}):u("div",{className:"grid gap-3",children:l.map(s=>c("div",{className:"border rounded-lg p-4 flex items-center justify-between",children:[c("div",{children:[u("div",{className:"font-semibold",children:s.name}),c("div",{className:"text-xs text-muted-foreground mt-1",children:["\u51ED\u8BC1\uFF1A",s.hasCredentials?"\u5DF2\u4FDD\u5B58":"\u672A\u767B\u5F55"]})]}),u("div",{className:"flex items-center gap-2 text-sm",children:s.connected?c(J,{children:[u(F,{className:"w-4 h-4 text-green-500"}),u("span",{className:"text-green-600",children:"\u5728\u7EBF"})]}):c(J,{children:[u(M,{className:"w-4 h-4 text-gray-400"}),u("span",{className:"text-gray-500",children:"\u79BB\u7EBF"})]})})]},s.name))})]}),e==="login"&&u(O,{})]})}function Ra(e){e.addRoute({path:"/console/weixin-ilink",name:"\u5FAE\u4FE1 iLink",element:e.React.createElement(H,{hostReact:e.React})}),e.addTool({id:"weixin-ilink",name:"\u5FAE\u4FE1 iLink",path:"/console/weixin-ilink"})}export{Ra as register};
|
|
2
|
+
/*! Bundled license information:
|
|
3
|
+
|
|
4
|
+
lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs:
|
|
5
|
+
lucide-react/dist/esm/shared/src/utils/toKebabCase.mjs:
|
|
6
|
+
lucide-react/dist/esm/shared/src/utils/toCamelCase.mjs:
|
|
7
|
+
lucide-react/dist/esm/shared/src/utils/toPascalCase.mjs:
|
|
8
|
+
lucide-react/dist/esm/defaultAttributes.mjs:
|
|
9
|
+
lucide-react/dist/esm/shared/src/utils/hasA11yProp.mjs:
|
|
10
|
+
lucide-react/dist/esm/context.mjs:
|
|
11
|
+
lucide-react/dist/esm/Icon.mjs:
|
|
12
|
+
lucide-react/dist/esm/createLucideIcon.mjs:
|
|
13
|
+
lucide-react/dist/esm/icons/bot.mjs:
|
|
14
|
+
lucide-react/dist/esm/icons/circle-alert.mjs:
|
|
15
|
+
lucide-react/dist/esm/icons/loader-circle.mjs:
|
|
16
|
+
lucide-react/dist/esm/icons/log-in.mjs:
|
|
17
|
+
lucide-react/dist/esm/icons/qr-code.mjs:
|
|
18
|
+
lucide-react/dist/esm/icons/refresh-cw.mjs:
|
|
19
|
+
lucide-react/dist/esm/icons/wifi-off.mjs:
|
|
20
|
+
lucide-react/dist/esm/icons/wifi.mjs:
|
|
21
|
+
lucide-react/dist/esm/icons/x.mjs:
|
|
22
|
+
lucide-react/dist/esm/lucide-react.mjs:
|
|
23
|
+
(**
|
|
24
|
+
* @license lucide-react v1.17.0 - ISC
|
|
25
|
+
*
|
|
26
|
+
* This source code is licensed under the ISC license.
|
|
27
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
28
|
+
*)
|
|
29
|
+
*/
|
package/lib/adapter.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信 iLink(ClawBot)适配器
|
|
3
|
+
*/
|
|
4
|
+
import { Adapter, type Plugin } from "zhin.js";
|
|
5
|
+
import { WeixinIlinkBot } from "./bot.js";
|
|
6
|
+
import type { WeixinIlinkBotConfig } from "./types.js";
|
|
7
|
+
export declare class WeixinIlinkAdapter extends Adapter<WeixinIlinkBot> {
|
|
8
|
+
constructor(plugin: Plugin);
|
|
9
|
+
createBot(config: WeixinIlinkBotConfig): WeixinIlinkBot;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,qBAAa,kBAAmB,SAAQ,OAAO,CAAC,cAAc,CAAC;gBACjD,MAAM,EAAE,MAAM;IAI1B,SAAS,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc;CAGxD"}
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信 iLink(ClawBot)适配器
|
|
3
|
+
*/
|
|
4
|
+
import { Adapter } from "zhin.js";
|
|
5
|
+
import { WeixinIlinkBot } from "./bot.js";
|
|
6
|
+
export class WeixinIlinkAdapter extends Adapter {
|
|
7
|
+
constructor(plugin) {
|
|
8
|
+
super(plugin, "weixin-ilink", []);
|
|
9
|
+
}
|
|
10
|
+
createBot(config) {
|
|
11
|
+
return new WeixinIlinkBot(this, config);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,OAAO,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,MAAM,OAAO,kBAAmB,SAAQ,OAAuB;IAC7D,YAAY,MAAc;QACxB,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,SAAS,CAAC,MAA4B;QACpC,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;CACF"}
|
package/lib/aes-ecb.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Encrypt buffer with AES-128-ECB (PKCS7 padding is default). */
|
|
2
|
+
export declare function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer;
|
|
3
|
+
/** Decrypt buffer with AES-128-ECB (PKCS7 padding). */
|
|
4
|
+
export declare function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer;
|
|
5
|
+
/** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
|
|
6
|
+
export declare function aesEcbPaddedSize(plaintextSize: number): number;
|
|
7
|
+
//# sourceMappingURL=aes-ecb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aes-ecb.d.ts","sourceRoot":"","sources":["../src/aes-ecb.ts"],"names":[],"mappings":"AAKA,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED,uDAAuD;AACvD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGrE;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE9D"}
|
package/lib/aes-ecb.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AES-128-ECB crypto utilities for CDN upload and download.
|
|
3
|
+
*/
|
|
4
|
+
import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
5
|
+
/** Encrypt buffer with AES-128-ECB (PKCS7 padding is default). */
|
|
6
|
+
export function encryptAesEcb(plaintext, key) {
|
|
7
|
+
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
8
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9
|
+
}
|
|
10
|
+
/** Decrypt buffer with AES-128-ECB (PKCS7 padding). */
|
|
11
|
+
export function decryptAesEcb(ciphertext, key) {
|
|
12
|
+
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
13
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
14
|
+
}
|
|
15
|
+
/** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
|
|
16
|
+
export function aesEcbPaddedSize(plaintextSize) {
|
|
17
|
+
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=aes-ecb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aes-ecb.js","sourceRoot":"","sources":["../src/aes-ecb.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/D,kEAAkE;AAClE,MAAM,UAAU,aAAa,CAAC,SAAiB,EAAE,GAAW;IAC1D,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACxD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,GAAW;IAC3D,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,aAAqB;IACpD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;AAClD,CAAC"}
|
package/lib/bot.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Bot, Message, type SendOptions } from "zhin.js";
|
|
2
|
+
import type { WeixinMessage } from "./ilink-types.js";
|
|
3
|
+
import type { WeixinInboundMediaOpts } from "./weixin-inbound.js";
|
|
4
|
+
import type { WeixinIlinkBotConfig } from "./types.js";
|
|
5
|
+
import type { WeixinIlinkAdapter } from "./adapter.js";
|
|
6
|
+
import { WeixinIlinkTypingIndicatorManager } from "./typing-indicator.js";
|
|
7
|
+
export type WeixinMessageWithMedia = WeixinMessage & {
|
|
8
|
+
_media?: WeixinInboundMediaOpts;
|
|
9
|
+
};
|
|
10
|
+
export declare class WeixinIlinkBot implements Bot<WeixinIlinkBotConfig, WeixinMessage> {
|
|
11
|
+
adapter: WeixinIlinkAdapter;
|
|
12
|
+
$config: WeixinIlinkBotConfig;
|
|
13
|
+
$connected: boolean;
|
|
14
|
+
$typingIndicator?: WeixinIlinkTypingIndicatorManager;
|
|
15
|
+
private creds;
|
|
16
|
+
private pollAbort?;
|
|
17
|
+
private pollPromise?;
|
|
18
|
+
private configManager?;
|
|
19
|
+
get $id(): string;
|
|
20
|
+
get hasCredentials(): boolean;
|
|
21
|
+
get pluginLogger(): import("zhin.js").Logger;
|
|
22
|
+
constructor(adapter: WeixinIlinkAdapter, $config: WeixinIlinkBotConfig);
|
|
23
|
+
apiBaseUrl(): string;
|
|
24
|
+
cdnBaseUrl(): string;
|
|
25
|
+
$connect(): Promise<void>;
|
|
26
|
+
$disconnect(): Promise<void>;
|
|
27
|
+
sendTypingToUser(userId: string, status: number): Promise<void>;
|
|
28
|
+
$recallMessage(_id: string): Promise<void>;
|
|
29
|
+
$formatMessage(msg: WeixinMessageWithMedia): Message<WeixinMessage>;
|
|
30
|
+
private buildInboundContent;
|
|
31
|
+
$sendMessage(options: SendOptions): Promise<string>;
|
|
32
|
+
private pollLoop;
|
|
33
|
+
private handleInboundMessage;
|
|
34
|
+
private downloadInboundMedia;
|
|
35
|
+
private saveInboundMedia;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=bot.d.ts.map
|