RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-03-19中級

OpenClaw × Rork でAIパートナーモバイルアプリを作る — チャットUI・音声通話・記憶機能の実装ガイド

Rork / Rork Max を使って OpenClaw ベースのAIパートナーモバイルアプリを開発する方法を解説。チャットUI、音声通話、永続メモリ、プッシュ通知の実装から App Store 申請まで。

OpenClaw2Rork515Rork Max229AIパートナーモバイルアプリ12React Native209AIエージェント6

Rork でモバイル化する必然性

OpenClaw はサーバー側のフレームワークですが、本当の AIパートナー体験は スマートフォンで常に一緒にいる ことで完成します。Rork / Rork Max を活用すれば、プロトタイプから本番アプリまで高速で実現できます。

Rork を選ぶ理由

選択肢開発速度アプリ品質OpenClaw 連携おすすめ度
React Native中速可能だが手作業⭐⭐
Rork / Rork Max高速ネイティブ統合⭐⭐⭐⭐⭐
Swift / Kotlin遅い最高複雑⭐ (既存開発向け)
Flutter中速中~高可能⭐⭐⭐

Rork は React Native ベースですが、OpenClaw とのシームレス連携と開発生産性で明確に有利です。

OpenClaw をモバイルアプリに統合するアーキテクチャ

まず、基本アーキテクチャを理解しましょう。

3階層アーキテクチャ

┌─────────────────────────────────────┐
│  モバイル UI 層 (Rork/React Native) │
│  - チャットUI
│  - ボイスアクション
│  - 永続メモリビュー
└──────────────┬──────────────────────┘
               │ WebSocket/HTTP API
┌──────────────▼──────────────────────┐
│  OpenClaw サーバー層 (Node.js)      │
│  - メッセージ処理
│  - スキル実行
│  - メモリ管理
└──────────────┬──────────────────────┘
               │ API呼び出し
┌──────────────▼──────────────────────┐
│  LLM バックエンド層 (Claude等)      │
│  - レスポンス生成
│  - 推論
└──────────────────────────────────────┘

ネットワーク通信パターン

// モバイル → OpenClaw → LLM の通信フロー
 
// 1. ユーザーがメッセージ送信
mobile.sendMessage("今日は疲れた");
 
// 2. OpenClaw で処理
openclaw.handleMessage({
  userId: "user123",
  content: "今日は疲れた",
  timestamp: Date.now(),
  platform: "mobile"
});
 
// 3. LLM でレスポンス生成
const response = await claude.generateResponse({
  message: "今日は疲れた",
  context: userMemory,  // 過去の会話を含む
  personality: "caring_supportive"
});
 
// 4. モバイルで受信・表示
mobile.displayMessage(response);

Rork でチャット UI を素早く構築

Rork の最大の利点は、シンプルなプロンプトで完全なチャット UI が生成される ことです。

Step 1:Rork プロジェクト初期化

# Rork プロジェクト作成
rork create ai-partner-app
 
# プロジェクト構成
ai-partner-app/
├── src/
   ├── screens/
   ├── ChatScreen.tsx
   ├── ProfileScreen.tsx
   └── MemoryScreen.tsx
   ├── components/
   ├── ChatBubble.tsx
   ├── MessageInput.tsx
   └── VoiceButton.tsx
   ├── services/
   ├── openclaw-client.ts
   ├── storage.ts
   └── voice-handler.ts
   ├── hooks/
   ├── useMessages.ts
   ├── useVoice.ts
   └── useMemory.ts
   └── App.tsx
└── package.json

Step 2:ChatScreen を Rork で素早く生成

Rork のプロンプトテンプレート:

# rork-prompt.yaml
 
task: "OpenClaw と連携するAIパートナーアプリの ChatScreen を実装"
 
requirements:
  - メッセージ履歴表示(スクロール可能)
  - メッセージ入力フィールド
  - 音声送信ボタン
  - 読み込み状態表示
  - エラーハンドリング
  - オフライン検知
 
