@uniqueli/openwork 0.1.0 → 0.2.1
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.
- package/README.md +67 -15
- package/out/main/index.js +146 -59
- package/out/renderer/assets/{index-D4BcB5ZM.js → index-BPV5Z3ZG.js} +227 -17
- package/out/renderer/index.html +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
A desktop interface for [deepagentsjs](https://github.com/langchain-ai/deepagentsjs) — an opinionated harness for building deep agents with filesystem capabilities, planning, and subagent delegation.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**✨ Enhanced with Multiple Custom API Support** - Add unlimited OpenAI-compatible API providers with a single click!
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
@@ -31,14 +31,6 @@ Requires Node.js 18+.
|
|
|
31
31
|
|
|
32
32
|
### From Source
|
|
33
33
|
|
|
34
|
-
```bash
|
|
35
|
-
git clone https://github.com/uniqueli/openwork.git
|
|
36
|
-
cd openwork
|
|
37
|
-
npm install
|
|
38
|
-
npm run dev
|
|
39
|
-
```
|
|
40
|
-
Or configure them in-app via the settings panel.
|
|
41
|
-
|
|
42
34
|
## Supported Models
|
|
43
35
|
|
|
44
36
|
| Provider | Models |
|
|
@@ -46,15 +38,57 @@ Or configure them in-app via the settings panel.
|
|
|
46
38
|
| Anthropic | Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5, Claude Opus 4.1, Claude Sonnet 4 |
|
|
47
39
|
| OpenAI | GPT-5.2, GPT-5.1, o3, o3 Mini, o4 Mini, o1, GPT-4.1, GPT-4o |
|
|
48
40
|
| Google | Gemini 3 Pro Preview, Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash Lite |
|
|
49
|
-
| Custom |
|
|
41
|
+
| **Custom** | **Add unlimited custom providers!** |
|
|
42
|
+
|
|
43
|
+
## ✨ Multiple Custom API Providers
|
|
44
|
+
|
|
45
|
+
**New in v0.2.0**: Add multiple custom OpenAI-compatible API providers directly from the UI!
|
|
46
|
+
|
|
47
|
+
### How to Add Custom Providers
|
|
48
|
+
|
|
49
|
+
1. Click the model selector in the chat interface
|
|
50
|
+
2. Click the **"+ 添加Provider"** button at the bottom of the provider list
|
|
51
|
+
3. Fill in the form:
|
|
52
|
+
- **ID**: Unique identifier (e.g., `moonshot`, `zhipu`, `deepseek`)
|
|
53
|
+
- **Display Name**: Name shown in UI (e.g., `Moonshot AI`, `Zhipu AI`)
|
|
54
|
+
- **Base URL**: API endpoint (e.g., `https://api.moonshot.cn/v1`)
|
|
55
|
+
- **API Key**: Your API key
|
|
56
|
+
- **Model Name**: Model identifier (e.g., `kimi-k2-turbo-preview`)
|
|
57
|
+
4. Click **Save** - your new provider appears immediately!
|
|
58
|
+
|
|
59
|
+
### Supported Custom APIs
|
|
60
|
+
|
|
61
|
+
Works with any OpenAI-compatible API:
|
|
62
|
+
- **Chinese AI Providers**: Moonshot AI (Kimi), Zhipu AI (GLM), DeepSeek, Baichuan, etc.
|
|
63
|
+
- **Self-hosted models**: vLLM, Text Generation WebUI, LocalAI, Ollama (with OpenAI compatibility)
|
|
64
|
+
- **Cloud services**: Azure OpenAI, AWS Bedrock (with proxy), Cloudflare AI
|
|
65
|
+
- **Other providers**: Together AI, Anyscale, Fireworks AI, etc.
|
|
66
|
+
|
|
67
|
+
### Example Configurations
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
**Moonshot AI (Kimi)**
|
|
70
|
+
```
|
|
71
|
+
ID: moonshot
|
|
72
|
+
Display Name: Moonshot AI
|
|
73
|
+
Base URL: https://api.moonshot.cn/v1
|
|
74
|
+
Model Name: kimi-k2-turbo-preview
|
|
75
|
+
```
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
**Zhipu AI (GLM)**
|
|
78
|
+
```
|
|
79
|
+
ID: zhipu
|
|
80
|
+
Display Name: Zhipu AI
|
|
81
|
+
Base URL: https://open.bigmodel.cn/api/paas/v4
|
|
82
|
+
Model Name: glm-4-plus
|
|
83
|
+
```
|
|
57
84
|
|
|
85
|
+
**DeepSeek**
|
|
86
|
+
```
|
|
87
|
+
ID: deepseek
|
|
88
|
+
Display Name: DeepSeek
|
|
89
|
+
Base URL: https://api.deepseek.com/v1
|
|
90
|
+
Model Name: deepseek-chat
|
|
91
|
+
```
|
|
58
92
|
Configure via Settings UI or by setting environment variables:
|
|
59
93
|
```bash
|
|
60
94
|
CUSTOM_BASE_URL=https://api.example.com/v1
|
|
@@ -64,6 +98,24 @@ CUSTOM_MODEL=your-model-name # optional
|
|
|
64
98
|
|
|
65
99
|
See [CUSTOM_API.md](CUSTOM_API.md) for detailed instructions.
|
|
66
100
|
|
|
101
|
+
## Changelog
|
|
102
|
+
|
|
103
|
+
### v0.2.1 (2026-01-19)
|
|
104
|
+
- 🐛 **Critical Fix**: Fixed "Missing credentials" error for users without OpenAI API key
|
|
105
|
+
- 🔧 Custom API now works correctly even when OPENAI_API_KEY is not set in environment
|
|
106
|
+
- 📝 Improved logging for debugging custom API configurations
|
|
107
|
+
|
|
108
|
+
### v0.2.0 (2026-01-18)
|
|
109
|
+
- ✨ **Multiple Custom API Providers**: Add unlimited custom providers via UI
|
|
110
|
+
- 🎨 **Improved UX**: One-click provider addition with "+ 添加Provider" button
|
|
111
|
+
- 🔧 **Better Configuration**: Each provider has its own name, base URL, API key, and model
|
|
112
|
+
- 🌐 **Chinese AI Support**: Perfect for Moonshot AI, Zhipu AI, DeepSeek, and other providers
|
|
113
|
+
- 📝 **Simplified Settings**: Cleaner settings dialog focused on standard providers
|
|
114
|
+
|
|
115
|
+
### v0.1.0 (2026-01-15)
|
|
116
|
+
- 🎉 Initial release with basic custom API support
|
|
117
|
+
- 🔑 Single custom API configuration via Settings
|
|
118
|
+
|
|
67
119
|
## Contributing
|
|
68
120
|
|
|
69
121
|
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
package/out/main/index.js
CHANGED
|
@@ -185,42 +185,84 @@ function deleteApiKey(provider) {
|
|
|
185
185
|
function hasApiKey(provider) {
|
|
186
186
|
return !!getApiKey(provider);
|
|
187
187
|
}
|
|
188
|
-
function
|
|
188
|
+
function getCustomApiConfigs() {
|
|
189
189
|
const env = parseEnvFile();
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
const configs = [];
|
|
191
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
192
|
+
for (const key of Object.keys(env)) {
|
|
193
|
+
const match = key.match(/^CUSTOM_API_(.+)_BASE_URL$/);
|
|
194
|
+
if (match) {
|
|
195
|
+
const id = match[1].toLowerCase();
|
|
196
|
+
if (processedIds.has(id)) continue;
|
|
197
|
+
processedIds.add(id);
|
|
198
|
+
const baseUrl = env[`CUSTOM_API_${match[1]}_BASE_URL`]?.trim();
|
|
199
|
+
const apiKey = env[`CUSTOM_API_${match[1]}_API_KEY`]?.trim();
|
|
200
|
+
const name = env[`CUSTOM_API_${match[1]}_NAME`]?.trim();
|
|
201
|
+
const model = env[`CUSTOM_API_${match[1]}_MODEL`]?.trim();
|
|
202
|
+
if (baseUrl && apiKey) {
|
|
203
|
+
configs.push({
|
|
204
|
+
id,
|
|
205
|
+
name: name || id,
|
|
206
|
+
baseUrl,
|
|
207
|
+
apiKey,
|
|
208
|
+
model
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (env.CUSTOM_BASE_URL && env.CUSTOM_API_KEY && !processedIds.has("custom")) {
|
|
214
|
+
configs.push({
|
|
215
|
+
id: "custom",
|
|
216
|
+
name: env.CUSTOM_NAME?.trim() || "Custom API",
|
|
217
|
+
baseUrl: env.CUSTOM_BASE_URL.trim(),
|
|
218
|
+
apiKey: env.CUSTOM_API_KEY.trim(),
|
|
219
|
+
model: env.CUSTOM_MODEL?.trim()
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return configs;
|
|
195
223
|
}
|
|
196
224
|
function setCustomApiConfig(config) {
|
|
197
225
|
const env = parseEnvFile();
|
|
198
|
-
|
|
199
|
-
env
|
|
226
|
+
const idUpper = config.id.toUpperCase();
|
|
227
|
+
env[`CUSTOM_API_${idUpper}_BASE_URL`] = config.baseUrl;
|
|
228
|
+
env[`CUSTOM_API_${idUpper}_API_KEY`] = config.apiKey;
|
|
229
|
+
env[`CUSTOM_API_${idUpper}_NAME`] = config.name;
|
|
200
230
|
if (config.model) {
|
|
201
|
-
env
|
|
231
|
+
env[`CUSTOM_API_${idUpper}_MODEL`] = config.model;
|
|
202
232
|
} else {
|
|
203
|
-
delete env
|
|
233
|
+
delete env[`CUSTOM_API_${idUpper}_MODEL`];
|
|
204
234
|
}
|
|
205
235
|
writeEnvFile(env);
|
|
206
|
-
process.env
|
|
207
|
-
process.env
|
|
236
|
+
process.env[`CUSTOM_API_${idUpper}_BASE_URL`] = config.baseUrl;
|
|
237
|
+
process.env[`CUSTOM_API_${idUpper}_API_KEY`] = config.apiKey;
|
|
238
|
+
process.env[`CUSTOM_API_${idUpper}_NAME`] = config.name;
|
|
208
239
|
if (config.model) {
|
|
209
|
-
process.env
|
|
240
|
+
process.env[`CUSTOM_API_${idUpper}_MODEL`] = config.model;
|
|
210
241
|
}
|
|
211
242
|
}
|
|
212
|
-
function deleteCustomApiConfig() {
|
|
243
|
+
function deleteCustomApiConfig(id) {
|
|
213
244
|
const env = parseEnvFile();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
245
|
+
if (!id) {
|
|
246
|
+
delete env.CUSTOM_BASE_URL;
|
|
247
|
+
delete env.CUSTOM_API_KEY;
|
|
248
|
+
delete env.CUSTOM_NAME;
|
|
249
|
+
delete env.CUSTOM_MODEL;
|
|
250
|
+
delete process.env.CUSTOM_BASE_URL;
|
|
251
|
+
delete process.env.CUSTOM_API_KEY;
|
|
252
|
+
delete process.env.CUSTOM_NAME;
|
|
253
|
+
delete process.env.CUSTOM_MODEL;
|
|
254
|
+
} else {
|
|
255
|
+
const idUpper = id.toUpperCase();
|
|
256
|
+
delete env[`CUSTOM_API_${idUpper}_BASE_URL`];
|
|
257
|
+
delete env[`CUSTOM_API_${idUpper}_API_KEY`];
|
|
258
|
+
delete env[`CUSTOM_API_${idUpper}_NAME`];
|
|
259
|
+
delete env[`CUSTOM_API_${idUpper}_MODEL`];
|
|
260
|
+
delete process.env[`CUSTOM_API_${idUpper}_BASE_URL`];
|
|
261
|
+
delete process.env[`CUSTOM_API_${idUpper}_API_KEY`];
|
|
262
|
+
delete process.env[`CUSTOM_API_${idUpper}_NAME`];
|
|
263
|
+
delete process.env[`CUSTOM_API_${idUpper}_MODEL`];
|
|
264
|
+
}
|
|
217
265
|
writeEnvFile(env);
|
|
218
|
-
delete process.env.CUSTOM_BASE_URL;
|
|
219
|
-
delete process.env.CUSTOM_API_KEY;
|
|
220
|
-
delete process.env.CUSTOM_MODEL;
|
|
221
|
-
}
|
|
222
|
-
function hasCustomApiConfig() {
|
|
223
|
-
return !!getCustomApiConfig();
|
|
224
266
|
}
|
|
225
267
|
const store = new Store({
|
|
226
268
|
name: "settings",
|
|
@@ -411,10 +453,28 @@ const AVAILABLE_MODELS = [
|
|
|
411
453
|
];
|
|
412
454
|
function registerModelHandlers(ipcMain) {
|
|
413
455
|
ipcMain.handle("models:list", async () => {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
456
|
+
const customConfigs = getCustomApiConfigs();
|
|
457
|
+
const models = AVAILABLE_MODELS.filter((m) => m.id !== "custom");
|
|
458
|
+
for (const config of customConfigs) {
|
|
459
|
+
const modelId = config.model || `custom-${config.id}`;
|
|
460
|
+
models.push({
|
|
461
|
+
id: modelId,
|
|
462
|
+
name: config.model || config.name,
|
|
463
|
+
// Display the model name or config name
|
|
464
|
+
provider: config.id,
|
|
465
|
+
// Use config ID as provider ID (dynamic)
|
|
466
|
+
model: modelId,
|
|
467
|
+
description: `${config.name} - ${config.baseUrl}`,
|
|
468
|
+
available: true
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return models.map((model) => {
|
|
472
|
+
const isCustom = customConfigs.some((c) => c.id === model.provider);
|
|
473
|
+
return {
|
|
474
|
+
...model,
|
|
475
|
+
available: isCustom ? true : hasApiKey(model.provider)
|
|
476
|
+
};
|
|
477
|
+
});
|
|
418
478
|
});
|
|
419
479
|
ipcMain.handle("models:getDefault", async () => {
|
|
420
480
|
return store.get("defaultModel", "claude-sonnet-4-5-20250929");
|
|
@@ -435,19 +495,35 @@ function registerModelHandlers(ipcMain) {
|
|
|
435
495
|
deleteApiKey(provider);
|
|
436
496
|
});
|
|
437
497
|
ipcMain.handle("models:listProviders", async () => {
|
|
438
|
-
|
|
498
|
+
const standardProviders = PROVIDERS.filter((p) => p.id !== "custom").map((provider) => ({
|
|
439
499
|
...provider,
|
|
440
|
-
hasApiKey:
|
|
500
|
+
hasApiKey: hasApiKey(provider.id)
|
|
441
501
|
}));
|
|
502
|
+
const customConfigs = getCustomApiConfigs();
|
|
503
|
+
const customProviders = customConfigs.map((config) => ({
|
|
504
|
+
id: config.id,
|
|
505
|
+
// Dynamic provider ID
|
|
506
|
+
name: config.name,
|
|
507
|
+
hasApiKey: true
|
|
508
|
+
// Custom configs always have their API key
|
|
509
|
+
}));
|
|
510
|
+
return [...standardProviders, ...customProviders];
|
|
442
511
|
});
|
|
443
|
-
ipcMain.handle("models:getCustomApiConfig", async () => {
|
|
444
|
-
|
|
512
|
+
ipcMain.handle("models:getCustomApiConfig", async (_event, id) => {
|
|
513
|
+
const configs = getCustomApiConfigs();
|
|
514
|
+
if (!id) {
|
|
515
|
+
return configs[0] ?? null;
|
|
516
|
+
}
|
|
517
|
+
return configs.find((c) => c.id === id) ?? null;
|
|
518
|
+
});
|
|
519
|
+
ipcMain.handle("models:getCustomApiConfigs", async () => {
|
|
520
|
+
return getCustomApiConfigs();
|
|
445
521
|
});
|
|
446
522
|
ipcMain.handle("models:setCustomApiConfig", async (_event, config) => {
|
|
447
523
|
setCustomApiConfig(config);
|
|
448
524
|
});
|
|
449
|
-
ipcMain.handle("models:deleteCustomApiConfig", async () => {
|
|
450
|
-
deleteCustomApiConfig();
|
|
525
|
+
ipcMain.handle("models:deleteCustomApiConfig", async (_event, id) => {
|
|
526
|
+
deleteCustomApiConfig(id);
|
|
451
527
|
});
|
|
452
528
|
ipcMain.on("app:version", (event) => {
|
|
453
529
|
event.returnValue = electron.app.getVersion();
|
|
@@ -1264,34 +1340,45 @@ async function closeCheckpointer(threadId) {
|
|
|
1264
1340
|
function getModelInstance(modelId) {
|
|
1265
1341
|
const model = modelId || getDefaultModel();
|
|
1266
1342
|
console.log("[Runtime] Using model:", model);
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1343
|
+
const customConfigs = getCustomApiConfigs();
|
|
1344
|
+
const matchingConfig = customConfigs.find((c) => {
|
|
1345
|
+
return c.model === model || `custom-${c.id}` === model;
|
|
1346
|
+
});
|
|
1347
|
+
if (matchingConfig) {
|
|
1348
|
+
console.log("[Runtime] Found custom API config:", matchingConfig.name);
|
|
1349
|
+
const cleanApiKey = matchingConfig.apiKey?.trim();
|
|
1273
1350
|
console.log("[Runtime] Custom API config:", {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1351
|
+
id: matchingConfig.id,
|
|
1352
|
+
name: matchingConfig.name,
|
|
1353
|
+
baseUrl: matchingConfig.baseUrl,
|
|
1354
|
+
model: matchingConfig.model,
|
|
1355
|
+
apiKeyLength: matchingConfig.apiKey?.length,
|
|
1356
|
+
cleanApiKeyLength: cleanApiKey?.length,
|
|
1357
|
+
apiKeyPrefix: cleanApiKey?.substring(0, 10)
|
|
1281
1358
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1359
|
+
if (cleanApiKey) {
|
|
1360
|
+
process.env.OPENAI_API_KEY = cleanApiKey;
|
|
1361
|
+
console.log("[Runtime] Set OPENAI_API_KEY environment variable for deepagents compatibility");
|
|
1362
|
+
}
|
|
1363
|
+
try {
|
|
1364
|
+
const chatModel = new openai.ChatOpenAI({
|
|
1365
|
+
model: matchingConfig.model || model,
|
|
1366
|
+
openAIApiKey: cleanApiKey,
|
|
1367
|
+
configuration: {
|
|
1368
|
+
baseURL: matchingConfig.baseUrl,
|
|
1369
|
+
defaultHeaders: {
|
|
1370
|
+
"Authorization": `Bearer ${cleanApiKey}`
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
timeout: 6e4,
|
|
1374
|
+
maxRetries: 2
|
|
1375
|
+
});
|
|
1376
|
+
console.log("[Runtime] ChatOpenAI instance created for custom API:", matchingConfig.name);
|
|
1377
|
+
return chatModel;
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
console.error("[Runtime] Error creating ChatOpenAI instance:", error);
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1295
1382
|
}
|
|
1296
1383
|
if (model.startsWith("claude")) {
|
|
1297
1384
|
const apiKey = getApiKey("anthropic");
|
|
@@ -74879,6 +74879,8 @@ function ApiKeyDialog({ open, onOpenChange, provider }) {
|
|
|
74879
74879
|
try {
|
|
74880
74880
|
if (provider.id === "custom") {
|
|
74881
74881
|
await window.api.models.setCustomApiConfig({
|
|
74882
|
+
id: "custom",
|
|
74883
|
+
name: "Custom API",
|
|
74882
74884
|
baseUrl: baseUrl.trim(),
|
|
74883
74885
|
apiKey: apiKey.trim(),
|
|
74884
74886
|
model: modelName.trim() || void 0
|
|
@@ -75009,6 +75011,176 @@ function ApiKeyDialog({ open, onOpenChange, provider }) {
|
|
|
75009
75011
|
] })
|
|
75010
75012
|
] }) });
|
|
75011
75013
|
}
|
|
75014
|
+
function AddProviderDialog({ open, onOpenChange, onSuccess }) {
|
|
75015
|
+
const [id, setId] = reactExports.useState("");
|
|
75016
|
+
const [name2, setName] = reactExports.useState("");
|
|
75017
|
+
const [baseUrl, setBaseUrl] = reactExports.useState("");
|
|
75018
|
+
const [apiKey, setApiKey] = reactExports.useState("");
|
|
75019
|
+
const [model, setModel] = reactExports.useState("");
|
|
75020
|
+
const [showApiKey, setShowApiKey] = reactExports.useState(false);
|
|
75021
|
+
const [saving, setSaving] = reactExports.useState(false);
|
|
75022
|
+
const [error, setError] = reactExports.useState("");
|
|
75023
|
+
async function handleSave() {
|
|
75024
|
+
if (!id || !name2 || !baseUrl || !apiKey) {
|
|
75025
|
+
setError("请填写所有必填字段");
|
|
75026
|
+
return;
|
|
75027
|
+
}
|
|
75028
|
+
if (!/^[a-z0-9-]+$/.test(id)) {
|
|
75029
|
+
setError("ID只能包含小写字母、数字和连字符");
|
|
75030
|
+
return;
|
|
75031
|
+
}
|
|
75032
|
+
setSaving(true);
|
|
75033
|
+
setError("");
|
|
75034
|
+
try {
|
|
75035
|
+
await window.api.models.setCustomApiConfig({
|
|
75036
|
+
id: id.toLowerCase(),
|
|
75037
|
+
name: name2,
|
|
75038
|
+
baseUrl,
|
|
75039
|
+
apiKey,
|
|
75040
|
+
model: model || void 0
|
|
75041
|
+
});
|
|
75042
|
+
setId("");
|
|
75043
|
+
setName("");
|
|
75044
|
+
setBaseUrl("");
|
|
75045
|
+
setApiKey("");
|
|
75046
|
+
setModel("");
|
|
75047
|
+
onOpenChange(false);
|
|
75048
|
+
onSuccess?.();
|
|
75049
|
+
} catch (e) {
|
|
75050
|
+
console.error("Failed to save custom API config:", e);
|
|
75051
|
+
setError("保存失败,请重试");
|
|
75052
|
+
} finally {
|
|
75053
|
+
setSaving(false);
|
|
75054
|
+
}
|
|
75055
|
+
}
|
|
75056
|
+
function handleCancel() {
|
|
75057
|
+
setId("");
|
|
75058
|
+
setName("");
|
|
75059
|
+
setBaseUrl("");
|
|
75060
|
+
setApiKey("");
|
|
75061
|
+
setModel("");
|
|
75062
|
+
setError("");
|
|
75063
|
+
onOpenChange(false);
|
|
75064
|
+
}
|
|
75065
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { className: "sm:max-w-[500px]", children: [
|
|
75066
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(DialogHeader, { children: [
|
|
75067
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "添加自定义Provider" }),
|
|
75068
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(DialogDescription, { children: "配置一个OpenAI兼容的API端点" })
|
|
75069
|
+
] }),
|
|
75070
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4 py-4", children: [
|
|
75071
|
+
error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-destructive bg-destructive/10 p-3 rounded-md", children: error }),
|
|
75072
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
|
|
75073
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-sm font-medium", children: [
|
|
75074
|
+
"ID ",
|
|
75075
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
|
|
75076
|
+
] }),
|
|
75077
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75078
|
+
Input,
|
|
75079
|
+
{
|
|
75080
|
+
type: "text",
|
|
75081
|
+
value: id,
|
|
75082
|
+
onChange: (e) => setId(e.target.value.toLowerCase()),
|
|
75083
|
+
placeholder: "moonshot, zhipu, deepseek"
|
|
75084
|
+
}
|
|
75085
|
+
),
|
|
75086
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "唯一标识符,只能使用小写字母、数字和连字符" })
|
|
75087
|
+
] }),
|
|
75088
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
|
|
75089
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-sm font-medium", children: [
|
|
75090
|
+
"显示名称 ",
|
|
75091
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
|
|
75092
|
+
] }),
|
|
75093
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75094
|
+
Input,
|
|
75095
|
+
{
|
|
75096
|
+
type: "text",
|
|
75097
|
+
value: name2,
|
|
75098
|
+
onChange: (e) => setName(e.target.value),
|
|
75099
|
+
placeholder: "Moonshot AI, Zhipu AI"
|
|
75100
|
+
}
|
|
75101
|
+
),
|
|
75102
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "这个名字会显示在Provider列表中" })
|
|
75103
|
+
] }),
|
|
75104
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
|
|
75105
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-sm font-medium", children: [
|
|
75106
|
+
"Base URL ",
|
|
75107
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
|
|
75108
|
+
] }),
|
|
75109
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75110
|
+
Input,
|
|
75111
|
+
{
|
|
75112
|
+
type: "text",
|
|
75113
|
+
value: baseUrl,
|
|
75114
|
+
onChange: (e) => setBaseUrl(e.target.value),
|
|
75115
|
+
placeholder: "https://api.moonshot.cn/v1"
|
|
75116
|
+
}
|
|
75117
|
+
)
|
|
75118
|
+
] }),
|
|
75119
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
|
|
75120
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-sm font-medium", children: [
|
|
75121
|
+
"API Key ",
|
|
75122
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
|
|
75123
|
+
] }),
|
|
75124
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [
|
|
75125
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75126
|
+
Input,
|
|
75127
|
+
{
|
|
75128
|
+
type: showApiKey ? "text" : "password",
|
|
75129
|
+
value: apiKey,
|
|
75130
|
+
onChange: (e) => setApiKey(e.target.value),
|
|
75131
|
+
placeholder: "sk-...",
|
|
75132
|
+
className: "pr-10"
|
|
75133
|
+
}
|
|
75134
|
+
),
|
|
75135
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75136
|
+
"button",
|
|
75137
|
+
{
|
|
75138
|
+
type: "button",
|
|
75139
|
+
onClick: () => setShowApiKey(!showApiKey),
|
|
75140
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors",
|
|
75141
|
+
children: showApiKey ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "size-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "size-4" })
|
|
75142
|
+
}
|
|
75143
|
+
)
|
|
75144
|
+
] })
|
|
75145
|
+
] }),
|
|
75146
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
|
|
75147
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm font-medium", children: "模型名称" }),
|
|
75148
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75149
|
+
Input,
|
|
75150
|
+
{
|
|
75151
|
+
type: "text",
|
|
75152
|
+
value: model,
|
|
75153
|
+
onChange: (e) => setModel(e.target.value),
|
|
75154
|
+
placeholder: "kimi-k2-turbo-preview, glm-4-plus"
|
|
75155
|
+
}
|
|
75156
|
+
),
|
|
75157
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "这个名字会直接显示在Model列表中(可选)" })
|
|
75158
|
+
] })
|
|
75159
|
+
] }),
|
|
75160
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
75161
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75162
|
+
Button,
|
|
75163
|
+
{
|
|
75164
|
+
variant: "outline",
|
|
75165
|
+
onClick: handleCancel,
|
|
75166
|
+
disabled: saving,
|
|
75167
|
+
children: "取消"
|
|
75168
|
+
}
|
|
75169
|
+
),
|
|
75170
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75171
|
+
Button,
|
|
75172
|
+
{
|
|
75173
|
+
onClick: handleSave,
|
|
75174
|
+
disabled: saving || !id || !name2 || !baseUrl || !apiKey,
|
|
75175
|
+
children: saving ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
75176
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "size-4 animate-spin mr-2" }),
|
|
75177
|
+
"保存中..."
|
|
75178
|
+
] }) : "保存"
|
|
75179
|
+
}
|
|
75180
|
+
)
|
|
75181
|
+
] })
|
|
75182
|
+
] }) });
|
|
75183
|
+
}
|
|
75012
75184
|
function AnthropicIcon({ className }) {
|
|
75013
75185
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className, viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M17.304 3.541h-3.672l6.696 16.918h3.672l-6.696-16.918zm-10.608 0L0 20.459h3.744l1.368-3.562h7.044l1.368 3.562h3.744L10.608 3.541H6.696zm.576 10.852l2.352-6.122 2.352 6.122H7.272z" }) });
|
|
75014
75186
|
}
|
|
@@ -75033,6 +75205,9 @@ const PROVIDER_ICONS = {
|
|
|
75033
75205
|
// No icon for ollama yet
|
|
75034
75206
|
custom: CustomIcon
|
|
75035
75207
|
};
|
|
75208
|
+
function getProviderIcon(providerId) {
|
|
75209
|
+
return PROVIDER_ICONS[providerId] || CustomIcon;
|
|
75210
|
+
}
|
|
75036
75211
|
const FALLBACK_PROVIDERS = [
|
|
75037
75212
|
{ id: "anthropic", name: "Anthropic", hasApiKey: false },
|
|
75038
75213
|
{ id: "openai", name: "OpenAI", hasApiKey: false },
|
|
@@ -75044,6 +75219,7 @@ function ModelSwitcher({ threadId }) {
|
|
|
75044
75219
|
const [selectedProviderId, setSelectedProviderId] = reactExports.useState(null);
|
|
75045
75220
|
const [apiKeyDialogOpen, setApiKeyDialogOpen] = reactExports.useState(false);
|
|
75046
75221
|
const [apiKeyProvider, setApiKeyProvider] = reactExports.useState(null);
|
|
75222
|
+
const [addProviderDialogOpen, setAddProviderDialogOpen] = reactExports.useState(false);
|
|
75047
75223
|
const { models, providers, loadModels, loadProviders } = useAppStore();
|
|
75048
75224
|
const { currentModel, setCurrentModel } = useCurrentThread(threadId);
|
|
75049
75225
|
reactExports.useEffect(() => {
|
|
@@ -75083,6 +75259,10 @@ function ModelSwitcher({ threadId }) {
|
|
|
75083
75259
|
loadModels();
|
|
75084
75260
|
}
|
|
75085
75261
|
}
|
|
75262
|
+
function handleAddProviderSuccess() {
|
|
75263
|
+
loadProviders();
|
|
75264
|
+
loadModels();
|
|
75265
|
+
}
|
|
75086
75266
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
75087
75267
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
75088
75268
|
/* @__PURE__ */ jsxRuntimeExports.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
@@ -75093,7 +75273,10 @@ function ModelSwitcher({ threadId }) {
|
|
|
75093
75273
|
className: "h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground",
|
|
75094
75274
|
children: [
|
|
75095
75275
|
selectedModel ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
75096
|
-
|
|
75276
|
+
(() => {
|
|
75277
|
+
const Icon2 = getProviderIcon(selectedModel.provider);
|
|
75278
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: "size-3.5" });
|
|
75279
|
+
})(),
|
|
75097
75280
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono", children: selectedModel.id })
|
|
75098
75281
|
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Select model" }),
|
|
75099
75282
|
/* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "size-3" })
|
|
@@ -75109,25 +75292,44 @@ function ModelSwitcher({ threadId }) {
|
|
|
75109
75292
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex min-h-[240px]", children: [
|
|
75110
75293
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-[140px] border-r border-border p-2 bg-muted/30", children: [
|
|
75111
75294
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wider px-2 py-1.5", children: "Provider" }),
|
|
75112
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
75113
|
-
|
|
75114
|
-
|
|
75295
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-0.5", children: [
|
|
75296
|
+
displayProviders.map((provider) => {
|
|
75297
|
+
const Icon2 = getProviderIcon(provider.id);
|
|
75298
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
75299
|
+
"button",
|
|
75300
|
+
{
|
|
75301
|
+
onClick: () => handleProviderClick(provider),
|
|
75302
|
+
className: cn(
|
|
75303
|
+
"w-full flex items-center gap-1.5 px-2 py-1 rounded-sm text-xs transition-colors text-left",
|
|
75304
|
+
selectedProviderId === provider.id ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
|
75305
|
+
),
|
|
75306
|
+
children: [
|
|
75307
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: "size-3.5 shrink-0" }),
|
|
75308
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 truncate", children: provider.name }),
|
|
75309
|
+
!provider.hasApiKey && /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "size-3 text-status-warning shrink-0" })
|
|
75310
|
+
]
|
|
75311
|
+
},
|
|
75312
|
+
provider.id
|
|
75313
|
+
);
|
|
75314
|
+
}),
|
|
75315
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
75115
75316
|
"button",
|
|
75116
75317
|
{
|
|
75117
|
-
onClick: () =>
|
|
75118
|
-
|
|
75119
|
-
|
|
75120
|
-
|
|
75121
|
-
|
|
75318
|
+
onClick: () => {
|
|
75319
|
+
setOpen(false);
|
|
75320
|
+
setAddProviderDialogOpen(true);
|
|
75321
|
+
},
|
|
75322
|
+
className: "w-full flex items-center justify-center gap-1.5 px-2 py-2 rounded-sm text-xs transition-colors text-muted-foreground hover:text-foreground hover:bg-muted/50 border-t border-border mt-1 pt-2",
|
|
75122
75323
|
children: [
|
|
75123
|
-
|
|
75124
|
-
|
|
75125
|
-
|
|
75324
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { className: "size-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
75325
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
|
|
75326
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
|
|
75327
|
+
] }),
|
|
75328
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "添加Provider" })
|
|
75126
75329
|
]
|
|
75127
|
-
}
|
|
75128
|
-
|
|
75129
|
-
|
|
75130
|
-
}) })
|
|
75330
|
+
}
|
|
75331
|
+
)
|
|
75332
|
+
] })
|
|
75131
75333
|
] }),
|
|
75132
75334
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 p-2", children: [
|
|
75133
75335
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wider px-2 py-1.5", children: "Model" }),
|
|
@@ -75194,6 +75396,14 @@ function ModelSwitcher({ threadId }) {
|
|
|
75194
75396
|
onOpenChange: handleApiKeyDialogClose,
|
|
75195
75397
|
provider: apiKeyProvider
|
|
75196
75398
|
}
|
|
75399
|
+
),
|
|
75400
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
75401
|
+
AddProviderDialog,
|
|
75402
|
+
{
|
|
75403
|
+
open: addProviderDialogOpen,
|
|
75404
|
+
onOpenChange: setAddProviderDialogOpen,
|
|
75405
|
+
onSuccess: handleAddProviderSuccess
|
|
75406
|
+
}
|
|
75197
75407
|
)
|
|
75198
75408
|
] });
|
|
75199
75409
|
}
|
|
@@ -76661,7 +76871,7 @@ function App() {
|
|
|
76661
76871
|
},
|
|
76662
76872
|
children: [
|
|
76663
76873
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "app-badge-name", children: "OPENWORK" }),
|
|
76664
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "app-badge-version", children: "0.1
|
|
76874
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "app-badge-version", children: "0.2.1" })
|
|
76665
76875
|
]
|
|
76666
76876
|
}
|
|
76667
76877
|
),
|
package/out/renderer/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
http-equiv="Content-Security-Policy"
|
|
8
8
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
|
9
9
|
/>
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-BPV5Z3ZG.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="./assets/index-BtAM3QNQ.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqueli/openwork",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "A tactical agent interface for deepagentsjs with custom API support",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "A tactical agent interface for deepagentsjs with multiple custom API support",
|
|
5
5
|
"main": "./out/main/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"out/",
|