vibe-ai-c 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LLM.txt +71 -0
  2. package/package.json +29 -0
  3. package/vibe-ai.js +279 -0
package/LLM.txt ADDED
@@ -0,0 +1,71 @@
1
+ # VibeAI
2
+
3
+ VibeAI 是一个极简的、面向浏览器的多供应商 AI SDK 与 UI 管理工具。它允许开发者通过几行代码集成 OpenAI 兼容的 API,并提供内置的供应商管理、模型资产池选择以及自动连通性校验功能。
4
+
5
+ ## 核心特性
6
+
7
+ - **多供应商管理**:支持 OpenAI, DeepSeek, Gemini, Qwen 等所有兼容 OpenAI 协议的供应商。
8
+ - **模型资产池 (Model Assets)**:用户可从数百个模型中多选常用模型,构建专属资产池。
9
+ - **自动校验**:后台静默进行连通性检查(✅/❌ 状态实时反馈)。
10
+ - **零依赖 ESM**:纯 JavaScript 实现,无任何外部库依赖。
11
+ - **持久化**:所有配置自动存储于浏览器的 `localStorage` 中。
12
+
13
+ ## 快速开始
14
+
15
+ ### 1. 引入与初始化
16
+ ```javascript
17
+ import { vibeAI } from './vibe-ai.js';
18
+
19
+ // 初始化并绑定 UI 元素
20
+ vibeAI.init({
21
+ setupBtnId: 'settings-btn', // 点击此按钮打开管理弹窗
22
+ modelSelectId: 'model-select' // 自动填充选中的模型资产池
23
+ });
24
+ ```
25
+
26
+ ### 2. 发起对话请求
27
+ ```javascript
28
+ // 支持流式传输 (Streaming)
29
+ const stream = await vibeAI.chat({
30
+ messages: [{ role: 'user', content: 'Hello!' }],
31
+ stream: true
32
+ });
33
+
34
+ for await (const chunk of stream) {
35
+ console.log(chunk); // 逐字输出
36
+ }
37
+
38
+ // 也支持普通请求
39
+ const response = await vibeAI.chat({
40
+ messages: [{ role: 'user', content: 'Hello!' }]
41
+ });
42
+ console.log(response.choices[0].message.content);
43
+ ```
44
+
45
+ ## API 参考
46
+
47
+ ### `vibeAI.init(config)`
48
+ - `setupBtnId`: (可选) 触发设置界面的 DOM ID。
49
+ - `modelSelectId`: (可选) 用于切换当前激活模型的 `<select>` 元素 ID。
50
+
51
+ ### `vibeAI.chat(payload)`
52
+ - `payload`: 符合 OpenAI Chat Completion 格式的对象。
53
+ - `payload.stream`: 布尔值,为 true 时返回 `AsyncGenerator`。
54
+ - **注意**:如果未配置 API Key,调用时会自动弹出设置窗口。
55
+
56
+ ### `vibeAI.openSettings()`
57
+ - 手动打开供应商管理界面。
58
+
59
+ ## 配置规范
60
+
61
+ - **Base URL**: 通常需要包含版本号,例如 `https://api.deepseek.com/v1`。
62
+ - **Presets**: 提供了常用的 Base URL 快速填充(Badges)。
63
+ - **连通性标识**:
64
+ - `⏳`: 正在测试连通性
65
+ - `✅`: 校验通过
66
+ - `❌`: 请求失败或 Key 无效
67
+
68
+ ## 开发建议
69
+
70
+ - 建议在 HTML 中预留一个空的 `<select id="model-select"></select>` 以获得最佳体验。
71
+ - 所有的配置都通过 `vibe_ai_v2_config` 键名存储在 `localStorage` 中。
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "vibe-ai-c",
3
+ "version": "2.1.0",
4
+ "description": "一个极简的、面向浏览器的多供应商 AI SDK 与 UI 管理工具。支持 OpenAI 兼容协议。",
5
+ "main": "vibe-ai.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "npx servor . demo.html 8080",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [
12
+ "ai",
13
+ "openai",
14
+ "deepseek",
15
+ "llm",
16
+ "sdk",
17
+ "ui-component",
18
+ "browser-only"
19
+ ],
20
+ "author": "Your Name",
21
+ "license": "MIT",
22
+ "dependencies": {},
23
+ "devDependencies": {},
24
+ "files": [
25
+ "vibe-ai.js",
26
+ "llm.txt",
27
+ "README.md"
28
+ ]
29
+ }
package/vibe-ai.js ADDED
@@ -0,0 +1,279 @@
1
+ /**
2
+ * VibeAI v2.1 - 完整修正版
3
+ */
4
+
5
+ const STORAGE_KEY = 'vibe_ai_v2_config';
6
+
7
+ class VibeAI {
8
+ constructor() {
9
+ this.data = this._load();
10
+ this._injectStyles();
11
+ this.presets = {
12
+ 'OpenAI': 'https://api.openai.com/v1',
13
+ 'DeepSeek': 'https://api.deepseek.com',
14
+ 'Gemini': 'https://generativelanguage.googleapis.com/v1beta/openai',
15
+ 'Qwen': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
16
+ };
17
+ this._createModal();
18
+ this.modelSelectElement = null;
19
+ }
20
+
21
+ // --- 初始化 ---
22
+ init({ setupBtnId, modelSelectId } = {}) {
23
+ if (setupBtnId) document.getElementById(setupBtnId).onclick = () => this.openSettings();
24
+ if (modelSelectId) {
25
+ this.modelSelectElement = document.getElementById(modelSelectId);
26
+ this.modelSelectElement.onchange = (e) => {
27
+ this.data.activeModel = e.target.value;
28
+ this._save();
29
+ };
30
+ this._refreshExternalSelect();
31
+ }
32
+ return this;
33
+ }
34
+
35
+ // --- 核心对话 API ---
36
+ async chat(payload) {
37
+ const profile = this._getActiveProfile();
38
+ if (!profile) { this.openSettings(); throw new Error('请先配置 AI 供应商'); }
39
+
40
+ const response = await fetch(`${profile.baseUrl}/chat/completions`, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ 'Authorization': `Bearer ${profile.apiKey}`
45
+ },
46
+ body: JSON.stringify({
47
+ model: this.data.activeModel || profile.selectedModels[0].id,
48
+ ...payload
49
+ })
50
+ });
51
+
52
+ if (!response.ok) {
53
+ const errData = await response.json().catch(() => ({}));
54
+ throw new Error(errData.error?.message || `请求失败: ${response.status}`);
55
+ }
56
+
57
+ return payload.stream ? this._handleStream(response) : await response.json();
58
+ }
59
+
60
+ // --- 流式处理生成器 ---
61
+ async* _handleStream(response) {
62
+ const reader = response.body.getReader();
63
+ const decoder = new TextDecoder();
64
+ while (true) {
65
+ const { done, value } = await reader.read();
66
+ if (done) break;
67
+ const lines = decoder.decode(value).split('\n');
68
+ for (const line of lines) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
71
+ if (trimmed.includes('[DONE]')) break;
72
+ try {
73
+ const data = JSON.parse(trimmed.slice(6));
74
+ const content = data.choices[0]?.delta?.content;
75
+ if (content) yield content;
76
+ } catch (e) {}
77
+ }
78
+ }
79
+ }
80
+
81
+ // --- 连通性校验 ---
82
+ async _testConnectivity(profile, model) {
83
+ try {
84
+ const resp = await fetch(`${profile.baseUrl}/chat/completions`, {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ 'Authorization': `Bearer ${profile.apiKey}`
89
+ },
90
+ body: JSON.stringify({
91
+ model,
92
+ messages: [{ role: 'user', content: 'hi' }],
93
+ max_tokens: 5
94
+ })
95
+ });
96
+ return resp.ok;
97
+ } catch { return false; }
98
+ }
99
+
100
+ // --- 内部逻辑与持久化 ---
101
+ _load() {
102
+ const raw = localStorage.getItem(STORAGE_KEY);
103
+ return raw ? JSON.parse(raw) : { profiles: [], activeProfileId: null, activeModel: null };
104
+ }
105
+
106
+ _save() {
107
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
108
+ this._refreshExternalSelect();
109
+ }
110
+
111
+ _getActiveProfile() {
112
+ return this.data.profiles.find(p => p.id === this.data.activeProfileId);
113
+ }
114
+
115
+ // --- UI 构建 ---
116
+ _createModal() {
117
+ this.modal = document.createElement('div');
118
+ this.modal.className = 'vaic-overlay';
119
+ const presetsHTML = Object.keys(this.presets)
120
+ .map(name => `<span class="vaic-badge" data-url="${this.presets[name]}">${name}</span>`)
121
+ .join('');
122
+
123
+ this.modal.innerHTML = `
124
+ <div class="vaic-card">
125
+ <div class="vaic-header">
126
+ <h3>AI 资产管理</h3>
127
+ <button class="vaic-close-x" id="vaic-close">✕</button>
128
+ </div>
129
+ <div class="vaic-body">
130
+ <div id="vaic-profile-list" class="vaic-list"></div>
131
+ <hr>
132
+ <div class="vaic-form">
133
+ <div class="vaic-presets">${presetsHTML}</div>
134
+ <input type="text" id="vaic-name" placeholder="供应商名称 (如: DeepSeek)">
135
+ <input type="text" id="vaic-base" placeholder="Base URL (包含 /v1)">
136
+ <p class="vaic-tip">提示:通常需以 /v1 结尾(如:https://api.deepseek.com/v1)</p>
137
+ <input type="password" id="vaic-key" placeholder="API Key">
138
+ <div class="vaic-model-picker">
139
+ <select id="vaic-all-models" multiple title="按住 Ctrl/Cmd 可多选"></select>
140
+ <button id="vaic-fetch-btn">获取模型</button>
141
+ </div>
142
+ <p class="vaic-tip">* 必须先点击“获取模型”,然后按住 Ctrl/Cmd 多选</p>
143
+ </div>
144
+ </div>
145
+ <div class="vaic-footer">
146
+ <button class="vaic-btn vaic-btn-primary" id="vaic-save">保存并使用</button>
147
+ </div>
148
+ </div>
149
+ `;
150
+ document.body.appendChild(this.modal);
151
+
152
+ // 绑定事件
153
+ this.modal.querySelectorAll('.vaic-badge').forEach(el => {
154
+ el.onclick = () => {
155
+ this.modal.querySelector('#vaic-base').value = el.dataset.url;
156
+ this.modal.querySelector('#vaic-name').value = el.innerText;
157
+ };
158
+ });
159
+ this.modal.querySelector('#vaic-close').onclick = () => this.closeSettings();
160
+ this.modal.querySelector('#vaic-fetch-btn').onclick = () => this._handleFetchModels();
161
+ this.modal.querySelector('#vaic-save').onclick = () => this._handleSave();
162
+ }
163
+
164
+ async _handleFetchModels() {
165
+ const btn = this.modal.querySelector('#vaic-fetch-btn');
166
+ const url = this.modal.querySelector('#vaic-base').value;
167
+ const key = this.modal.querySelector('#vaic-key').value;
168
+ if (!url || !key) return alert('请先填写 URL 和 Key');
169
+ btn.innerText = '加载中...';
170
+ try {
171
+ const resp = await fetch(`${url}/models`, { headers: { 'Authorization': `Bearer ${key}` } });
172
+ const json = await resp.json();
173
+ const models = json.data.map(m => m.id).sort((a, b) => a.localeCompare(b));
174
+ this.modal.querySelector('#vaic-all-models').innerHTML = models.map(m => `<option value="${m}">${m}</option>`).join('');
175
+ } catch (e) { alert('获取失败,请检查 Base URL 是否支持跨域或 URL 格式'); }
176
+ btn.innerText = '获取模型';
177
+ }
178
+
179
+ async _handleSave() {
180
+ const name = this.modal.querySelector('#vaic-name').value;
181
+ const baseUrl = this.modal.querySelector('#vaic-base').value;
182
+ const apiKey = this.modal.querySelector('#vaic-key').value;
183
+ const selectedOptions = Array.from(this.modal.querySelector('#vaic-all-models').selectedOptions);
184
+
185
+ if (!name || !apiKey || selectedOptions.length === 0) return alert('请完整填写并选择模型');
186
+
187
+ const modelPool = selectedOptions.map(opt => ({ id: opt.value, status: 'testing' }));
188
+ const profile = { id: Date.now().toString(), name, baseUrl, apiKey, selectedModels: modelPool };
189
+
190
+ this.data.profiles.push(profile);
191
+ this.data.activeProfileId = profile.id;
192
+ this.data.activeModel = modelPool[0].id;
193
+ this._save();
194
+ this._renderProfiles();
195
+ this.closeSettings();
196
+
197
+ // 异步校验所有选中的模型
198
+ for (let mObj of modelPool) {
199
+ this._testConnectivity(profile, mObj.id).then(ok => {
200
+ mObj.status = ok ? 'pass' : 'fail';
201
+ this._save();
202
+ });
203
+ }
204
+ }
205
+
206
+ _switchProfile(id) {
207
+ this.data.activeProfileId = id;
208
+ const profile = this._getActiveProfile();
209
+ if (profile && profile.selectedModels.length > 0) {
210
+ this.data.activeModel = profile.selectedModels[0].id;
211
+ }
212
+ this._save();
213
+ this._renderProfiles();
214
+ }
215
+
216
+ _deleteProfile(id) {
217
+ this.data.profiles = this.data.profiles.filter(p => p.id !== id);
218
+ if (this.data.activeProfileId === id) this.data.activeProfileId = null;
219
+ this._save();
220
+ this._renderProfiles();
221
+ }
222
+
223
+ _refreshExternalSelect() {
224
+ if (!this.modelSelectElement) return;
225
+ const profile = this._getActiveProfile();
226
+ if (!profile) {
227
+ this.modelSelectElement.innerHTML = '<option>未配置</option>';
228
+ return;
229
+ }
230
+ this.modelSelectElement.innerHTML = profile.selectedModels.map(m =>
231
+ `<option value="${m.id}" ${m.id === this.data.activeModel ? 'selected' : ''}>
232
+ ${m.status === 'pass' ? '✅' : m.status === 'fail' ? '❌' : '⏳'} ${m.id}
233
+ </option>`
234
+ ).join('');
235
+ }
236
+
237
+ _renderProfiles() {
238
+ const list = this.modal.querySelector('#vaic-profile-list');
239
+ list.innerHTML = this.data.profiles.map(p => `
240
+ <div class="vaic-item ${p.id === this.data.activeProfileId ? 'active' : ''}">
241
+ <span onclick="vibeAI._switchProfile('${p.id}')" style="cursor:pointer; flex:1;">
242
+ <b>${p.name}</b> <small>(${p.selectedModels.length} models)</small>
243
+ </span>
244
+ <button onclick="vibeAI._deleteProfile('${p.id}')">删除</button>
245
+ </div>
246
+ `).join('');
247
+ }
248
+
249
+ openSettings() { this.modal.style.display = 'flex'; this._renderProfiles(); }
250
+ closeSettings() { this.modal.style.display = 'none'; }
251
+
252
+ _injectStyles() {
253
+ const style = document.createElement('style');
254
+ style.textContent = `
255
+ .vaic-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); backdrop-filter: blur(10px); display: none; align-items: center; justify-content: center; z-index: 10000; font-family: -apple-system, system-ui, sans-serif; }
256
+ .vaic-card { background: #fff; width: 420px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); overflow: hidden; display: flex; flex-direction: column; }
257
+ .vaic-header { padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f0f0; }
258
+ .vaic-body { padding: 20px; overflow-y: auto; max-height: 70vh; }
259
+ .vaic-presets { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 15px; }
260
+ .vaic-badge { padding: 4px 10px; background: #f0f0f0; border-radius: 6px; font-size: 11px; cursor: pointer; border: 1px solid #eee; }
261
+ .vaic-badge:hover { background: #000; color: #fff; }
262
+ .vaic-form input { width: 100%; padding: 10px; margin-bottom: 8px; border: 1px solid #ddd; border-radius: 8px; box-sizing: border-box; font-size: 13px; }
263
+ .vaic-tip { font-size: 10px; color: #999; margin: -4px 0 12px 2px; }
264
+ .vaic-model-picker { display: flex; gap: 8px; }
265
+ .vaic-model-picker select { flex: 1; height: 100px; border-radius: 8px; border: 1px solid #ddd; padding: 5px; font-size: 12px; }
266
+ .vaic-model-picker button { width: 80px; border-radius: 8px; border: 1px solid #ddd; cursor: pointer; font-size: 11px; background: #f9f9f9; }
267
+ .vaic-item { display: flex; justify-content: space-between; padding: 10px; background: #f8f8f8; border-radius: 10px; margin-bottom: 8px; font-size: 13px; border: 1px solid transparent; }
268
+ .vaic-item.active { border-color: #000; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
269
+ .vaic-btn { width: 100%; padding: 14px; border-radius: 12px; border: none; cursor: pointer; font-weight: bold; background: #000; color: #fff; }
270
+ .vaic-item button { background: none; border: none; color: #ff4444; cursor: pointer; font-size: 11px; }
271
+ .vaic-close-x { background: none; border: none; font-size: 20px; cursor: pointer; color: #ccc; }
272
+ hr { border: 0; border-top: 1px solid #eee; margin: 15px 0; }
273
+ `;
274
+ document.head.appendChild(style);
275
+ }
276
+ }
277
+
278
+ export const vibeAI = new VibeAI();
279
+ window.vibeAI = vibeAI;