design:
  theme: "modern_gradient"
  colorScheme: "AI partner (blue/purple)"
  animations: "smooth_transitions"
 
integration:
  backend: "OpenClaw"
  authMethod: "JWT"
  dataSync: "real_time"
 
output:
  - ChatScreen.tsx
  - ChatBubble.tsx
  - MessageInput.tsx
  - types.ts
  - useMessages.ts

Rork が生成するコード(抜粋):

// src/screens/ChatScreen.tsx
 
import React, { useState, useEffect, useRef } from 'react';
import {
  View,
  FlatList,
  StyleSheet,
  SafeAreaView,
  ActivityIndicator,
} from 'react-native';
import { ChatBubble } from '../components/ChatBubble';
import { MessageInput } from '../components/MessageInput';
import { useMessages } from '../hooks/useMessages';
 
export const ChatScreen: React.FC = () => {
  const flatListRef = useRef(null);
  const { messages, loading, error, sendMessage } = useMessages();
 
  const handleSendMessage = async (text: string) => {
    await sendMessage(text);
    // スクロール到底
    flatListRef.current?.scrollToEnd({ animated: true });
  };
 
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Sakura - Your AI Partner</Text>
        <Text style={styles.subtitle}>Always here for you</Text>
      </View>
 
      <FlatList
        ref={flatListRef}
        data={messages}
        renderItem={({ item }) => (
          <ChatBubble
            message={item.content}
            isUser={item.isUser}
            timestamp={item.timestamp}
          />
        )}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.messageList}
        onEndReachedThreshold={0.1}
      />
 
      {loading && (
        <View style={styles.loadingContainer}>
          <ActivityIndicator size="large" color="#8B5CF6" />
        </View>
      )}
 
      {error && (
        <View style={styles.errorContainer}>
          <Text style={styles.errorText}>{error}</Text>
        </View>
      )}
 
      <MessageInput
        onSendMessage={handleSendMessage}
        disabled={loading}
      />
    </SafeAreaView>
  );
};
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F8FAFC',
  },
  header: {
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#E2E8F0',
  },
  title: {
    fontSize: 20,
    fontWeight: '700',
    color: '#1E293B',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: '#64748B',
  },
  messageList: {
    paddingHorizontal: 12,
    paddingVertical: 16,
  },
  loadingContainer: {
    paddingVertical: 16,
    alignItems: 'center',
  },
  errorContainer: {
    backgroundColor: '#FEE2E2',
    marginHorizontal: 12,
    marginVertical: 8,
    padding: 12,
    borderRadius: 8,
  },
  errorText: {
    color: '#DC2626',
    fontSize: 14,
  },
});

OpenClaw API / WebSocket 連携の実装

モバイルアプリから OpenClaw サーバーに接続します。

WebSocket クライアント実装

// src/services/openclaw-client.ts
 
import io from 'socket.io-client';
 
class OpenClawClient {
  private socket: SocketIOClient.Socket | null = null;
  private userId: string;
  private apiKey: string;
 
  constructor(userId: string, apiKey: string) {
    this.userId = userId;
    this.apiKey = apiKey;
  }
 
