File size: 8,953 Bytes
69a3dd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5db8244
 
 
 
 
 
 
 
69a3dd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213

export interface Message {
  role: 'user' | 'assistant' | 'system';
  content: string;
  timestamp?: number;
}

export interface ChatSession {
  id: string;
  title: string;
  timestamp: number;
  messages: Message[];
}

const API_URL = 'https://api.siliconflow.cn/v1/chat/completions';
const API_KEY_STORAGE_KEY = 'siliconflow_api_key';
const HISTORY_STORAGE_KEY = 'manus_chat_history';

const DEMO_SESSIONS: ChatSession[] = [
  {
    id: 'demo-1',
    title: 'React 组件优化指南',
    timestamp: Date.now() - 1000 * 60 * 60 * 2,
    messages: [
      { role: 'user', content: '如何优化 React 组件的渲染性能?', timestamp: Date.now() - 1000 * 60 * 60 * 2 },
      { role: 'assistant', content: '### React 组件性能优化策略\n\n#### 1. 减少不必要的渲染\n- **使用 `React.memo`**:对于纯展示组件,使用 `memo` 包裹,避免父组件更新导致的不必要重渲染。\n- **`useCallback` 和 `useMemo`**:缓存函数和计算结果,避免作为 props 传递时导致子组件更新。\n\n#### 2. 状态管理优化\n- **状态下放 (State Colocation)**:将状态尽可能靠近使用它的组件,避免在顶层组件管理过多状态。\n- **Context 拆分**:避免将大对象放入 Context,导致所有消费者组件在部分属性变化时重渲染。\n\n#### 3. 列表渲染\n- **使用 `key`**:确保列表项有唯一且稳定的 `key`。\n- **虚拟滚动**:对于长列表,使用 `react-window` 或 `react-virtualized` 进行虚拟渲染。\n\n#### 4. 代码分割\n- **`React.lazy` + `Suspense`**:对路由组件或大型组件进行懒加载,减小首屏 bundle 体积。', timestamp: Date.now() - 1000 * 60 * 60 * 2 + 1000 }
    ]
  },
  {
    id: 'demo-2',
    title: 'Python 异步编程入门',
    timestamp: Date.now() - 1000 * 60 * 60 * 24,
    messages: [
      { role: 'user', content: 'Python 中的 async/await 是什么?', timestamp: Date.now() - 1000 * 60 * 60 * 24 },
      { role: 'assistant', content: '### Python 异步编程 (async/await) 简介\n\n`async` 和 `await` 是 Python 3.5 引入的用于编写并发代码的语法,基于 `asyncio` 库。\n\n#### 核心概念\n1. **协程 (Coroutine)**:使用 `async def` 定义的函数。调用它不会立即执行,而是返回一个协程对象。\n2. **事件循环 (Event Loop)**:负责调度和运行协程的机制。\n3. **Awaitable**:可以在 `await` 表达式中使用的对象(如协程、Task、Future)。\n\n#### 简单示例\n```python\nimport asyncio\n\nasync def say_hello():\n    print("Hello")\n    await asyncio.sleep(1) # 模拟IO操作\n    print("World")\n\nasync def main():\n    await asyncio.gather(say_hello(), say_hello())\n\nif __name__ == "__main__":\n    asyncio.run(main())\n```\n\n#### 适用场景\n- **IO 密集型任务**:如网络请求、数据库读写。异步可以在等待 IO 时执行其他任务,极大提高吞吐量。\n- **Web 服务器**:如 FastAPI,天生支持异步,性能优越。', timestamp: Date.now() - 1000 * 60 * 60 * 24 + 1000 }
    ]
  },
  {
    id: 'demo-3',
    title: 'Docker 容器化部署',
    timestamp: Date.now() - 1000 * 60 * 60 * 48,
    messages: [
      { role: 'user', content: '编写一个简单的 Dockerfile', timestamp: Date.now() - 1000 * 60 * 60 * 48 },
      { role: 'assistant', content: '### Node.js 应用的 Dockerfile 示例\n\n这是一个标准的多阶段构建 Dockerfile,用于优化镜像体积:\n\n```dockerfile\n# Build Stage\nFROM node:18-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# Production Stage\nFROM node:18-alpine\nWORKDIR /app\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/package*.json ./\nRUN npm ci --only=production\n\nEXPOSE 3000\nCMD ["node", "dist/index.js"]\n```\n\n#### 关键点解析\n- **Alpine 镜像**:使用 `alpine` 版本减小基础镜像体积。\n- **多阶段构建**:将构建环境(包含 devDependencies)和运行环境(仅生产依赖)分离。\n- **npm ci**:在 CI/CD 环境中使用,确保依赖版本与 lock 文件完全一致。', timestamp: Date.now() - 1000 * 60 * 60 * 48 + 1000 }
    ]
  }
];

