Просмотр исходного кода

feat(v0.4.0): mobile app scaffold + cloud sync server

- packages/mobile/ — React Native project (iOS + Android)
- packages/kirincode-cloud/ — Cloud sync backend (Bun + Elysia + JWT)
- packages/kirincode-ohos/ — HarmonyOS phone UI (PhoneApp.ets)
- ARCHITECTURE.md — Full mobile + cloud sync design doc
- API: sessions, messages, memory, skills, workflows, auth
- Offline-first with local SQLite caching
KirinCode 1 месяц назад
Родитель
Сommit
0904a250e2

+ 26 - 0
packages/kirincode-cloud/package.json

@@ -0,0 +1,26 @@
+{
+  "name": "@kirincode-ai/kirincode-cloud",
+  "version": "0.4.0",
+  "type": "module",
+  "description": "KirinCode Cloud Sync Server — multi-user backend with session sync, memory, skills, workflows",
+  "license": "MIT",
+  "scripts": {
+    "dev": "bun run src/index.ts",
+    "db:migrate": "bun run src/db/migrate.ts",
+    "start": "bun run src/index.ts"
+  },
+  "dependencies": {
+    "elysia": "^1.0.0",
+    "@elysiajs/jwt": "^1.0.0",
+    "@elysiajs/cors": "^1.0.0",
+    "drizzle-orm": "^0.36.0",
+    "postgres": "^3.4.0",
+    "bcryptjs": "^2.4.3",
+    "uuid": "^10.0.0"
+  },
+  "devDependencies": {
+    "drizzle-kit": "^0.28.0",
+    "typescript": "catalog:",
+    "@types/bcryptjs": "^2.4.0"
+  }
+}

+ 142 - 0
packages/kirincode-cloud/src/index.ts