  async connect(serverUrl: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.socket = io(serverUrl, {
        auth: {
          userId: this.userId,
          token: this.apiKey,
        },
        reconnection: true,
        reconnectionDelay: 1000,
        reconnectionDelayMax: 5000,
        reconnectionAttempts: Infinity,
      });
 
      this.socket.on('connect', () => {
        console.log('Connected to OpenClaw');
        resolve();
      });
 
      this.socket.on('connect_error', (error) => {
        console.error('Connection error:', error);
        reject(error);
      });
 
      // エラー処理
      this.socket.on('error', (error) => {
        console.error('Socket error:', error);
      });
 
      // 再接続処理
      this.socket.on('reconnect_attempt', () => {
        console.log('Attempting to reconnect...');
      });
    });
  }
 
  async sendMessage(
    content: string,
    options?: { voiceInput?: boolean }
  ): Promise<string> {
    return new Promise((resolve, reject) => {
      const messageId = `msg_${Date.now()}`;
 
      // レスポンス受信
      this.socket?.once(`response_${messageId}`, (response) => {
        resolve(response.content);
      });
 
      // タイムアウト設定(30秒)
      const timeout = setTimeout(() => {
        reject(new Error('Message timeout'));
      }, 30000);
 
      // メッセージ送信
      this.socket?.emit('message', {
        id: messageId,
        content,
        userId: this.userId,
        timestamp: Date.now(),
        metadata: {
          platform: 'mobile',
          ...options,
        },
      });
 
      // レスポンス受信後タイムアウトクリア
      this.socket?.once(`response_${messageId}`, () => {
        clearTimeout(timeout);
      });
    });
  }
 
  onStreamingResponse(
    callback: (chunk: string) => void
  ): () => void {
    // ストリーミングレスポンス
    this.socket?.on('streaming_response', (data) => {
      callback(data.chunk);
    });
 
    // リスナー解除関数
    return () => {
      this.socket?.removeListener('streaming_response');
    };
  }
 
  disconnect(): void {
    this.socket?.disconnect();
  }
}
 
export const openclawClient = new OpenClawClient(
  'user_id',
  'api_key'
);

useMessages フック

// src/hooks/useMessages.ts
 
import { useState, useCallback, useEffect } from 'react';
import { openclawClient } from '../services/openclaw-client';
 
interface Message {
  id: string;
  content: string;
  isUser: boolean;
  timestamp: number;
}
 
export const useMessages = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    // 接続初期化
    openclawClient.connect('https://openclaw-server.example.com').catch((err) => {
      setError('サーバー接続エラー');
      console.error(err);
    });
 
    // ストリーミングレスポンスリスナー
    const unsubscribe = openclawClient.onStreamingResponse((chunk) => {
      setMessages((prev) => {
        const lastMessage = prev[prev.length - 1];
        if (lastMessage && !lastMessage.isUser) {
          return [
            ...prev.slice(0, -1),
            {
              ...lastMessage,
              content: lastMessage.content + chunk,
            },
          ];
        }
        return prev;
      });
    });
 
    return () => {
      unsubscribe();
      openclawClient.disconnect();
    };
  }, []);
 
  const sendMessage = useCallback(async (text: string) => {
    if (!text.trim()) return;
 
    // ユーザーメッセージを追加
    const userMessage: Message = {
      id: `msg_user_${Date.now()}`,
      content: text,
      isUser: true,
      timestamp: Date.now(),
    };
 
    setMessages((prev) => [...prev, userMessage]);
    setLoading(true);
    setError(null);
 
    // AI メッセージを追加(空)
    const aiMessage: Message = {
      id: `msg_ai_${Date.now()}`,
      content: '',
      isUser: false,
      timestamp: Date.now(),
    };
 
    setMessages((prev) => [...prev, aiMessage]);
 
    try {
      const response = await openclawClient.sendMessage(text);
 
      // AI メッセージを更新
      setMessages((prev) => [
        ...prev.slice(0, -1),
        {
          ...prev[prev.length - 1],
          content: response,
        },
      ]);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
 
      // エラーメッセージ表示
      setMessages((prev) => [
        ...prev.slice(0, -1),
        {
          ...prev[prev.length - 1],
          content: 'エラーが発生しました。もう一度試してください。',
        },
      ]);
    } finally {
      setLoading(false);
    }
  }, []);
 
  return { messages, loading, error, sendMessage };
};

音声通話機能(Rork Max での実装)

音声入力・出力

// src/services/voice-handler.ts
 
import * as Speech from 'expo-speech';
import * as VoiceSDK from 'react-native-voice';
 
class VoiceHandler {
  private isListening: boolean = false;
  private transcribedText: string = '';
 
