mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Add WeChat embedded webview support (#1991)
This commit is contained in:
@@ -49,6 +49,7 @@ const GOOGLE_MEET_RECIPE_JS: &str = include_str!("../../recipes/google-meet/reci
|
||||
fn provider_url(provider: &str) -> Option<&'static str> {
|
||||
match provider {
|
||||
"whatsapp" => Some("https://web.whatsapp.com/"),
|
||||
"wechat" => Some("https://web.wechat.com/"),
|
||||
"telegram" => Some("https://web.telegram.org/k/"),
|
||||
"linkedin" => Some("https://www.linkedin.com/messaging/"),
|
||||
"slack" => Some("https://app.slack.com/client/"),
|
||||
@@ -87,6 +88,12 @@ fn provider_is_supported(provider: &str) -> bool {
|
||||
fn provider_allowed_hosts(provider: &str) -> &'static [&'static str] {
|
||||
match provider {
|
||||
"whatsapp" => &["whatsapp.com", "whatsapp.net", "wa.me"],
|
||||
"wechat" => &[
|
||||
"wechat.com",
|
||||
"wx.qq.com",
|
||||
"weixin.qq.com",
|
||||
"login.weixin.qq.com",
|
||||
],
|
||||
"telegram" => &["telegram.org", "t.me"],
|
||||
"linkedin" => &[
|
||||
"linkedin.com",
|
||||
@@ -649,6 +656,7 @@ async fn post_provider_surfaces_event(args: &RecipeEventArgs) -> Result<(), Stri
|
||||
pub fn provider_display_name(provider: &str) -> &'static str {
|
||||
match provider {
|
||||
"whatsapp" => "WhatsApp",
|
||||
"wechat" => "WeChat",
|
||||
"telegram" => "Telegram",
|
||||
"linkedin" => "LinkedIn",
|
||||
"slack" => "Slack",
|
||||
@@ -1751,12 +1759,13 @@ fn data_directory_for<R: Runtime>(app: &AppHandle<R>, account_id: &str) -> Resul
|
||||
|
||||
/// Produce the `initialization_script` payload for this webview.
|
||||
///
|
||||
/// Empty for the 5 migrated providers (whatsapp, telegram, slack, discord,
|
||||
/// browserscan) — they load with ZERO injected JS; their scraping runs via
|
||||
/// CDP, and the per-account CDP session opener (`cdp::session`) injects the
|
||||
/// notification-permission shim via `Page.addScriptToEvaluateOnNewDocument`
|
||||
/// before the real provider URL loads. The 2 deferred providers
|
||||
/// (linkedin, google-meet) still get the JS recipe bridge.
|
||||
/// Empty for the 6 zero-injection providers (whatsapp, wechat, telegram,
|
||||
/// slack, discord, browserscan) — they load with ZERO injected JS. Some have
|
||||
/// native/CDP scraper paths; WeChat is shell-only for now. The per-account
|
||||
/// CDP session opener (`cdp::session`) still injects the notification-permission
|
||||
/// shim via `Page.addScriptToEvaluateOnNewDocument` before the real provider
|
||||
/// URL loads. The 2 deferred providers (linkedin, google-meet) still get the
|
||||
/// JS recipe bridge.
|
||||
fn build_init_script(account_id: &str, provider: &str) -> String {
|
||||
let Some(recipe_js) = provider_recipe_js(provider) else {
|
||||
return String::new();
|
||||
@@ -3370,6 +3379,27 @@ mod tests {
|
||||
assert_eq!(provider_url("zoom"), Some("https://zoom.us/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wechat_registered_in_provider_url() {
|
||||
assert_eq!(provider_url("wechat"), Some("https://web.wechat.com/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wechat_has_no_recipe_js_injection() {
|
||||
assert!(provider_recipe_js("wechat").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wechat_allowed_hosts_cover_web_and_login_domains() {
|
||||
let hosts = provider_allowed_hosts("wechat");
|
||||
assert!(hosts.contains(&"wechat.com"), "wechat.com in allowlist");
|
||||
assert!(hosts.contains(&"wx.qq.com"), "wx.qq.com in allowlist");
|
||||
assert!(
|
||||
hosts.contains(&"login.weixin.qq.com"),
|
||||
"login.weixin.qq.com in allowlist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_has_no_recipe_js_injection() {
|
||||
// Per the CLAUDE.md "no new JS injection" rule for CEF child
|
||||
|
||||
@@ -379,6 +379,7 @@ const DiscordBody = ({ close }: { close: () => void }) => {
|
||||
*/
|
||||
const ACCOUNTS_SETUP_PROVIDERS: readonly AccountProvider[] = [
|
||||
'whatsapp',
|
||||
'wechat',
|
||||
'telegram',
|
||||
'slack',
|
||||
'discord',
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('OpenhumanLinkModal accounts setup', () => {
|
||||
openAccountsModal();
|
||||
|
||||
expect(screen.getByLabelText('Connect WhatsApp Web')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Connect WeChat Web')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Connect Telegram Web')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Connect Slack')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Connect Discord')).toBeInTheDocument();
|
||||
|
||||
@@ -22,6 +22,7 @@ const LOADING_STATUSES: ReadonlySet<AccountStatus> = new Set(['pending', 'loadin
|
||||
|
||||
const PROVIDER_COPY: Record<AccountProvider, string> = {
|
||||
whatsapp: 'WhatsApp',
|
||||
wechat: 'WeChat',
|
||||
telegram: 'Telegram',
|
||||
linkedin: 'LinkedIn',
|
||||
slack: 'Slack',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FaLinkedin } from 'react-icons/fa';
|
||||
import { FaLinkedin, FaWeixin } from 'react-icons/fa';
|
||||
import { SiDiscord, SiGooglemeet, SiSlack, SiTelegram, SiWhatsapp, SiZoom } from 'react-icons/si';
|
||||
import { TbRobot } from 'react-icons/tb';
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { AccountProvider } from '../../types/accounts';
|
||||
*/
|
||||
const PROVIDER_COLOR: Record<AccountProvider, string> = {
|
||||
whatsapp: '#25D366',
|
||||
wechat: '#07C160',
|
||||
telegram: '#229ED9',
|
||||
linkedin: '#0A66C2',
|
||||
slack: '#4A154B',
|
||||
@@ -36,6 +37,8 @@ export const ProviderIcon = ({
|
||||
switch (provider) {
|
||||
case 'whatsapp':
|
||||
return <SiWhatsapp className={className} style={style} />;
|
||||
case 'wechat':
|
||||
return <FaWeixin className={className} style={style} />;
|
||||
case 'telegram':
|
||||
return <SiTelegram className={className} style={style} />;
|
||||
case 'linkedin':
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('resolveIntegrationRoute', () => {
|
||||
expect(resolveIntegrationRoute(n)).toBe('/chat?account=abc');
|
||||
});
|
||||
|
||||
it.each(['gmail', 'slack', 'whatsapp', 'telegram', 'discord', 'linkedin'])(
|
||||
it.each(['gmail', 'slack', 'whatsapp', 'wechat', 'telegram', 'discord', 'linkedin'])(
|
||||
'routes %s provider to /chat',
|
||||
provider => {
|
||||
expect(resolveIntegrationRoute(makeIntegration({ provider }))).toBe('/chat');
|
||||
|
||||
@@ -24,6 +24,7 @@ const MESSAGE_PROVIDERS = new Set([
|
||||
'gmail',
|
||||
'slack',
|
||||
'whatsapp',
|
||||
'wechat',
|
||||
'telegram',
|
||||
'discord',
|
||||
'linkedin',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IS_DEV } from '../utils/config';
|
||||
|
||||
export type AccountProvider =
|
||||
| 'whatsapp'
|
||||
| 'wechat'
|
||||
| 'telegram'
|
||||
| 'linkedin'
|
||||
| 'slack'
|
||||
@@ -74,6 +75,12 @@ const BASE_PROVIDERS: ProviderDescriptor[] = [
|
||||
description: 'Open web.whatsapp.com inside the app and stream chat updates.',
|
||||
serviceUrl: 'https://web.whatsapp.com/',
|
||||
},
|
||||
{
|
||||
id: 'wechat',
|
||||
label: 'WeChat Web',
|
||||
description: 'Open WeChat in-app for QR sign-in and desktop chat access.',
|
||||
serviceUrl: 'https://web.wechat.com/',
|
||||
},
|
||||
{
|
||||
id: 'telegram',
|
||||
label: 'Telegram Web',
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Add WeChat message scraping into context and memory
|
||||
|
||||
## Summary
|
||||
|
||||
Ingest messages from the embedded WeChat webview into OpenHuman so the agent can use recent WeChat conversations as context and persist useful history into memory.
|
||||
|
||||
## Problem
|
||||
|
||||
We can embed WeChat Web in the Tauri shell for sign-in and manual use, but OpenHuman does not yet extract message data from that surface. That leaves a gap for users who coordinate through WeChat, especially for China-based communities and teams. The work needs to respect platform limits in WeChat Web, avoid broad DOM scraping that breaks easily, and keep private message content scoped to explicit product surfaces and consented storage paths.
|
||||
|
||||
## Solution (optional)
|
||||
|
||||
Add a dedicated WeChat webview ingestion path, following the existing Franz-style account model but with provider-specific extraction for chats, messages, unread state, and stable account/chat identifiers. Scope should cover:
|
||||
|
||||
- Tauri/app shell:
|
||||
Add a WeChat-specific extractor path for the embedded webview, with clear load-state handling and bounded retries when login/session state changes.
|
||||
- Core:
|
||||
Define a WeChat webview ingest contract that can write normalized message snapshots into context/memory namespaces, similar to other webview-account providers.
|
||||
- Product safeguards:
|
||||
Gate persistence behind existing memory/write surfaces, document what is stored, and make it easy to disable or purge.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] **Embedded WeChat extraction** — OpenHuman can detect the active WeChat chat surface and extract normalized message data from the embedded webview.
|
||||
- [ ] **Context + memory ingestion** — extracted WeChat messages can be written into the existing context/memory pipeline with provider/account provenance.
|
||||
- [ ] **Unread/respond surfaces** — unread or actionable WeChat activity appears in the same provider-surface/respond-queue flows used by other messaging integrations where applicable.
|
||||
- [ ] **Privacy + control** — users can understand, disable, and purge stored WeChat-derived data using existing account/memory controls or clearly-scoped additions.
|
||||
- [ ] **Tests** — add focused Vitest and Rust coverage for the WeChat provider path, including at least one failure/edge case.
|
||||
- [ ] **Diff coverage ≥ 80%** — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml)).
|
||||
|
||||
- Evaluate whether WeChat needs a native scanner path, injected bridge, or hybrid approach after validating what the embedded web client exposes at runtime.
|
||||
|
||||
## Related
|
||||
|
||||
- Follows the initial Tauri WeChat webview support added in `feat/wechat-webview-support`.
|
||||
Reference in New Issue
Block a user