@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 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
- **Enhanced with Custom API Support** - Configure any OpenAI-compatible API endpoint!
12
+ **✨ Enhanced with Multiple Custom API Support** - Add unlimited OpenAI-compatible API providers with a single click!
13
13
 
14
14
  ![openwork screenshot](docs/screenshot.png)
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 | Any OpenAI-compatible API endpoint |
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
- ### Custom API Configuration
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
- You can now configure custom OpenAI-compatible API endpoints. This allows you to use:
54
- - Self-hosted models (vLLM, Text Generation WebUI, etc.)
55
- - Azure OpenAI
56
- - Other OpenAI-compatible services
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 getCustomApiConfig() {
188
+ function getCustomApiConfigs() {
189
189
  const env = parseEnvFile();
190
- const baseUrl = env.CUSTOM_BASE_URL;
191
- const apiKey = env.CUSTOM_API_KEY;
192
- const model = env.CUSTOM_MODEL;
193
- if (!baseUrl || !apiKey) return void 0;
194
- return { baseUrl, apiKey, model };
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
- env.CUSTOM_BASE_URL = config.baseUrl;
199
- env.CUSTOM_API_KEY = config.apiKey;
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.CUSTOM_MODEL = config.model;
231
+ env[`CUSTOM_API_${idUpper}_MODEL`] = config.model;
202
232
  } else {
203
- delete env.CUSTOM_MODEL;
233
+ delete env[`CUSTOM_API_${idUpper}_MODEL`];
204
234
  }
205
235
  writeEnvFile(env);
206
- process.env.CUSTOM_BASE_URL = config.baseUrl;
207
- process.env.CUSTOM_API_KEY = config.apiKey;
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.CUSTOM_MODEL = config.model;
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
- delete env.CUSTOM_BASE_URL;
215
- delete env.CUSTOM_API_KEY;
216
- delete env.CUSTOM_MODEL;
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
- return AVAILABLE_MODELS.map((model) => ({
415
- ...model,
416
- available: model.provider === "custom" ? hasCustomApiConfig() : hasApiKey(model.provider)
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
- return PROVIDERS.map((provider) => ({
498
+ const standardProviders = PROVIDERS.filter((p) => p.id !== "custom").map((provider) => ({
439
499
  ...provider,
440
- hasApiKey: provider.id === "custom" ? hasCustomApiConfig() : hasApiKey(provider.id)
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
- return getCustomApiConfig() ?? null;
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
- if (model === "custom" || model.startsWith("custom-")) {
1268
- const customConfig = getCustomApiConfig();
1269
- console.log("[Runtime] Custom API config present:", !!customConfig);
1270
- if (!customConfig) {
1271
- throw new Error("Custom API configuration not set");
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
- baseUrl: customConfig.baseUrl,
1275
- model: customConfig.model,
1276
- apiKeyLength: customConfig.apiKey?.length,
1277
- apiKeyPrefix: customConfig.apiKey?.substring(0, 10),
1278
- apiKeySuffix: customConfig.apiKey?.substring(customConfig.apiKey.length - 10),
1279
- apiKeyHasNewline: customConfig.apiKey?.includes("\n"),
1280
- apiKeyHasSpace: customConfig.apiKey?.includes(" ")
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
- const chatModel = new openai.ChatOpenAI({
1283
- model: customConfig.model || model,
1284
- apiKey: customConfig.apiKey,
1285
- // 尝试使用 apiKey 而不是 openAIApiKey
1286
- configuration: {
1287
- baseURL: customConfig.baseUrl
1288
- },
1289
- timeout: 6e4,
1290
- // 60 seconds timeout
1291
- maxRetries: 2
1292
- });
1293
- console.log("[Runtime] ChatOpenAI instance created");
1294
- return chatModel;
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
- PROVIDER_ICONS[selectedModel.provider]?.({ className: "size-3.5" }),
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.jsx("div", { className: "space-y-0.5", children: displayProviders.map((provider) => {
75113
- const Icon2 = PROVIDER_ICONS[provider.id];
75114
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
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: () => handleProviderClick(provider),
75118
- className: cn(
75119
- "w-full flex items-center gap-1.5 px-2 py-1 rounded-sm text-xs transition-colors text-left",
75120
- selectedProviderId === provider.id ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-muted/50"
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
- Icon2 && /* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: "size-3.5 shrink-0" }),
75124
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 truncate", children: provider.name }),
75125
- !provider.hasApiKey && /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "size-3 text-status-warning shrink-0" })
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
- provider.id
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.0" })
76874
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "app-badge-version", children: "0.2.1" })
76665
76875
  ]
76666
76876
  }
76667
76877
  ),
@@ -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-D4BcB5ZM.js"></script>
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.0",
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/",