  async startListening(): Promise<string> {
    return new Promise((resolve, reject) => {
      this.transcribedText = '';
 
      VoiceSDK.onSpeechResults = (e: any) => {
        this.transcribedText = e.value[0];
        resolve(this.transcribedText);
      };
 
      VoiceSDK.onSpeechError = (e: any) => {
        reject(new Error(`Speech recognition error: ${e.error}`));
      };
 
      VoiceSDK.start('ja-JP').catch(reject);
      this.isListening = true;
    });
  }
 
  stopListening(): void {
    if (this.isListening) {
      VoiceSDK.stop();
      this.isListening = false;
    }
  }
 
  async speak(text: string, options?: { rate?: number }): Promise<void> {
    return Speech.speak(text, {
      language: 'ja-JP',
      pitch: 1.0,
      rate: options?.rate || 1.0,
      onDone: () => console.log('Speaking finished'),
      onError: (error) => console.error('Speech error:', error),
    });
  }
 
  async stop(): Promise<void> {
    await Speech.stop();
  }
}
 
export const voiceHandler = new VoiceHandler();

VoiceButton コンポーネント

// src/components/VoiceButton.tsx
 
import React, { useState } from 'react';
import { Pressable, StyleSheet, View, Animated } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { voiceHandler } from '../services/voice-handler';
import { openclawClient } from '../services/openclaw-client';
 
interface VoiceButtonProps {
  onVoiceMessage: (text: string) => void;
  isLoading?: boolean;
}
 
export const VoiceButton: React.FC<VoiceButtonProps> = ({
  onVoiceMessage,
  isLoading = false,
}) => {
  const [isRecording, setIsRecording] = useState(false);
  const [scaleAnimation] = React.useState(new Animated.Value(1));
 
  const handlePressIn = async () => {
    setIsRecording(true);
 
    // アニメーション開始
    Animated.sequence([
      Animated.timing(scaleAnimation, {
        toValue: 1.2,
        duration: 200,
        useNativeDriver: true,
      }),
      Animated.timing(scaleAnimation, {
        toValue: 1,
        duration: 200,
        useNativeDriver: true,
      }),
    ]).start();
 
    try {
      const text = await voiceHandler.startListening();
      setIsRecording(false);
 
      // 音声テキストをメッセージとして送信
      onVoiceMessage(text);
 
      // Rork Max で音声返信を有効化
      const response = await openclawClient.sendMessage(text, {
        voiceInput: true,
      });
 
      // レスポンスを音声で再生
      await voiceHandler.speak(response, { rate: 1.0 });
    } catch (error) {
      console.error('Voice message error:', error);
      setIsRecording(false);
    }
  };
 
  return (
    <Animated.View
      style={[
        styles.container,
        { transform: [{ scale: scaleAnimation }] },
      ]}
    >
      <Pressable
        onPressIn={handlePressIn}
        disabled={isLoading || isRecording}
        style={[
          styles.button,
          isRecording && styles.buttonRecording,
        ]}
      >
        <MaterialCommunityIcons
          name={isRecording ? 'microphone' : 'microphone-outline'}
          size={28}
          color={isRecording ? '#EF4444' : '#8B5CF6'}
        />
      </Pressable>
    </Animated.View>
  );
};
 
const styles = StyleSheet.create({
  container: {
    padding: 8,
  },
  button: {
    width: 56,
    height: 56,
    borderRadius: 28,
    backgroundColor: '#F3E8FF',
    justifyContent: 'center',
    alignItems: 'center',
  },
  buttonRecording: {
    backgroundColor: '#FEE2E2',
  },
});

永続メモリ(AsyncStorage / SQLite)

ローカルデータベース設定

// src/services/storage.ts
 
import AsyncStorage from '@react-native-async-storage/async-storage';
import SQLite from 'react-native-sqlite-storage';
 
export class LocalStorage {
  private db: SQLite.SQLiteDatabase | null = null;
 