@@ -0,0 +1,142 @@
+// KirinCode Cloud Sync Server
+// Bun + Elysia + Drizzle ORM + PostgreSQL
+
+import { Elysia, t } from "elysia"
+import { jwt } from "@elysiajs/jwt"
+import { cors } from "@elysiajs/cors"
+import { hash, compare } from "bcryptjs"
+import { v4 as uuid } from "uuid"
+
+const JWT_SECRET = process.env.JWT_SECRET || "kirincode-cloud-secret-change-me"
+const PORT = parseInt(process.env.PORT || "3000")
+
+// In-memory store (replace with PostgreSQL + Drizzle in production)
+const db = {
+  users: new Map<string, { id: string; email: string; password: string }>(),
+  sessions: new Map<string, any>(),
+  messages: new Map<string, any[]>(),
+  memory: new Map<string, any[]>(),
+  skills: new Map<string, any[]>(),
+  workflows: new Map<string, any[]>(),
+}
+
+const app = new Elysia()
+  .use(cors())
+  .use(jwt({ name: "jwt", secret: JWT_SECRET }))
+
+  // Auth middleware
+  .derive(async ({ jwt, headers, set }) => {
+    const auth = headers["authorization"]
+    if (!auth?.startsWith("Bearer ")) return { userId: null }
+    try {
+      const payload = await jwt.verify(auth.slice(7))
+      return { userId: (payload as any).sub as string }
+    } catch {
+      return { userId: null }
+    }
+  })
+
+  .guard({ beforeHandle: ({ userId, set }) => {
+    if (!userId) { set.status = 401; return "Unauthorized" }
+  }})
+
+  // === Auth ===
+  .post("/api/v1/auth/register", async ({ body }) => {
+    const id = uuid()
+    const password = await hash(body.password, 10)
+    db.users.set(id, { id, email: body.email, password })
+    const token = await app.decorator.jwt.sign({ sub: id })
+    return { token, user: { id, email: body.email } }
+  }, { body: t.Object({ email: t.String(), password: t.String() }) })
+
+  .post("/api/v1/auth/login", async ({ body, set }) => {
+    const user = Array.from(db.users.values()).find(u => u.email === body.email)
+    if (!user || !(await compare(body.password, user.password))) {
+      set.status = 401
+      return { error: "Invalid credentials" }
+    }
+    const token = await app.decorator.jwt.sign({ sub: user.id })
+    return { token, user: { id: user.id, email: user.email } }
+  }, { body: t.Object({ email: t.String(), password: t.String() }) })
+
+  // === Sessions ===
+  .get("/api/v1/sessions", async ({ userId }) => {
+    const list = Array.from(db.sessions.values()).filter(s => s.userId === userId)
+    return { data: list }
+  })
+
+  .post("/api/v1/sessions", async ({ userId, body }) => {
+    const session = { id: uuid(), userId, title: body.title || "New Chat", agent: body.agent || "build", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
+    db.sessions.set(session.id, session)
+    db.messages.set(session.id, [])
+    return { data: session }
+  }, { body: t.Object({ title: t.Optional(t.String()), agent: t.Optional(t.String()) }) })
+
+  .post("/api/v1/sessions/:id/prompt", async ({ userId, params, body }) => {
+    const session = db.sessions.get(params.id)
+    if (!session || session.userId !== userId) return { error: "Session not found" }
+    const msgs = db.messages.get(params.id) || []
+    const userMsg = { id: uuid(), role: "user", content: (body.parts || []).filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n"), createdAt: new Date().toISOString() }
+    msgs.push(userMsg)
+    // AI response placeholder — in production, call LLM provider
+    const aiMsg = { id: uuid(), role: "assistant", content: "[AI response — connect LLM provider]", createdAt: new Date().toISOString() }
+    msgs.push(aiMsg)
+    db.messages.set(params.id, msgs)
+    db.sessions.set(params.id, { ...session, updatedAt: new Date().toISOString() })
+    return { data: { messages: msgs } }
+  })
+
+  .get("/api/v1/sessions/:id/messages", async ({ userId, params }) => {
+    const session = db.sessions.get(params.id)
+    if (!session || session.userId !== userId) return { error: "Session not found" }
+    return { data: db.messages.get(params.id) || [] }
+  })
+
+  // === Memory ===
+  .get("/api/v1/memory", async ({ userId }) => {
+    return { data: db.memory.get(userId!) || [] }
+  })
+
+  .post("/api/v1/memory", async ({ userId, body }) => {
+    const list = db.memory.get(userId!) || []
+    const entry = { id: uuid(), ...body, createdAt: new Date().toISOString() }
+    list.push(entry)
+    db.memory.set(userId!, list)
+    return { data: entry }
+  })
+
+  // === Skills ===
+  .get("/api/v1/skills", async ({ userId }) => {
+    return { data: db.skills.get(userId!) || [] }
+  })
+
+  .post("/api/v1/skills", async ({ userId, body }) => {
+    const list = db.skills.get(userId!) || []
+    const skill = { id: uuid(), ...body, createdAt: new Date().toISOString() }
+    list.push(skill)
+    db.skills.set(userId!, list)
+    return { data: skill }
+  })
+
+  // === Workflows ===
+  .get("/api/v1/workflows", async ({ userId }) => {
+    return { data: db.workflows.get(userId!) || [] }
+  })
+
+  .post("/api/v1/workflows", async ({ userId, body }) => {
+    const list = db.workflows.get(userId!) || []
+    const wf = { id: uuid(), ...body, createdAt: new Date().toISOString() }
+    list.push(wf)
+    db.workflows.set(userId!, list)
+    return { data: wf }
+  })
+
+  // === Health ===
+  .get("/api/v1/health", () => ({ status: "ok", version: "0.4.0" }))
+
+  .listen(PORT)
+
+console.log(`🦄 KirinCode Cloud Server running on http://localhost:${PORT}`)
+console.log(`   API: http://localhost:${PORT}/api/v1/health`)
+
+export type App = typeof app

+ 109 - 0
packages/kirincode-ohos/entry/src/main/ets/pages/phone/PhoneApp.ets

@@ -0,0 +1,109 @@
+// KirinCode HarmonyOS Phone UI — optimized for mobile portrait
+
+import { promptAction } from '@kit.ArkUI';
+import { KirinCodeClient, Message, Session } from '../../service/KirinCodeClient';
+
+const client = new KirinCodeClient();
+const MODES = ['build', 'plan', 'debug', 'solo'] as const;
+type Mode = typeof MODES[number];
+
+@Entry
+@Component
+struct PhoneApp {
+  @State messages: Message[] = [];
+  @State inputText: string = '';
+  @State currentSession: Session | null = null;
+  @State connected: boolean = false;
+  @State connecting: boolean = true;
+  @State selectedMode: Mode = 'build';
+  @State sessions: Session[] = [];
+
+  async aboutToAppear(): Promise<void> {
+    this.connected = await client.healthCheck();
+    this.connecting = false;
+    if (this.connected) {
+      try { this.sessions = await client.listSessions(); } catch { /* offline */ }
+    }
+  }
+
+  async createSession(): Promise<void> {
+    try {
+      const s = await client.createSession(`Chat ${new Date().toLocaleTimeString()}`);
+      this.currentSession = s;
+      this.messages = [];
+      try { this.sessions = await client.listSessions(); } catch { /* */ }
+    } catch {
+      promptAction.showToast({ message: 'Failed to create session' });
+    }
+  }
+
+  async sendMessage(): Promise<void> {
+    const text = this.inputText.trim();
+    if (!text || !this.currentSession) return;
+    const userMsg: Message = { id: `u_${Date.now()}`, role: 'user', content: text, timestamp: Date.now() };
+    this.messages = [...this.messages, userMsg];
+    this.inputText = '';
+    try {
+      await client.sendPrompt(this.currentSession.id, text, this.selectedMode);
+      this.messages = await client.getMessages(this.currentSession.id);
+    } catch {
+      promptAction.showToast({ message: 'Send failed' });
+    }
+  }
+
+  build() {
+    Column() {
+      // Top bar
+      Row() {
+        Text('K I R I N').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#e74c3c')
+        Blank()
+        Text(this.connecting ? '...' : (this.connected ? '🟢' : '🔴')).fontSize(12)
+      }
+      .width('100%').padding({ left: 12, right: 12, top: 8, bottom: 6 })
+      .backgroundColor('#1a1a2e')
+
+      // Mode tabs
+      Row({ space: 4 }) {
+        ForEach(MODES, (m: Mode) => {
+          Text(m).fontSize(11).padding({ left: 10, right: 10, top: 4, bottom: 4 })
+            .borderRadius(4).fontColor(this.selectedMode === m ? '#fff' : '#7f8c8d')
+            .backgroundColor(this.selectedMode === m ? '#e74c3c' : 'transparent')
+            .onClick(() => this.selectedMode = m)
+        })
+      }.width('100%').padding({ left: 8, right: 8, top: 4, bottom: 4 }).backgroundColor('#16213e')
+
+      // Chat area
+      if (!this.currentSession) {
+        Column() {
+          Text('🦄').fontSize(48).opacity(0.3)
+          Text('KirinCode Mobile').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#ecf0f1').margin({ top: 8 })
+          Button('New Chat').margin({ top: 16 }).onClick(() => this.createSession())
+        }.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center).backgroundColor('#0f172a')
+      } else {
+        List({ space: 6 }) {
+          ForEach(this.messages, (msg: Message) => {
+            ListItem() {
+              Column() {
+                Text(msg.content).fontSize(14).padding(10)
+                  .borderRadius(8).width('90%')
+                  .fontColor(msg.role === 'user' ? '#ecf0f1' : '#bdc3c7')
+                  .backgroundColor(msg.role === 'user' ? '#2c3e5022' : '#16213e')
+                  .alignSelf(msg.role === 'user' ? ItemAlign.End : ItemAlign.Start)
+              }
+            }
+          })
+        }.layoutWeight(1).scrollBar(BarState.Auto).padding(6).backgroundColor('#0f172a')
+      }
+
+      // Input
+      Row() {
+        TextInput({ placeholder: 'Message...', text: this.inputText })
+          .layoutWeight(1).height(40).backgroundColor('#1a1a2e').borderRadius(8)
+          .border({ width: 1, color: '#2c3e50' }).fontColor('#ecf0f1')
+          .onChange((v: string) => this.inputText = v).onSubmit(() => this.sendMessage())
+        Button('→').height(40).margin({ left: 6 }).backgroundColor('#e74c3c').borderRadius(8)
+          .onClick(() => this.sendMessage())
+      }.padding(8).backgroundColor('#1a1a2e').width('100%')
+    }.width('100%').height('100%')
+  }
+}

+ 140 - 0
packages/mobile/ARCHITECTURE.md

@@ -0,0 +1,140 @@
+# KirinCode Mobile — Architecture
+
+## Overview
+
+Mobile apps (iOS / Android / HarmonyOS) that mirror the desktop experience:
+- AI chat sessions with build/plan/debug/solo modes
+- Auto-memory and skill system sync
+- Workflow engine triggers
+- Real-time cloud sync across all devices
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────┐
+│                    Mobile Clients                     │
+├──────────────┬──────────────┬────────────────────────┤
+│   iOS App    │  Android App │  HarmonyOS App         │
+│  (React Ntv) │ (React Ntv)  │  (ArkUI)              │
+│  SwiftUI     │  Jetpack     │  @ohos.net.http        │
+│  bridge      │  Compose     │                        │
+└──────┬───────┴──────┬───────┴──────────┬─────────────┘
+       │              │                  │
+       └──────────────┼──────────────────┘
+                      │ HTTPS + WebSocket
+              ┌───────▼──────────┐
+              │   Cloud Server   │
+              │  (Bun + Elysia)  │
+              │                  │
+              │  /api/v1/        │
+              │   - sessions     │
+              │   - memory       │
+              │   - skills       │
+              │   - workflows    │
+              │   - auth         │
+              │   - sync (WS)    │
+              └───────┬──────────┘
+                      │
+              ┌───────▼──────────┐
+              │   PostgreSQL     │
+              │   + pgvector     │
+              │                  │
+              │   Tables:        │
+              │   - users        │
+              │   - sessions     │
+              │   - messages     │
+              │   - memory_entries│
+              │   - skills       │
+              │   - workflows    │
+              │   - sync_log     │
+              └──────────────────┘
+```
+
+## Sync Protocol
+
+### Real-time Sync (WebSocket)
+
+```
+Client ──connect──> Server
+Client <──session:updated── Server
+Client ──prompt──> Server
+Client <──message:streaming── Server
+Client <──message:completed── Server
+```
+
+### Offline Support
+
+- All data cached locally (SQLite on mobile)
+- Queue mutations when offline
+- Replay on reconnect
+- Conflict resolution: last-write-wins with server timestamp
+
+## Data Model
+
+### Sessions
+```sql
+CREATE TABLE sessions (
+  id UUID PRIMARY KEY,
+  user_id UUID NOT NULL,
+  title TEXT,
+  agent TEXT DEFAULT 'build',
+  created_at TIMESTAMPTZ DEFAULT NOW(),
+  updated_at TIMESTAMPTZ DEFAULT NOW(),
+  archived BOOLEAN DEFAULT FALSE
+);
+```
+
+### Memory
+```sql
+CREATE TABLE memory_entries (
+  id UUID PRIMARY KEY,
+  user_id UUID NOT NULL,
+  project_hash TEXT NOT NULL,
+  category TEXT,
+  content TEXT,
+  embedding VECTOR(1536),
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+```
+
+## API Endpoints
+
+### Auth
+- `POST /api/v1/auth/login` — email + password
+- `POST /api/v1/auth/register` — create account
+- `POST /api/v1/auth/refresh` — refresh JWT
+
+### Sessions
+- `GET /api/v1/sessions` — list user sessions
+- `POST /api/v1/sessions` — create session
+- `POST /api/v1/sessions/:id/prompt` — send prompt
+- `GET /api/v1/sessions/:id/messages` — get messages
+
+### Sync
+- `WS /api/v1/sync` — real-time sync channel
+- `POST /api/v1/sync/push` — push offline changes
+- `GET /api/v1/sync/pull?since=<timestamp>` — pull changes
+
+### Memory & Skills
+- `GET /api/v1/memory` — list memory entries
+- `POST /api/v1/memory` — create memory entry
+- `GET /api/v1/skills` — list skills
+- `POST /api/v1/skills` — create skill
+
+## Mobile App Screens
+
+```
+Navigation:
+├── Sessions (list)
+│   └── Chat (detail)
+│       ├── MessageList
+│       ├── ModeSelector (build/plan/debug/solo)
+│       └── InputBar
+├── Memory (search + list)
+├── Skills (list + detail)
+├── Workflows (list + detail)
+└── Settings
+    ├── Model config
+    ├── Account
+    └── Sync status
+```

+ 28 - 0
packages/mobile/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "@kirincode-ai/mobile",
+  "version": "0.4.0",
+  "private": true,
+  "scripts": {
+    "start": "expo start",
+    "android": "expo start --android",
+    "ios": "expo start --ios",
+    "web": "expo start --web"
+  },
+  "dependencies": {
+    "expo": "~52.0.0",
+    "react": "18.3.1",
+    "react-native": "0.76.0",
+    "react-native-gesture-handler": "~2.20.0",
+    "react-native-reanimated": "~3.16.0",
+    "react-native-safe-area-context": "4.12.0",
+    "react-native-screens": "~4.0.0",
+    "@react-navigation/native": "^7.0.0",
+    "@react-navigation/native-stack": "^7.0.0",
+    "expo-sqlite": "~15.0.0",
+    "expo-secure-store": "~14.0.0"
+  },
+  "devDependencies": {
+    "typescript": "~5.3.0",
+    "@types/react": "~18.3.0"
+  }
+}

+ 94 - 0
packages/mobile/src/services/api.ts

@@ -0,0 +1,94 @@
+// KirinCode Cloud API Client
+const DEFAULT_BASE_URL = "https://api.kirincode.ai/v1"
+
+export interface Session {
+  id: string
+  title: string
+  agent: string
+  createdAt: string
+  updatedAt: string
+}
+
+export interface Message {
+  id: string
+  role: "user" | "assistant"
+  content: string
+  createdAt: string
+}
+
+export class KirinCodeClient {
+  private baseUrl: string
+  private token: string | null = null
+
+  constructor(baseUrl = DEFAULT_BASE_URL) {
+    this.baseUrl = baseUrl
+  }
+
+  setToken(token: string) { this.token = token }
+
+  private headers(): Record<string, string> {
+    const h: Record<string, string> = { "Content-Type": "application/json" }
+    if (this.token) h["Authorization"] = `Bearer ${this.token}`
+    return h
+  }
+
+  async login(email: string, password: string): Promise<string> {
+    const res = await fetch(`${this.baseUrl}/auth/login`, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ email, password }),
+    })
+    const data = await res.json()
+    this.token = data.token
+    return data.token
+  }
+
+  async listSessions(): Promise<Session[]> {
+    const res = await fetch(`${this.baseUrl}/sessions`, { headers: this.headers() })
+    return (await res.json()).data
+  }
+
+  async createSession(title: string, agent = "build"): Promise<Session> {
+    const res = await fetch(`${this.baseUrl}/sessions`, {
+      method: "POST",
+      headers: this.headers(),
+      body: JSON.stringify({ title, agent }),
+    })
+    return (await res.json()).data
+  }
+
+  async sendPrompt(sessionId: string, text: string, agent?: string): Promise<void> {
+    await fetch(`${this.baseUrl}/sessions/${sessionId}/prompt`, {
+      method: "POST",
+      headers: this.headers(),
+      body: JSON.stringify({ parts: [{ type: "text", text }], agent }),
+    })
+  }
+
+  async getMessages(sessionId: string): Promise<Message[]> {
+    const res = await fetch(`${this.baseUrl}/sessions/${sessionId}/messages`, {
+      headers: this.headers(),
+    })
+    return (await res.json()).data
+  }
+
+  // Memory
+  async listMemory(): Promise<any[]> {
+    const res = await fetch(`${this.baseUrl}/memory`, { headers: this.headers() })
+    return (await res.json()).data
+  }
+
+  // Skills  
+  async listSkills(): Promise<any[]> {
+    const res = await fetch(`${this.baseUrl}/skills`, { headers: this.headers() })
+    return (await res.json()).data
+  }
+
+  // Workflows
+  async listWorkflows(): Promise<any[]> {
+    const res = await fetch(`${this.baseUrl}/workflows`, { headers: this.headers() })
+    return (await res.json()).data
+  }
+}
+
+export const client = new KirinCodeClient()