Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ docker run -d -p 9800:9800 ghcr.io/openilink/openilink-hub:latest # GHCR
- 扫码绑定,Web 后台管理多个 Bot
- 应用市场一键装功能 —— 对接飞书 / Slack / GitHub / Notion,查天气、记账、AI 对话,不用写代码
- WebSocket / Webhook / AI 三个通道同时转发消息到你的服务
- 24 小时窗口自动续期,不掉线
- 24 小时窗口到期前提醒,避免错过续期
- 消息链路追踪,出问题一眼看到卡在哪

```mermaid
Expand Down Expand Up @@ -116,7 +116,7 @@ OpenClaw 是 AI Agent 框架,OpeniLink Hub 是消息管理平台,两个东
|---|---|
| iLink 没有官方文档,全靠社区逆向 | 完善中文文档 + 7 种语言 SDK |
| context_token 管理复杂,消息经常发不出去 | SDK 自动处理,你只管收消息发回复 |
| 24 小时过期掉线,重要消息丢了 | 自动续期 + 消息持久化 |
| 24 小时过期掉线,重要消息丢了 | 到期前提醒 + 消息持久化 |
| 发图片要自己搞 CDN 上传 + AES 加密 | 一行代码发图片视频文件 |
| 只能命令行操作,管不了多个 Bot | Web 控制台,扫码绑定、状态监控、消息追踪 |
| 想加功能得自己写代码 | 应用市场一键安装,不写代码也能扩展 |
Expand All @@ -139,7 +139,9 @@ GitHub 上有不少 iLink 相关的开源项目,但大多是底层 SDK 或 Age

**App 应用市场** · 不写代码也能扩展 Bot。20+ 官方 App 覆盖平台互通(飞书、Slack、Discord、钉钉、企业微信)、效率工具(GitHub、Google Workspace、Notion、Linear)、生活工具(天气、汇率、记账、地图、RSS)等场景。通过 PKCE OAuth 安全安装,支持第三方开发者上架。

**多 Bot 管理** · 扫码绑定多个微信号,统一面板看状态,自动续期防掉线。
**多 Bot 管理** · 扫码绑定多个微信号,统一面板看状态,到期前提醒防掉线。

> 说明:微信 24 小时窗口目前不能由 Hub 在后台静默自动续期。Hub 现在支持的是“到期前提醒”,会在窗口快到期时提醒你回一条消息,收到回复后窗口会重新开始计时。

Comment on lines +142 to 145
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

English section still contradicts the new reminder-only behavior.

The Chinese clarification is good, but README.md Line 472 still says “auto session renewal,” which conflicts with this PR’s corrected behavior and can mislead English users.

📝 Proposed doc fix
-- **Multi-bot management** — Bind and manage multiple WeChat accounts, auto session renewal
+- **Multi-bot management** — Bind and manage multiple WeChat accounts, expiry reminders before the 24-hour window ends
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 142 - 145, The English README still claims "auto
session renewal" which contradicts the Chinese clarification about reminder-only
behavior; find the English section that mentions "auto session renewal" (e.g.,
the "Multiple Bot Management" / "Multi-Bot management" paragraph) and update the
wording to match the Chinese note — replace claims of automatic background
renewal with a clear statement that Hub only provides "pre-expiry reminders" and
that users must reply to the message to restart the 24-hour window; ensure the
English text mirrors the Chinese explanation and removes any implication of
silent/automatic renewal.

**多通道消息下发**
- **App** — 已安装的 App 自动接收匹配的消息,通过 WebSocket 或 Webhook 投递
Expand Down
2 changes: 1 addition & 1 deletion internal/bot/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func (m *Manager) checkReminders() {

hours := bot.ReminderHours
remaining := 24 - hours
text := fmt.Sprintf("[系统提醒] 您的 Bot 已超过 %d 小时未收到消息,距离会话过期还有约 %d 小时。请回复 OK 以保持会话活跃。", hours, remaining)
text := fmt.Sprintf("[系统提醒] 您的 Bot 已超过 %d 小时未收到消息,距离会话过期还有约 %d 小时。请在微信里回复任意消息以刷新 24 小时窗口。", hours, remaining)

token := m.store.GetLatestContextToken(bot.ID)
_, err := inst.Send(context.Background(), provider.OutboundMessage{
Expand Down
17 changes: 15 additions & 2 deletions web/src/pages/bot-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ vi.mock("@/hooks/use-bots", () => ({
useDeleteBot: () => ({
mutate: (
id: string,
options?: { onSuccess?: () => void; onError?: (error: Error) => void; onSettled?: () => void },
options?: {
onSuccess?: () => void;
onError?: (error: Error) => void;
onSettled?: () => void;
},
) => {
Promise.resolve(deleteBotMock(id))
.then(() => options?.onSuccess?.())
Expand Down Expand Up @@ -112,7 +116,9 @@ describe("BotDetailPage", () => {
}

function getDeleteButton() {
const deleteButton = container.querySelector('button[aria-label="删除账号"]') as HTMLButtonElement | null;
const deleteButton = container.querySelector(
'button[aria-label="删除账号"]',
) as HTMLButtonElement | null;
expect(deleteButton).not.toBeNull();
return deleteButton!;
}
Expand All @@ -125,6 +131,13 @@ describe("BotDetailPage", () => {
});
}

it("shows the expiry reminder controls instead of misleading auto-renew wording", async () => {
await renderPage();

expect(container.textContent).toContain("到期提醒");
expect(container.textContent).not.toContain("自动续期");
});

it("deletes the current bot after confirmation", async () => {
await renderPage();
await clickDeleteButton();
Expand Down
12 changes: 7 additions & 5 deletions web/src/pages/bot-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,9 @@ export function BotDetailPage() {

<Separator orientation="vertical" className="h-6 mx-1" />

{/* Auto-renewal */}
{/* Expiry reminder */}
<div className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground">自动续期</span>
<span className="text-xs text-muted-foreground">到期提醒</span>
<Select
value={String(bot.reminder_hours || 0)}
onValueChange={(v) => handleAutoRenewalChange(Number(v))}
Expand All @@ -368,9 +368,9 @@ export function BotDetailPage() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">不提醒</SelectItem>
<SelectItem value="23">提前 1 小时</SelectItem>
<SelectItem value="22">提前 2 小时</SelectItem>
<SelectItem value="0">关闭提醒</SelectItem>
<SelectItem value="23">到期前 1 小时提醒</SelectItem>
<SelectItem value="22">到期前 2 小时提醒</SelectItem>
</SelectContent>
</Select>
{bot.reminder_hours > 0 && (
Expand All @@ -383,6 +383,7 @@ export function BotDetailPage() {
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs space-y-1">
<p>微信 24 小时窗口目前不能静默自动续期,Hub 只会在到期前提醒你回一条消息。</p>
<p>
上次消息:{" "}
{bot.last_msg_at ? new Date(bot.last_msg_at * 1000).toLocaleString() : "无"}
Expand All @@ -404,6 +405,7 @@ export function BotDetailPage() {
).toLocaleString()
: "等待首条消息"}
</p>
<p>收到提醒后,在微信里回复任意消息即可刷新窗口。</p>
</TooltipContent>
</Tooltip>
)}
Expand Down
26 changes: 10 additions & 16 deletions web/src/pages/bots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ export function BotsPage() {
}
}
};
ws.onerror = () => { ws.close(); };
ws.onerror = () => {
ws.close();
};
ws.onclose = () => {
if (settled) return;
if (retries < MAX_RETRIES) {
Expand Down Expand Up @@ -184,11 +186,7 @@ export function BotsPage() {
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{bots.map((bot) => (
<BotInstanceCard
key={bot.id}
bot={bot}
onRebind={() => setBinding(true)}
/>
<BotInstanceCard key={bot.id} bot={bot} onRebind={() => setBinding(true)} />
))}

{bots.length === 0 ? (
Expand Down Expand Up @@ -224,13 +222,7 @@ function QrCanvas({ url }: { url: string }) {
return <canvas ref={ref} className="block rounded-lg" />;
}

function BotInstanceCard({
bot,
onRebind,
}: {
bot: any;
onRebind: () => void;
}) {
function BotInstanceCard({ bot, onRebind }: { bot: any; onRebind: () => void }) {
const { toast } = useToast();
const { confirm, ConfirmDialog } = useConfirm();
const deleteMutation = useDeleteBot();
Expand All @@ -249,12 +241,14 @@ function BotInstanceCard({
if (!ok) return;
deleteMutation.mutate(bot.id, {
onSuccess: () => toast({ title: "已删除账号" }),
onError: (e) => toast({ variant: "destructive", title: "操作失败", description: e.message }),
onError: (e) =>
toast({ variant: "destructive", title: "操作失败", description: e.message }),
});
} else if (action === "reconnect") {
reconnectMutation.mutate(bot.id, {
onSuccess: () => toast({ title: "指令已发出", description: "正在尝试重新建立连接..." }),
onError: (e) => toast({ variant: "destructive", title: "操作失败", description: e.message }),
onError: (e) =>
toast({ variant: "destructive", title: "操作失败", description: e.message }),
});
}
}
Expand Down Expand Up @@ -321,7 +315,7 @@ function BotInstanceCard({
{bot.reminder_hours ? (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Clock className="h-3.5 w-3.5 text-orange-500" />
<span>提前 {24 - bot.reminder_hours}h 提醒</span>
<span>到期前 {24 - bot.reminder_hours}h 提醒</span>
</div>
) : null}
</div>
Expand Down