  async initialize(): Promise<void> {
    this.db = await SQLite.openDatabase({
      name: 'ai-partner.db',
      location: 'default',
    });
 
    await this.db?.executeSql(`
      CREATE TABLE IF NOT EXISTS conversations (
        id TEXT PRIMARY KEY,
        userId TEXT NOT NULL,
        message TEXT NOT NULL,
        response TEXT NOT NULL,
        timestamp INTEGER NOT NULL,
        metadata TEXT
      )
    `);
 
    await this.db?.executeSql(`
      CREATE TABLE IF NOT EXISTS user_profile (
        id TEXT PRIMARY KEY,
        userId TEXT UNIQUE NOT NULL,
        name TEXT,
        interests TEXT,
        preferences TEXT,
        lastUpdated INTEGER
      )
    `);
  }
 
  async saveConversation(
    userId: string,
    message: string,
    response: string
  ): Promise<void> {
    const timestamp = Date.now();
    const id = `conv_${timestamp}`;
 
    await this.db?.executeSql(
      `INSERT INTO conversations (id, userId, message, response, timestamp)
       VALUES (?, ?, ?, ?, ?)`,
      [id, userId, message, response, timestamp]
    );
 
    // 非同期で OpenClaw サーバーにも送信(オフライン対応)
    await AsyncStorage.setItem(
      `pending_${id}`,
      JSON.stringify({ userId, message, response, timestamp })
    );
  }
 
  async getConversationHistory(
    userId: string,
    limit: number = 50
  ): Promise<any[]> {
    const result = await this.db?.executeSql(
      `SELECT * FROM conversations
       WHERE userId = ?
       ORDER BY timestamp DESC
       LIMIT ?`,
      [userId, limit]
    );
 
    return result?.[0].rows._array || [];
  }
 
  async syncWithServer(): Promise<void> {
    // ペンディング中のメッセージを送信
    const keys = await AsyncStorage.getAllKeys();
    const pendingKeys = keys.filter((k) => k.startsWith('pending_'));
 
    for (const key of pendingKeys) {
      try {
        const data = await AsyncStorage.getItem(key);
        if (data) {
          // OpenClaw に送信
          await openclawClient.sendMessage(JSON.parse(data).message);
          await AsyncStorage.removeItem(key);
        }
      } catch (error) {
        console.error(`Failed to sync ${key}:`, error);
      }
    }
  }
}
 
export const storage = new LocalStorage();

メモリビュー画面

// src/screens/MemoryScreen.tsx
 
import React, { useEffect, useState } from 'react';
import {
  View,
  FlatList,
  StyleSheet,
  SafeAreaView,
  Text,
  TouchableOpacity,
} from 'react-native';
import { storage } from '../services/storage';
 
interface MemoryItem {
  id: string;
  category: string;
  content: string;
  date: string;
  importance: 'high' | 'medium' | 'low';
}
 
