@cremini/skillpack 1.1.9 → 1.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/dist/cli.js +508 -180
- package/dist/runtime/registry.js +244 -0
- package/package.json +2 -2
- package/web/index.html +29 -62
- package/web/js/api-key-dialog.js +189 -36
- package/web/js/chat-apps-dialog.js +4 -10
- package/web/js/main.js +0 -2
- package/web/styles.css +65 -27
- package/web/js/settings.js +0 -205
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/runtime/registry.ts
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
var SKILLPACK_HOME = path.join(os.homedir(), ".skillpack");
|
|
9
|
+
var LEGACY_REGISTRY_FILE = path.join(SKILLPACK_HOME, "registry.json");
|
|
10
|
+
var REGISTRY_DIR = path.join(SKILLPACK_HOME, "registry.d");
|
|
11
|
+
var migrationChecked = false;
|
|
12
|
+
function getRegistryPath() {
|
|
13
|
+
ensureRegistryReady();
|
|
14
|
+
return REGISTRY_DIR;
|
|
15
|
+
}
|
|
16
|
+
function ensureHomeDir() {
|
|
17
|
+
if (!fs.existsSync(SKILLPACK_HOME)) {
|
|
18
|
+
fs.mkdirSync(SKILLPACK_HOME, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function ensureRegistryDir() {
|
|
22
|
+
ensureHomeDir();
|
|
23
|
+
if (!fs.existsSync(REGISTRY_DIR)) {
|
|
24
|
+
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function canonicalizeDir(dir) {
|
|
28
|
+
const resolved = path.resolve(dir);
|
|
29
|
+
try {
|
|
30
|
+
return fs.realpathSync(resolved);
|
|
31
|
+
} catch {
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function hashDir(dir) {
|
|
36
|
+
return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
function getEntryPathForCanonicalDir(dir) {
|
|
39
|
+
return path.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
|
|
40
|
+
}
|
|
41
|
+
function getEntryPath(dir) {
|
|
42
|
+
ensureRegistryReady();
|
|
43
|
+
return getEntryPathForCanonicalDir(canonicalizeDir(dir));
|
|
44
|
+
}
|
|
45
|
+
function listEntryFiles() {
|
|
46
|
+
ensureRegistryReady();
|
|
47
|
+
return fs.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path.join(REGISTRY_DIR, file));
|
|
48
|
+
}
|
|
49
|
+
function readEntryFile(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
52
|
+
const data = JSON.parse(raw);
|
|
53
|
+
if (typeof data?.dir !== "string" || typeof data?.name !== "string" || typeof data?.version !== "string" || typeof data?.port !== "number" || typeof data?.pid !== "number" && data?.pid !== null || data?.status !== "running" && data?.status !== "stopped") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
dir: canonicalizeDir(data.dir),
|
|
58
|
+
name: data.name,
|
|
59
|
+
version: data.version,
|
|
60
|
+
port: data.port,
|
|
61
|
+
pid: data.pid,
|
|
62
|
+
status: data.status,
|
|
63
|
+
startedAt: data.startedAt,
|
|
64
|
+
stoppedAt: data.stoppedAt,
|
|
65
|
+
updatedAt: data.updatedAt
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function createTmpPath(entryPath) {
|
|
72
|
+
const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
73
|
+
return `${entryPath}.tmp.${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
function writeEntryFile(entry) {
|
|
76
|
+
ensureRegistryReady();
|
|
77
|
+
const normalized = {
|
|
78
|
+
...entry,
|
|
79
|
+
dir: canonicalizeDir(entry.dir),
|
|
80
|
+
updatedAt: entry.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
81
|
+
};
|
|
82
|
+
const entryPath = getEntryPathForCanonicalDir(normalized.dir);
|
|
83
|
+
const tmpPath = createTmpPath(entryPath);
|
|
84
|
+
fs.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
|
|
85
|
+
fs.renameSync(tmpPath, entryPath);
|
|
86
|
+
}
|
|
87
|
+
function migrateLegacyRegistryIfNeeded() {
|
|
88
|
+
if (migrationChecked) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
migrationChecked = true;
|
|
92
|
+
ensureRegistryDir();
|
|
93
|
+
if (!fs.existsSync(LEGACY_REGISTRY_FILE)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (listEntryFiles().length > 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const raw = fs.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
|
|
101
|
+
const data = JSON.parse(raw);
|
|
102
|
+
const packs = Array.isArray(data?.packs) ? data.packs : [];
|
|
103
|
+
for (const pack of packs) {
|
|
104
|
+
try {
|
|
105
|
+
writeEntryFile({
|
|
106
|
+
...pack,
|
|
107
|
+
dir: canonicalizeDir(pack.dir),
|
|
108
|
+
updatedAt: pack.updatedAt ?? pack.stoppedAt ?? pack.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
fs.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function ensureRegistryReady() {
|
|
119
|
+
ensureRegistryDir();
|
|
120
|
+
migrateLegacyRegistryIfNeeded();
|
|
121
|
+
}
|
|
122
|
+
function entriesEqual(a, b) {
|
|
123
|
+
return a.dir === b.dir && a.name === b.name && a.version === b.version && a.port === b.port && a.pid === b.pid && a.status === b.status && a.startedAt === b.startedAt && a.stoppedAt === b.stoppedAt;
|
|
124
|
+
}
|
|
125
|
+
function readEntry(dir) {
|
|
126
|
+
ensureRegistryReady();
|
|
127
|
+
return readEntryFile(getEntryPath(dir));
|
|
128
|
+
}
|
|
129
|
+
function writeEntry(entry) {
|
|
130
|
+
writeEntryFile(entry);
|
|
131
|
+
}
|
|
132
|
+
function deleteEntry(dir) {
|
|
133
|
+
ensureRegistryReady();
|
|
134
|
+
const entryPath = getEntryPath(dir);
|
|
135
|
+
if (fs.existsSync(entryPath)) {
|
|
136
|
+
fs.unlinkSync(entryPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function readRegistry() {
|
|
140
|
+
return { packs: readAll() };
|
|
141
|
+
}
|
|
142
|
+
function writeRegistry(data) {
|
|
143
|
+
ensureRegistryReady();
|
|
144
|
+
const nextPaths = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const pack of data.packs) {
|
|
146
|
+
const normalized = {
|
|
147
|
+
...pack,
|
|
148
|
+
dir: canonicalizeDir(pack.dir),
|
|
149
|
+
updatedAt: pack.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
150
|
+
};
|
|
151
|
+
const entryPath = getEntryPathForCanonicalDir(normalized.dir);
|
|
152
|
+
nextPaths.add(entryPath);
|
|
153
|
+
writeEntryFile(normalized);
|
|
154
|
+
}
|
|
155
|
+
for (const existingPath of listEntryFiles()) {
|
|
156
|
+
if (!nextPaths.has(existingPath)) {
|
|
157
|
+
fs.unlinkSync(existingPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function register(opts) {
|
|
162
|
+
try {
|
|
163
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
164
|
+
const entry = {
|
|
165
|
+
dir: canonicalizeDir(opts.dir),
|
|
166
|
+
name: opts.name,
|
|
167
|
+
version: opts.version,
|
|
168
|
+
port: opts.port,
|
|
169
|
+
pid: process.pid,
|
|
170
|
+
status: "running",
|
|
171
|
+
startedAt: now,
|
|
172
|
+
updatedAt: now
|
|
173
|
+
};
|
|
174
|
+
writeEntryFile(entry);
|
|
175
|
+
console.log(` [Registry] Registered "${opts.name}" (pid ${process.pid})`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.warn(" [Registry] Failed to register:", err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function deregister(dir, pid) {
|
|
181
|
+
try {
|
|
182
|
+
const entry = readEntry(dir);
|
|
183
|
+
if (!entry || entry.pid !== pid) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
187
|
+
writeEntryFile({
|
|
188
|
+
...entry,
|
|
189
|
+
pid: null,
|
|
190
|
+
status: "stopped",
|
|
191
|
+
stoppedAt: now,
|
|
192
|
+
updatedAt: now
|
|
193
|
+
});
|
|
194
|
+
console.log(` [Registry] Deregistered "${entry.name}"`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn(" [Registry] Failed to deregister:", err);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function readAll() {
|
|
200
|
+
return listEntryFiles().map((entryPath) => readEntryFile(entryPath)).filter((entry) => entry !== null);
|
|
201
|
+
}
|
|
202
|
+
function isPidAlive(pid) {
|
|
203
|
+
try {
|
|
204
|
+
process.kill(pid, 0);
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function validateEntries() {
|
|
211
|
+
const entries = readAll();
|
|
212
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (entry.status === "running" && entry.pid !== null && !isPidAlive(entry.pid)) {
|
|
215
|
+
writeEntryFile({
|
|
216
|
+
...entry,
|
|
217
|
+
pid: null,
|
|
218
|
+
status: "stopped",
|
|
219
|
+
stoppedAt: now,
|
|
220
|
+
updatedAt: now
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const nextEntries = readAll();
|
|
225
|
+
if (entries.length === nextEntries.length && entries.every((entry, index) => entriesEqual(entry, nextEntries[index]))) {
|
|
226
|
+
return entries;
|
|
227
|
+
}
|
|
228
|
+
return nextEntries;
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
canonicalizeDir,
|
|
232
|
+
deleteEntry,
|
|
233
|
+
deregister,
|
|
234
|
+
getEntryPath,
|
|
235
|
+
getRegistryPath,
|
|
236
|
+
isPidAlive,
|
|
237
|
+
readAll,
|
|
238
|
+
readEntry,
|
|
239
|
+
readRegistry,
|
|
240
|
+
register,
|
|
241
|
+
validateEntries,
|
|
242
|
+
writeEntry,
|
|
243
|
+
writeRegistry
|
|
244
|
+
};
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -39,12 +39,6 @@
|
|
|
39
39
|
<ul id="skills-list"></ul>
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
|
-
<div class="sidebar-settings-section">
|
|
43
|
-
<button id="open-settings-btn" class="settings-trigger-btn">
|
|
44
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="btn-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
|
45
|
-
Settings
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
42
|
</aside>
|
|
49
43
|
|
|
50
44
|
<main id="chat-area" class="mode-welcome">
|
|
@@ -71,59 +65,6 @@
|
|
|
71
65
|
</main>
|
|
72
66
|
</div>
|
|
73
67
|
|
|
74
|
-
<!-- Settings Dialog -->
|
|
75
|
-
<dialog id="settings-dialog" class="settings-modal">
|
|
76
|
-
<div class="settings-modal-header">
|
|
77
|
-
<h2>Settings</h2>
|
|
78
|
-
<button id="close-settings-btn" class="close-btn" aria-label="Close">
|
|
79
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
80
|
-
</button>
|
|
81
|
-
</div>
|
|
82
|
-
<div class="settings-modal-body">
|
|
83
|
-
<div class="settings-sections">
|
|
84
|
-
<!-- General Section -->
|
|
85
|
-
<div class="settings-section">
|
|
86
|
-
<h3 class="section-title">General</h3>
|
|
87
|
-
<div class="form-group">
|
|
88
|
-
<label>Provider</label>
|
|
89
|
-
<div class="provider-select-wrapper">
|
|
90
|
-
<select id="provider-select">
|
|
91
|
-
<option value="openai">OpenAI</option>
|
|
92
|
-
<option value="anthropic">Anthropic</option>
|
|
93
|
-
</select>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
<div class="form-group">
|
|
97
|
-
<label>API Key</label>
|
|
98
|
-
<input type="password" id="api-key-input" placeholder="sk-..." class="form-input" />
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<!-- IM Bots Section -->
|
|
103
|
-
<div class="settings-section">
|
|
104
|
-
<h3 class="section-title">IM Bots</h3>
|
|
105
|
-
<div class="form-group">
|
|
106
|
-
<label>Telegram Bot Token</label>
|
|
107
|
-
<input type="password" id="telegram-token-input" placeholder="123456:ABC-DEF..." class="form-input" />
|
|
108
|
-
</div>
|
|
109
|
-
<div class="form-group">
|
|
110
|
-
<label>Slack Bot Token</label>
|
|
111
|
-
<input type="password" id="slack-bot-token-input" placeholder="xoxb-..." class="form-input" />
|
|
112
|
-
</div>
|
|
113
|
-
<div class="form-group">
|
|
114
|
-
<label>Slack App Token</label>
|
|
115
|
-
<input type="password" id="slack-app-token-input" placeholder="xapp-..." class="form-input" />
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
<div class="settings-modal-footer">
|
|
121
|
-
<p id="key-status" class="status-text"></p>
|
|
122
|
-
<button id="restart-service-btn" class="secondary-btn" hidden>Restart Service</button>
|
|
123
|
-
<button id="save-settings-btn" class="primary-btn">Save Settings</button>
|
|
124
|
-
</div>
|
|
125
|
-
</dialog>
|
|
126
|
-
|
|
127
68
|
<!-- API Key Dialog -->
|
|
128
69
|
<dialog id="apikey-dialog" class="settings-modal compact-modal">
|
|
129
70
|
<div class="settings-modal-header">
|
|
@@ -140,12 +81,38 @@
|
|
|
140
81
|
<select id="apikey-provider-select">
|
|
141
82
|
<option value="openai">OpenAI</option>
|
|
142
83
|
<option value="anthropic">Anthropic</option>
|
|
84
|
+
<option value="google">Google (Gemini)</option>
|
|
85
|
+
<option value="openai-codex">OpenAI Codex</option>
|
|
143
86
|
</select>
|
|
144
87
|
</div>
|
|
145
88
|
</div>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
89
|
+
<!-- API Key Section -->
|
|
90
|
+
<div id="apikey-apikey-section">
|
|
91
|
+
<div class="form-group">
|
|
92
|
+
<label>API Key</label>
|
|
93
|
+
<input type="password" id="apikey-input" placeholder="sk-..." class="form-input" />
|
|
94
|
+
</div>
|
|
95
|
+
<div class="form-group" id="apikey-baseurl-group">
|
|
96
|
+
<label>Custom Base URL <span class="label-hint">(optional)</span></label>
|
|
97
|
+
<input type="text" id="apikey-baseurl-input" placeholder="https://api.example.com/v1" class="form-input" />
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- OAuth Section -->
|
|
102
|
+
<div id="apikey-oauth-section" style="display: none;">
|
|
103
|
+
<div class="oauth-status-card">
|
|
104
|
+
<div id="oauth-status-indicator" class="oauth-disconnected">
|
|
105
|
+
<span class="oauth-status-dot"></span>
|
|
106
|
+
<span id="oauth-status-text">Not Connected</span>
|
|
107
|
+
</div>
|
|
108
|
+
<p class="oauth-hint">Login with your ChatGPT account to use Codex subscription.</p>
|
|
109
|
+
</div>
|
|
110
|
+
<button id="oauth-login-btn" class="primary-btn oauth-btn">
|
|
111
|
+
Login with ChatGPT
|
|
112
|
+
</button>
|
|
113
|
+
<button id="oauth-logout-btn" class="secondary-btn oauth-btn" style="display: none;">
|
|
114
|
+
Logout
|
|
115
|
+
</button>
|
|
149
116
|
</div>
|
|
150
117
|
</div>
|
|
151
118
|
</div>
|
package/web/js/api-key-dialog.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Key Dialog Module
|
|
3
3
|
*
|
|
4
|
-
* 负责 Model API Key
|
|
5
|
-
* 独立的 Dialog,从原 SettingDialog 的 API Key 部分拆分出来。
|
|
4
|
+
* 负责 Model 认证配置管理。支持 API Key 模式和 OAuth 模式切换。
|
|
6
5
|
*/
|
|
7
6
|
import { state } from "./config.js";
|
|
8
7
|
import { saveConfigData, restartRuntime } from "./api.js";
|
|
@@ -15,6 +14,14 @@ let saveBtn;
|
|
|
15
14
|
let restartBtn;
|
|
16
15
|
let providerSelect;
|
|
17
16
|
let apiKeyInput;
|
|
17
|
+
let baseUrlInput;
|
|
18
|
+
let baseUrlGroup;
|
|
19
|
+
let apikeySection;
|
|
20
|
+
let oauthSection;
|
|
21
|
+
let oauthLoginBtn;
|
|
22
|
+
let oauthLogoutBtn;
|
|
23
|
+
let oauthStatusIndicator;
|
|
24
|
+
let oauthStatusText;
|
|
18
25
|
let statusEl;
|
|
19
26
|
|
|
20
27
|
// --- Public API ---
|
|
@@ -27,6 +34,14 @@ export function initApiKeyDialog() {
|
|
|
27
34
|
restartBtn = document.getElementById("restart-apikey-btn");
|
|
28
35
|
providerSelect = document.getElementById("apikey-provider-select");
|
|
29
36
|
apiKeyInput = document.getElementById("apikey-input");
|
|
37
|
+
baseUrlInput = document.getElementById("apikey-baseurl-input");
|
|
38
|
+
baseUrlGroup = document.getElementById("apikey-baseurl-group");
|
|
39
|
+
apikeySection = document.getElementById("apikey-apikey-section");
|
|
40
|
+
oauthSection = document.getElementById("apikey-oauth-section");
|
|
41
|
+
oauthLoginBtn = document.getElementById("oauth-login-btn");
|
|
42
|
+
oauthLogoutBtn = document.getElementById("oauth-logout-btn");
|
|
43
|
+
oauthStatusIndicator = document.getElementById("oauth-status-indicator");
|
|
44
|
+
oauthStatusText = document.getElementById("oauth-status-text");
|
|
30
45
|
statusEl = document.getElementById("apikey-status");
|
|
31
46
|
|
|
32
47
|
if (!dialog) return;
|
|
@@ -44,7 +59,13 @@ export function initApiKeyDialog() {
|
|
|
44
59
|
restartBtn.addEventListener("click", handleRestart);
|
|
45
60
|
}
|
|
46
61
|
if (providerSelect) {
|
|
47
|
-
providerSelect.addEventListener("change",
|
|
62
|
+
providerSelect.addEventListener("change", updateProviderUI);
|
|
63
|
+
}
|
|
64
|
+
if (oauthLoginBtn) {
|
|
65
|
+
oauthLoginBtn.addEventListener("click", handleOAuthLogin);
|
|
66
|
+
}
|
|
67
|
+
if (oauthLogoutBtn) {
|
|
68
|
+
oauthLogoutBtn.addEventListener("click", handleOAuthLogout);
|
|
48
69
|
}
|
|
49
70
|
}
|
|
50
71
|
|
|
@@ -54,12 +75,19 @@ export function initApiKeyDialog() {
|
|
|
54
75
|
export function updateApiKeyButton() {
|
|
55
76
|
if (!openBtn) return;
|
|
56
77
|
const config = state.config;
|
|
57
|
-
|
|
78
|
+
const currentProvider = config?.provider || "openai";
|
|
79
|
+
const meta = config?.supportedProviders?.[currentProvider];
|
|
80
|
+
|
|
81
|
+
const connected = meta?.authType === "oauth"
|
|
82
|
+
? config?.oauthConnected
|
|
83
|
+
: config?.hasApiKey;
|
|
84
|
+
|
|
85
|
+
if (connected) {
|
|
58
86
|
openBtn.classList.add("connected");
|
|
59
|
-
openBtn.querySelector(".action-btn-label").textContent = "
|
|
87
|
+
openBtn.querySelector(".action-btn-label").textContent = "Model Configured";
|
|
60
88
|
} else {
|
|
61
89
|
openBtn.classList.remove("connected");
|
|
62
|
-
openBtn.querySelector(".action-btn-label").textContent = "Provide Model
|
|
90
|
+
openBtn.querySelector(".action-btn-label").textContent = "Provide Model Auth";
|
|
63
91
|
}
|
|
64
92
|
}
|
|
65
93
|
|
|
@@ -83,10 +111,11 @@ function populateForm() {
|
|
|
83
111
|
if (config.provider && providerSelect) {
|
|
84
112
|
providerSelect.value = config.provider;
|
|
85
113
|
}
|
|
86
|
-
|
|
114
|
+
updateProviderUI();
|
|
87
115
|
|
|
88
116
|
setStatus("", "");
|
|
89
117
|
|
|
118
|
+
// API Key & BaseURL
|
|
90
119
|
if (config.hasApiKey && config.apiKey) {
|
|
91
120
|
apiKeyInput.value = config.apiKey;
|
|
92
121
|
} else if (config.hasApiKey) {
|
|
@@ -94,27 +123,64 @@ function populateForm() {
|
|
|
94
123
|
} else {
|
|
95
124
|
apiKeyInput.value = "";
|
|
96
125
|
}
|
|
126
|
+
if (baseUrlInput) {
|
|
127
|
+
baseUrlInput.value = config.baseUrl || "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// OAuth Status
|
|
131
|
+
if (config.oauthConnected) {
|
|
132
|
+
updateOAuthUI(true);
|
|
133
|
+
} else {
|
|
134
|
+
updateOAuthUI(false);
|
|
135
|
+
}
|
|
97
136
|
}
|
|
98
137
|
|
|
99
|
-
function
|
|
100
|
-
if (!providerSelect
|
|
138
|
+
function updateProviderUI() {
|
|
139
|
+
if (!providerSelect) return;
|
|
101
140
|
const p = providerSelect.value;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
const meta = state.config?.supportedProviders?.[p];
|
|
142
|
+
|
|
143
|
+
if (!meta) return;
|
|
144
|
+
|
|
145
|
+
if (meta.authType === "oauth") {
|
|
146
|
+
// Show OAuth section, hide API Key section
|
|
147
|
+
if (apikeySection) apikeySection.style.display = "none";
|
|
148
|
+
if (oauthSection) oauthSection.style.display = "";
|
|
149
|
+
if (saveBtn) saveBtn.style.display = "none";
|
|
150
|
+
checkOAuthStatus();
|
|
151
|
+
} else {
|
|
152
|
+
// Show API Key section, hide OAuth section
|
|
153
|
+
if (apikeySection) apikeySection.style.display = "";
|
|
154
|
+
if (oauthSection) oauthSection.style.display = "none";
|
|
155
|
+
if (saveBtn) saveBtn.style.display = "";
|
|
156
|
+
|
|
157
|
+
// Toggle BaseURL input
|
|
158
|
+
if (baseUrlGroup) {
|
|
159
|
+
baseUrlGroup.style.display = meta.supportsBaseUrl ? "" : "none";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Update placeholder
|
|
163
|
+
if (apiKeyInput) {
|
|
164
|
+
apiKeyInput.placeholder = meta.placeholder || "sk-...";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
105
167
|
}
|
|
106
168
|
|
|
107
169
|
async function handleSave() {
|
|
108
170
|
const key = apiKeyInput.value.trim();
|
|
109
171
|
const provider = providerSelect.value;
|
|
172
|
+
const baseUrl = baseUrlInput.value.trim();
|
|
110
173
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
174
|
+
// If OAuth is selected, we don't handle it here but it shouldn't happen as save button is hidden
|
|
175
|
+
const meta = state.config?.supportedProviders?.[provider];
|
|
176
|
+
if (meta?.authType === "oauth") return;
|
|
115
177
|
|
|
116
178
|
const updates = { provider };
|
|
117
|
-
if (
|
|
179
|
+
if (baseUrl !== state.config.baseUrl) {
|
|
180
|
+
updates.baseUrl = baseUrl;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (key && key !== "***************************************************" && key !== state.config.apiKey) {
|
|
118
184
|
updates.key = key;
|
|
119
185
|
}
|
|
120
186
|
|
|
@@ -123,35 +189,21 @@ async function handleSave() {
|
|
|
123
189
|
const res = await saveConfigData(updates);
|
|
124
190
|
|
|
125
191
|
state.config.provider = res.provider;
|
|
192
|
+
state.config.baseUrl = res.baseUrl || "";
|
|
126
193
|
if (updates.key) {
|
|
127
194
|
state.config.hasApiKey = true;
|
|
128
195
|
state.config.apiKey = updates.key;
|
|
129
196
|
}
|
|
130
197
|
|
|
131
|
-
|
|
132
|
-
apiKeyInput.value = state.config.apiKey;
|
|
133
|
-
} else if (state.config.hasApiKey) {
|
|
134
|
-
apiKeyInput.value = "***************************************************";
|
|
135
|
-
} else {
|
|
136
|
-
apiKeyInput.value = "";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
state.config.runtimeControl = res.runtimeControl;
|
|
198
|
+
populateForm();
|
|
140
199
|
state.restartRequired = !!res.requiresRestart;
|
|
141
|
-
|
|
142
200
|
updateApiKeyButton();
|
|
143
201
|
|
|
144
202
|
if (res.requiresRestart) {
|
|
145
|
-
setStatus(
|
|
146
|
-
|
|
147
|
-
? "API key saved. Restart service to apply changes."
|
|
148
|
-
: "API key saved. Restart the service manually to apply changes.",
|
|
149
|
-
"warning",
|
|
150
|
-
);
|
|
151
|
-
updateRestartButton(!!res.runtimeControl?.canManagedRestart);
|
|
203
|
+
setStatus("Settings saved. Restart service to apply changes.", "warning");
|
|
204
|
+
updateRestartButton(true);
|
|
152
205
|
} else {
|
|
153
|
-
setStatus("
|
|
154
|
-
// 延迟关闭让用户看到成功消息
|
|
206
|
+
setStatus("Settings saved successfully", "success");
|
|
155
207
|
setTimeout(() => close(), 1200);
|
|
156
208
|
}
|
|
157
209
|
} catch (err) {
|
|
@@ -161,6 +213,107 @@ async function handleSave() {
|
|
|
161
213
|
}
|
|
162
214
|
}
|
|
163
215
|
|
|
216
|
+
async function handleOAuthLogin() {
|
|
217
|
+
const provider = providerSelect.value;
|
|
218
|
+
oauthLoginBtn.disabled = true;
|
|
219
|
+
setStatus("Starting OAuth login...", "warning");
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Ensure provider is saved first
|
|
223
|
+
await saveConfigData({ provider });
|
|
224
|
+
|
|
225
|
+
const res = await fetch("/api/oauth/login", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/json" },
|
|
228
|
+
body: JSON.stringify({ provider }),
|
|
229
|
+
});
|
|
230
|
+
const data = await res.json();
|
|
231
|
+
|
|
232
|
+
if (data.authUrl) {
|
|
233
|
+
window.open(data.authUrl, "_blank");
|
|
234
|
+
setStatus("Waiting for authorization in browser...", "warning");
|
|
235
|
+
pollOAuthStatus();
|
|
236
|
+
} else {
|
|
237
|
+
setStatus("OAuth process started. Check terminal for URL if browser didn't open.", "warning");
|
|
238
|
+
pollOAuthStatus();
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
setStatus("Login failed: " + err.message, "error");
|
|
242
|
+
oauthLoginBtn.disabled = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function handleOAuthLogout() {
|
|
247
|
+
const provider = providerSelect.value;
|
|
248
|
+
try {
|
|
249
|
+
await fetch("/api/oauth/logout", {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "Content-Type": "application/json" },
|
|
252
|
+
body: JSON.stringify({ provider }),
|
|
253
|
+
});
|
|
254
|
+
updateOAuthUI(false);
|
|
255
|
+
state.config.oauthConnected = false;
|
|
256
|
+
updateApiKeyButton();
|
|
257
|
+
setStatus("Logged out successfully", "success");
|
|
258
|
+
} catch (err) {
|
|
259
|
+
setStatus("Logout failed: " + err.message, "error");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function checkOAuthStatus() {
|
|
264
|
+
try {
|
|
265
|
+
const res = await fetch("/api/oauth/status");
|
|
266
|
+
const { connected } = await res.json();
|
|
267
|
+
updateOAuthUI(connected);
|
|
268
|
+
state.config.oauthConnected = connected;
|
|
269
|
+
updateApiKeyButton();
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error("Failed to check OAuth status:", err);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function pollOAuthStatus() {
|
|
276
|
+
const interval = setInterval(async () => {
|
|
277
|
+
const res = await fetch("/api/oauth/status");
|
|
278
|
+
const { connected } = await res.json();
|
|
279
|
+
if (connected) {
|
|
280
|
+
clearInterval(interval);
|
|
281
|
+
updateOAuthUI(true);
|
|
282
|
+
state.config.oauthConnected = true;
|
|
283
|
+
state.restartRequired = true;
|
|
284
|
+
updateApiKeyButton();
|
|
285
|
+
setStatus("Connected successfully!", "success");
|
|
286
|
+
updateRestartButton(true);
|
|
287
|
+
}
|
|
288
|
+
}, 2000);
|
|
289
|
+
|
|
290
|
+
// Timeout after 5 minutes
|
|
291
|
+
setTimeout(() => clearInterval(interval), 300000);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function updateOAuthUI(connected) {
|
|
295
|
+
if (connected) {
|
|
296
|
+
if (oauthStatusIndicator) {
|
|
297
|
+
oauthStatusIndicator.classList.remove("oauth-disconnected");
|
|
298
|
+
oauthStatusIndicator.classList.add("oauth-connected");
|
|
299
|
+
}
|
|
300
|
+
if (oauthStatusText) oauthStatusText.textContent = "Connected";
|
|
301
|
+
if (oauthLoginBtn) oauthLoginBtn.style.display = "none";
|
|
302
|
+
if (oauthLogoutBtn) oauthLogoutBtn.style.display = "";
|
|
303
|
+
} else {
|
|
304
|
+
if (oauthStatusIndicator) {
|
|
305
|
+
oauthStatusIndicator.classList.add("oauth-disconnected");
|
|
306
|
+
oauthStatusIndicator.classList.remove("oauth-connected");
|
|
307
|
+
}
|
|
308
|
+
if (oauthStatusText) oauthStatusText.textContent = "Not Connected";
|
|
309
|
+
if (oauthLoginBtn) {
|
|
310
|
+
oauthLoginBtn.style.display = "";
|
|
311
|
+
oauthLoginBtn.disabled = false;
|
|
312
|
+
}
|
|
313
|
+
if (oauthLogoutBtn) oauthLogoutBtn.style.display = "none";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
164
317
|
async function handleRestart() {
|
|
165
318
|
if (!restartBtn) return;
|
|
166
319
|
|