CI / backend (push) Canceled after 0s
一条通道一张令牌,机器人把消息 POST 到 /integrations/clawbot/{token},
正文按前缀落到任务或知识草稿:没有前缀就是任务,「知识:」「笔记:」记成知识,
「帮助」只回用法。首行当标题、其余当正文,`#标签` 摘出来挂到事项上。
同一个 msg_id 只处理一次——微信会重推,重推不能变成两条任务。
为什么不塞进 system_setting 的 JSON:一个工作区可能挂多个通道,还得能单独停用。
令牌库里只存 sha256,只在创建/重新生成时显示一次;收消息时还不知道是哪个工作区,
所以 bot_channel 的 RLS 额外放行 token_hash = current_setting('app.bot_token_hash'),
读到通道再 bind_tenant 进上下文,按通道创建者记账。
- 后端:models_integrations.py(bot_channel / bot_message)、迁移 0012(RLS)、
api/integrations.py(收消息 + 管理接口)、错误码 9002/9003
- Web:/w/:wsId/integrations「接入」页——建通道、复制回调地址与令牌、最近消息、
停用/换令牌/删除,只有 owner/admin 能改
- 测试:test_clawbot.py 10 例;web/e2e/integrations.mjs 7 项(冒充 clawbot 真推一条)
验证:pytest 150 passed,ruff check/format 全绿,tsc 0,vite build 通过,e2e 7 项全绿。
117 lines
4.2 KiB
TypeScript
117 lines
4.2 KiB
TypeScript
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";
|
|
import { ToastProvider } from "./components/ui";
|
|
import { UpdatePrompt } from "./components/UpdatePrompt";
|
|
import { api, authExpired, session } from "./lib/client";
|
|
import type { User, Workspace } from "./lib/types";
|
|
import { LoginPage } from "./pages/Login";
|
|
import { RegisterPage } from "./pages/Register";
|
|
import { HomePage } from "./pages/Home";
|
|
import { WorkspacePage } from "./pages/Workspace";
|
|
import { TasksPage } from "./pages/Tasks";
|
|
import { BoardPage } from "./pages/Board";
|
|
import { TicketsPage } from "./pages/Tickets";
|
|
import { KnowledgePage } from "./pages/Knowledge";
|
|
import { KnowledgeDocPage } from "./pages/KnowledgeDoc";
|
|
import { NotificationsPage } from "./pages/Notifications";
|
|
import { JoinPage } from "./pages/Join";
|
|
import { AccountPage } from "./pages/Account";
|
|
import { IntegrationsPage } from "./pages/Integrations";
|
|
import { SettingsPage } from "./pages/Settings";
|
|
|
|
interface AuthState {
|
|
user: User | null;
|
|
workspaces: Workspace[];
|
|
loading: boolean;
|
|
reload: () => Promise<void>;
|
|
signOut: () => void;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthState | null>(null);
|
|
|
|
export function useAuth(): AuthState {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error("useAuth 必须在 AuthProvider 内使用");
|
|
return ctx;
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const navigate = useNavigate();
|
|
|
|
const reload = useCallback(async () => {
|
|
if (!session.access) {
|
|
setUser(null);
|
|
setWorkspaces([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
try {
|
|
const me = await api.get<{ user: User; workspaces: Workspace[] }>("/auth/me");
|
|
setUser(me.user);
|
|
setWorkspaces(me.workspaces);
|
|
} catch {
|
|
setUser(null);
|
|
setWorkspaces([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void reload();
|
|
const onExpired = () => {
|
|
setUser(null);
|
|
setWorkspaces([]);
|
|
navigate("/login", { replace: true });
|
|
};
|
|
authExpired.addEventListener("expired", onExpired);
|
|
return () => authExpired.removeEventListener("expired", onExpired);
|
|
}, [reload, navigate]);
|
|
|
|
const signOut = useCallback(() => {
|
|
session.clear();
|
|
setUser(null);
|
|
setWorkspaces([]);
|
|
navigate("/login", { replace: true });
|
|
}, [navigate]);
|
|
|
|
const value = useMemo<AuthState>(
|
|
() => ({ user, workspaces, loading, reload, signOut }),
|
|
[user, workspaces, loading, reload, signOut],
|
|
);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
export function App() {
|
|
return (
|
|
<ToastProvider>
|
|
<AuthProvider>
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route path="/register" element={<RegisterPage />} />
|
|
<Route path="/" element={<HomePage />} />
|
|
<Route path="/account" element={<AccountPage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
<Route path="/w/:wsId" element={<TasksPage />} />
|
|
<Route path="/w/:wsId/board" element={<BoardPage />} />
|
|
<Route path="/w/:wsId/tickets" element={<TicketsPage />} />
|
|
<Route path="/w/:wsId/knowledge" element={<KnowledgePage />} />
|
|
{/* 阅读是整页:抽屉只用来编辑,长文在抽屉里读会一直偏在屏幕右侧 */}
|
|
<Route path="/w/:wsId/knowledge/:itemId" element={<KnowledgeDocPage />} />
|
|
<Route path="/w/:wsId/notifications" element={<NotificationsPage />} />
|
|
<Route path="/w/:wsId/members" element={<WorkspacePage />} />
|
|
{/* 接入:微信一句话变成任务/知识,配置在这一页 */}
|
|
<Route path="/w/:wsId/integrations" element={<IntegrationsPage />} />
|
|
<Route path="/join/:wsId/:code" element={<JoinPage />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
<UpdatePrompt />
|
|
</AuthProvider>
|
|
</ToastProvider>
|
|
);
|
|
}
|