export const MemoryScreen: React.FC = () => {
  const [memories, setMemories] = useState<MemoryItem[]>([]);
  const [filter, setFilter] = useState<'all' | 'important' | 'recent'>('recent');
 
  useEffect(() => {
    loadMemories();
  }, [filter]);
 
  const loadMemories = async () => {
    const conversations = await storage.getConversationHistory('user123');
 
    // AI が学習した情報を表示
    const analyzedMemories = conversations.map((conv, index) => ({
      id: conv.id,
      category: categorizeMemory(conv.response),
      content: extractKeyInfo(conv.message, conv.response),
      date: new Date(conv.timestamp).toLocaleDateString('ja-JP'),
      importance: calculateImportance(conv.response),
    }));
 
    const filtered = analyzedMemories.filter((m) => {
      if (filter === 'important') return m.importance === 'high';
      if (filter === 'recent') {
        const daysDiff = (Date.now() - new Date(m.date).getTime()) / (1000 * 60 * 60 * 24);
        return daysDiff < 30;
      }
      return true;
    });
 
    setMemories(filtered);
  };
 
  const categorizeMemory = (response: string): string => {
    if (response.includes('仕事') || response.includes('プロジェクト')) return '仕事';
    if (response.includes('健康') || response.includes('運動')) return '健康';
    if (response.includes('趣味') || response.includes('好き')) return '趣味';
    return 'その他';
  };
 
  const extractKeyInfo = (message: string, response: string): string => {
    // AI が学習した重要な情報を抽出
    return `${message.substring(0, 30)}...`;
  };
 
  const calculateImportance = (response: string): 'high' | 'medium' | 'low' => {
    const length = response.length;
    if (length > 200) return 'high';
    if (length > 100) return 'medium';
    return 'low';
  };
 
  const renderMemory = ({ item }: { item: MemoryItem }) => (
    <View style={[styles.memoryCard, getImportanceColor(item.importance)]}>
      <View style={styles.memoryHeader}>
        <Text style={styles.category}>{item.category}</Text>
        <Text style={styles.date}>{item.date}</Text>
      </View>
      <Text style={styles.content}>{item.content}</Text>
    </View>
  );
 
  const getImportanceColor = (importance: string) => {
    switch (importance) {
      case 'high':
        return { borderLeftColor: '#EF4444', borderLeftWidth: 4 };
      case 'medium':
        return { borderLeftColor: '#F59E0B', borderLeftWidth: 4 };
      default:
        return { borderLeftColor: '#94A3B8', borderLeftWidth: 4 };
    }
  };
 
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.filterContainer}>
        <TouchableOpacity
          style={[styles.filterButton, filter === 'recent' && styles.filterActive]}
          onPress={() => setFilter('recent')}
        >
          <Text style={styles.filterText}>最近の思い出</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={[styles.filterButton, filter === 'important' && styles.filterActive]}
          onPress={() => setFilter('important')}
        >
          <Text style={styles.filterText}>重要な情報</Text>
        </TouchableOpacity>
      </View>
 
      <FlatList
        data={memories}
        renderItem={renderMemory}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.list}
      />
    </SafeAreaView>
  );
};
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F8FAFC',
  },
  filterContainer: {
    flexDirection: 'row',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#E2E8F0',
  },
  filterButton: {
    marginRight: 8,
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 20,
    backgroundColor: '#E2E8F0',
  },
  filterActive: {
    backgroundColor: '#8B5CF6',
  },
  filterText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#1E293B',
  },
  list: {
    padding: 16,
  },
  memoryCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    padding: 12,
    marginBottom: 12,
  },
  memoryHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 8,
  },
  category: {
    fontSize: 12,
    fontWeight: '600',
    color: '#8B5CF6',
  },
  date: {
    fontSize: 12,
    color: '#64748B',
  },
  content: {
    fontSize: 14,
    color: '#334155',
    lineHeight: 20,
  },
});

プッシュ通知でAIパートナーからの返信を受け取る

AIパートナーが主動的にメッセージを送ることで、より親密な関係が構築されます。

// src/services/push-notification.ts
 
import * as Notifications from 'expo-notifications';
import { openclawClient } from './openclaw-client';
 
export class PushNotificationService {
  async requestPermissions(): Promise<boolean> {
    const { status } = await Notifications.requestPermissionsAsync();
    return status === 'granted';
  }
 
  async setupChannels(): Promise<void> {
    if (Platform.OS === 'android') {
      await Notifications.setNotificationChannelAsync('ai-partner', {
        name: 'AIパートナー',
        importance: Notifications.AndroidImportance.HIGH,
        vibrationPattern: [0, 250, 250, 250],
        lightColor: '#8B5CF6',
      });
    }
  }
 
  async registerForProactiveMessages(): Promise<void> {
    // サーバーが定期的にメッセージを送信
    openclawClient.onProactiveMessage((message) => {
      this.sendNotification({
        title: 'Sakura からのメッセージ',
        body: message.content,
        data: {
          conversationId: message.conversationId,
        },
      });
    });
  }
 