export const chatService = {
  getApiKey(): string | null {
    return localStorage.getItem(API_KEY_STORAGE_KEY);
  },

  setApiKey(key: string) {
    localStorage.setItem(API_KEY_STORAGE_KEY, key);
  },

  getHistory(): ChatSession[] {
    const stored = localStorage.getItem(HISTORY_STORAGE_KEY);
    if (!stored) {
      this.saveHistory(DEMO_SESSIONS);
      return DEMO_SESSIONS;
    }
    
    let sessions: ChatSession[] = JSON.parse(stored);
    
    if (sessions.length === 0) {
      this.saveHistory(DEMO_SESSIONS);
      return DEMO_SESSIONS;
    }

    let hasChanges = false;

    // Data migration: Ensure all messages have timestamps
    sessions = sessions.map(session => {
      let sessionChanged = false;
      const newMessages = session.messages.map((msg, index) => {
        if (!msg.timestamp) {
          sessionChanged = true;
          hasChanges = true;
          // Fallback: Use session timestamp + small offset to maintain order
          const baseTime = session.timestamp || Date.now();
          return { ...msg, timestamp: baseTime + (index * 1000) };
        }
        return msg;
      });

      if (sessionChanged) {
        return { ...session, messages: newMessages };
      }
      return session;
    });

    // Persist changes if any migration happened
    if (hasChanges) {
      this.saveHistory(sessions);
    }

    return sessions;
  },

  saveHistory(sessions: ChatSession[]) {
    localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(sessions));
  },

  deleteSession(sessionId: string) {
    const sessions = this.getHistory();
    const newSessions = sessions.filter(s => s.id !== sessionId);
    this.saveHistory(newSessions);
    return newSessions;
  },

  updateSession(updatedSession: ChatSession) {
    const sessions = this.getHistory();
    const index = sessions.findIndex(s => s.id === updatedSession.id);
    if (index !== -1) {
      sessions[index] = updatedSession;
      this.saveHistory(sessions);
    }
    return sessions;
  },

  createSession(initialMessage: string): ChatSession {
    const generateUUID = () => {
      if (typeof crypto !== 'undefined' && crypto.randomUUID) {
        return crypto.randomUUID();
      }
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    };

    const session: ChatSession = {
      id: generateUUID(),
      title: initialMessage.slice(0, 20) + (initialMessage.length > 20 ? '...' : ''),
      timestamp: Date.now(),
      messages: [{ role: 'user', content: initialMessage, timestamp: Date.now() }]
    };
    return session;
  },

  async sendMessage(messages: Message[], onChunk: (content: string) => void, signal?: AbortSignal): Promise<void> {
    let apiKey = this.getApiKey();
    if (!apiKey) {
      const input = prompt('Please enter your SiliconFlow API Key:');
      if (input) {
        this.setApiKey(input);
        apiKey = input;
      }
    }
    
    try {
      const response = await fetch(API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey || ''}`
        },
        body: JSON.stringify({
          model: 'deepseek-ai/DeepSeek-V3',
          messages: messages,
          stream: true
        }),
        signal // Pass the abort signal to fetch
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'API request failed');
      }

      const reader = response.body?.getReader();
      const decoder = new TextDecoder('utf-8');

      if (!reader) throw new Error('Response body is unavailable');

      // Handle abort signal for the reader loop
      if (signal) {
        signal.addEventListener('abort', () => {
          reader.cancel();
        });
      }

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value, { stream: true });
        const lines = chunk.split('\n');

        for (const line of lines) {
            if (line.startsWith('data: ') && line !== 'data: [DONE]') {
                try {
                    const data = JSON.parse(line.slice(6));
                    const content = data.choices[0]?.delta?.content || '';
                    if (content) {
                        onChunk(content);
                    }
                } catch (e) {
                    console.error('Error parsing chunk', e);
                }
            }
        }
      }
    } catch (error) {
      console.error('Chat error:', error);
      throw error;
    }
  }
};