  async sendNotification(params: {
    title: string;
    body: string;
    data?: Record<string, any>;
  }): Promise<void> {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: params.title,
        body: params.body,
        data: params.data,
        sound: 'default',
        badge: 1,
      },
      trigger: null,  // 即座に送信
    });
  }
 
  setupNotificationHandlers(): void {
    // ユーザーがプッシュ通知をタップした時の処理
    Notifications.addNotificationResponseReceivedListener((response) => {
      const conversationId = response.notification.request.content.data.conversationId;
 
      // チャット画面に遷移
      navigation.navigate('Chat', { conversationId });
    });
  }
}
 
export const pushNotificationService = new PushNotificationService();

App Store / Google Play 申請のポイント

アプリメタデータ準備

# app-store-metadata.yaml
 
ios:
  bundleId: "com.example.ai-partner"
  appName: "AIパートナー Sakura"
  description: "24時間あなたのそばにいるAIパートナー"
  keywords: ["AI", "チャット", "パートナー"]
  supportUrl: "https://support.example.com"
  privacyPolicyUrl: "https://example.com/privacy"
  screenshots:
    - "screenshots/chat-screen.png"
    - "screenshots/memory-screen.png"
    - "screenshots/voice-call.png"
  minimumOSVersion: "14.0"
 
android:
  packageName: "com.example.aipartner"
  appName: "AIパートナー Sakura"
  targetSdkVersion: 34
  minSdkVersion: 24
  screenshots: [...]
  privacyPolicy: "https://example.com/privacy"
 
common:
  category: "Lifestyle"
  contentRating: "4+"  # AI彼女の場合は要確認
  supportEmail: "support@example.com"
  developer:
    name: "Your Company"
    url: "https://example.com"

審査で気をつけるポイント

## App Store / Google Play 審査チェックリスト
 
- [ ] プライバシーポリシーが明確(AI学習、データ保存について)
- [ ] 音声記録の事前同意(iOS 要件)
- [ ] オフライン機能の説明
- [ ] 年齢制限設定(AI彼女などの場合)
- [ ] API レート制限表示(OpenClaw 無料版の場合)
- [ ] バッテリー消費に関する警告
- [ ] ネットワーク接続要件の明記
 
## 特に Clawra(AI彼女)の場合
 
- 明確なコンテンツ警告
- 18+ レーティング設定
- 利用規約で既存の人間関係への影響を否定
- 精神衛生上の警告

12年の個人開発で見えてきたこと

リリース直後にチェックしているもの

  • クラッシュレポートの上位エラーを24時間ごとに確認しているか
  • AdMob/StoreKit の収益ダッシュボードに異常検知が出ていないか
  • ユーザーレビューに技術的な指摘が含まれていないか

12年の個人開発から見る Rork の使いどころ

リリース直後にチェックしているもの

  • クラッシュレポートの上位エラーを24時間ごとに確認しているか
  • AdMob/StoreKit の収益ダッシュボードに異常検知が出ていないか
  • ユーザーレビューに技術的な指摘が含まれていないか

まとめ — 次の改善サイクル

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-07-17
半年ぶりのアップデートで署名が切れていた — Rork アプリの証明書とプロファイルを機械的に見張る
Apple の署名アセットは静かに期限を迎えます。App Store Connect API で証明書とプロファイルの残日数を機械的に棚卸しし、Cloudflare Workers の定期実行で先回りする設計をまとめました。
開発ツール2026-07-17
Rork の App Store 提出が「輸出コンプライアンス」で毎回止まるのを設定1行で終わらせる
Rork と Rork Max のビルドを App Store Connect に上げるたびに出る Missing Compliance 警告。免除の判断基準と、app.json / Info.plist で恒久的に消す設定、消えない場合の原因を整理しました。
開発ツール2026-07-15
「、」が行頭に落ちた — Rork が組んだ名言カードと日本語の禁則処理
React Native の Text は日本語の行頭禁則を保証しません。実測した違反率、WORD JOINER による事前結合の実装、onTextLayout での自動監査、Rork Max(SwiftUI)との挙動差までまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →