@cremini/skillpack 1.2.0 → 1.2.2
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 +296 -131
- package/package.json +1 -1
- package/web/index.html +29 -62
- package/web/js/api-key-dialog.js +193 -33
- package/web/js/main.js +0 -2
- package/web/styles.css +65 -27
- package/web/js/settings.js +0 -200
package/dist/cli.js
CHANGED
|
@@ -9,89 +9,48 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/runtime/
|
|
12
|
+
// src/runtime/config.ts
|
|
13
13
|
import fs5 from "fs";
|
|
14
14
|
import path5 from "path";
|
|
15
|
-
|
|
16
|
-
import { Readable } from "stream";
|
|
17
|
-
function getAttachmentDir(rootDir, channelId) {
|
|
18
|
-
return path5.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
|
|
19
|
-
}
|
|
20
|
-
function sanitizeFilename(name) {
|
|
21
|
-
return name.replace(/[/\\:*?"<>|]/g, "_").replace(/\s+/g, "_");
|
|
22
|
-
}
|
|
23
|
-
async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mimeType, headers) {
|
|
24
|
-
const dir = getAttachmentDir(rootDir, channelId);
|
|
25
|
-
fs5.mkdirSync(dir, { recursive: true });
|
|
26
|
-
const ts = Date.now();
|
|
27
|
-
const safeName = sanitizeFilename(filename);
|
|
28
|
-
const storedName = `${ts}-${safeName}`;
|
|
29
|
-
const fullPath = path5.join(dir, storedName);
|
|
30
|
-
const response = await fetch(url, { headers });
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`Failed to download attachment from ${url}: ${response.status} ${response.statusText}`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
const body = response.body;
|
|
37
|
-
if (!body) {
|
|
38
|
-
throw new Error(`Empty response body when downloading ${url}`);
|
|
39
|
-
}
|
|
40
|
-
const nodeStream = Readable.fromWeb(body);
|
|
41
|
-
const writeStream = fs5.createWriteStream(fullPath);
|
|
42
|
-
await pipeline(nodeStream, writeStream);
|
|
43
|
-
const stats = fs5.statSync(fullPath);
|
|
44
|
-
const detectedMime = mimeType || response.headers.get("content-type")?.split(";")[0] || void 0;
|
|
45
|
-
return {
|
|
46
|
-
filename,
|
|
47
|
-
localPath: fullPath,
|
|
48
|
-
mimeType: detectedMime,
|
|
49
|
-
size: stats.size
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function isImageMime(mimeType) {
|
|
53
|
-
return !!mimeType && mimeType.startsWith("image/");
|
|
54
|
-
}
|
|
55
|
-
function formatSize(bytes) {
|
|
56
|
-
if (bytes === void 0 || bytes === null) return "";
|
|
57
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
58
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
59
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
60
|
-
}
|
|
61
|
-
function formatAttachmentsPrompt(attachments) {
|
|
62
|
-
if (attachments.length === 0) return "";
|
|
63
|
-
const lines = attachments.map((a) => {
|
|
64
|
-
const meta = [a.mimeType, formatSize(a.size)].filter(Boolean).join(", ");
|
|
65
|
-
return `- ${a.filename} (${meta}) \u2192 ${a.localPath}`;
|
|
66
|
-
});
|
|
67
|
-
return `[Attachments]
|
|
68
|
-
${lines.join("\n")}`;
|
|
69
|
-
}
|
|
70
|
-
function attachmentsToImageContent(attachments) {
|
|
71
|
-
return attachments.filter((a) => isImageMime(a.mimeType)).map((a) => {
|
|
72
|
-
const buffer = fs5.readFileSync(a.localPath);
|
|
73
|
-
return {
|
|
74
|
-
type: "image",
|
|
75
|
-
data: buffer.toString("base64"),
|
|
76
|
-
mimeType: a.mimeType || "image/png"
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
var ATTACHMENTS_DIR;
|
|
81
|
-
var init_attachment_utils = __esm({
|
|
82
|
-
"src/runtime/adapters/attachment-utils.ts"() {
|
|
83
|
-
"use strict";
|
|
84
|
-
ATTACHMENTS_DIR = "attachments";
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// src/runtime/config.ts
|
|
89
|
-
import fs8 from "fs";
|
|
90
|
-
import path8 from "path";
|
|
91
|
-
var ConfigManager, configManager;
|
|
15
|
+
var SUPPORTED_PROVIDERS, ConfigManager, configManager, ConfigFileAuthBackend;
|
|
92
16
|
var init_config = __esm({
|
|
93
17
|
"src/runtime/config.ts"() {
|
|
94
18
|
"use strict";
|
|
19
|
+
SUPPORTED_PROVIDERS = {
|
|
20
|
+
openai: {
|
|
21
|
+
label: "OpenAI",
|
|
22
|
+
defaultModelId: "gpt-5.4",
|
|
23
|
+
authType: "api_key",
|
|
24
|
+
envKey: "OPENAI_API_KEY",
|
|
25
|
+
placeholder: "sk-proj-...",
|
|
26
|
+
baseUrlPlaceholder: "https://api.openai.com/v1",
|
|
27
|
+
supportsBaseUrl: true
|
|
28
|
+
},
|
|
29
|
+
anthropic: {
|
|
30
|
+
label: "Anthropic",
|
|
31
|
+
defaultModelId: "claude-opus-4-6",
|
|
32
|
+
authType: "api_key",
|
|
33
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
34
|
+
placeholder: "sk-ant-api03-...",
|
|
35
|
+
baseUrlPlaceholder: "https://api.anthropic.com",
|
|
36
|
+
supportsBaseUrl: true
|
|
37
|
+
},
|
|
38
|
+
google: {
|
|
39
|
+
label: "Google (Gemini)",
|
|
40
|
+
defaultModelId: "gemini-2.5-pro",
|
|
41
|
+
authType: "api_key",
|
|
42
|
+
envKey: "GOOGLE_API_KEY",
|
|
43
|
+
placeholder: "AIza...",
|
|
44
|
+
supportsBaseUrl: false
|
|
45
|
+
},
|
|
46
|
+
"openai-codex": {
|
|
47
|
+
label: "OpenAI Codex",
|
|
48
|
+
defaultModelId: "gpt-5.4",
|
|
49
|
+
authType: "oauth",
|
|
50
|
+
oauthProviderId: "openai-codex",
|
|
51
|
+
supportsBaseUrl: false
|
|
52
|
+
}
|
|
53
|
+
};
|
|
95
54
|
ConfigManager = class _ConfigManager {
|
|
96
55
|
static instance;
|
|
97
56
|
configData = {};
|
|
@@ -105,10 +64,10 @@ var init_config = __esm({
|
|
|
105
64
|
return _ConfigManager.instance;
|
|
106
65
|
}
|
|
107
66
|
load(rootDir) {
|
|
108
|
-
this.configPath =
|
|
109
|
-
if (
|
|
67
|
+
this.configPath = path5.join(rootDir, "data", "config.json");
|
|
68
|
+
if (fs5.existsSync(this.configPath)) {
|
|
110
69
|
try {
|
|
111
|
-
this.configData = JSON.parse(
|
|
70
|
+
this.configData = JSON.parse(fs5.readFileSync(this.configPath, "utf-8"));
|
|
112
71
|
console.log(" Loaded config from data/config.json");
|
|
113
72
|
} catch (err) {
|
|
114
73
|
console.warn(" Warning: Failed to parse data/config.json:", err);
|
|
@@ -122,6 +81,9 @@ var init_config = __esm({
|
|
|
122
81
|
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
123
82
|
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
124
83
|
provider = "anthropic";
|
|
84
|
+
} else if (process.env.GOOGLE_API_KEY) {
|
|
85
|
+
apiKey = process.env.GOOGLE_API_KEY;
|
|
86
|
+
provider = "google";
|
|
125
87
|
}
|
|
126
88
|
}
|
|
127
89
|
this.configData.apiKey = apiKey;
|
|
@@ -133,12 +95,12 @@ var init_config = __esm({
|
|
|
133
95
|
return this.configData;
|
|
134
96
|
}
|
|
135
97
|
save(rootDir, updates) {
|
|
136
|
-
const configDir =
|
|
98
|
+
const configDir = path5.join(rootDir, "data");
|
|
137
99
|
if (!this.configPath) {
|
|
138
|
-
this.configPath =
|
|
100
|
+
this.configPath = path5.join(rootDir, "data", "config.json");
|
|
139
101
|
}
|
|
140
|
-
if (!
|
|
141
|
-
|
|
102
|
+
if (!fs5.existsSync(configDir)) {
|
|
103
|
+
fs5.mkdirSync(configDir, { recursive: true });
|
|
142
104
|
}
|
|
143
105
|
if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
|
|
144
106
|
if (updates.provider !== void 0) this.configData.provider = updates.provider;
|
|
@@ -160,7 +122,7 @@ var init_config = __esm({
|
|
|
160
122
|
this.configData.scheduledJobs = updates.scheduledJobs;
|
|
161
123
|
}
|
|
162
124
|
try {
|
|
163
|
-
|
|
125
|
+
fs5.writeFileSync(
|
|
164
126
|
this.configPath,
|
|
165
127
|
JSON.stringify(this.configData, null, 2),
|
|
166
128
|
"utf-8"
|
|
@@ -171,6 +133,137 @@ var init_config = __esm({
|
|
|
171
133
|
}
|
|
172
134
|
};
|
|
173
135
|
configManager = ConfigManager.getInstance();
|
|
136
|
+
ConfigFileAuthBackend = class {
|
|
137
|
+
constructor(configPath) {
|
|
138
|
+
this.configPath = configPath;
|
|
139
|
+
}
|
|
140
|
+
ensureFile() {
|
|
141
|
+
const dir = path5.dirname(this.configPath);
|
|
142
|
+
if (!fs5.existsSync(dir)) {
|
|
143
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
if (!fs5.existsSync(this.configPath)) {
|
|
146
|
+
fs5.writeFileSync(this.configPath, "{}", "utf-8");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
readAuthJson() {
|
|
150
|
+
this.ensureFile();
|
|
151
|
+
try {
|
|
152
|
+
const raw = fs5.readFileSync(this.configPath, "utf-8");
|
|
153
|
+
const config = JSON.parse(raw);
|
|
154
|
+
if (config._auth && typeof config._auth === "object") {
|
|
155
|
+
return JSON.stringify(config._auth);
|
|
156
|
+
}
|
|
157
|
+
return void 0;
|
|
158
|
+
} catch {
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
writeAuthJson(authJson) {
|
|
163
|
+
this.ensureFile();
|
|
164
|
+
try {
|
|
165
|
+
const raw = fs5.readFileSync(this.configPath, "utf-8");
|
|
166
|
+
const config = JSON.parse(raw);
|
|
167
|
+
config._auth = JSON.parse(authJson);
|
|
168
|
+
fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
169
|
+
} catch {
|
|
170
|
+
const config = { _auth: JSON.parse(authJson) };
|
|
171
|
+
fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
withLock(fn) {
|
|
175
|
+
const current = this.readAuthJson();
|
|
176
|
+
const { result, next } = fn(current);
|
|
177
|
+
if (next !== void 0) {
|
|
178
|
+
this.writeAuthJson(next);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
async withLockAsync(fn) {
|
|
183
|
+
const current = this.readAuthJson();
|
|
184
|
+
const { result, next } = await fn(current);
|
|
185
|
+
if (next !== void 0) {
|
|
186
|
+
this.writeAuthJson(next);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// src/runtime/adapters/attachment-utils.ts
|
|
195
|
+
import fs6 from "fs";
|
|
196
|
+
import path6 from "path";
|
|
197
|
+
import { pipeline } from "stream/promises";
|
|
198
|
+
import { Readable } from "stream";
|
|
199
|
+
function getAttachmentDir(rootDir, channelId) {
|
|
200
|
+
return path6.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
|
|
201
|
+
}
|
|
202
|
+
function sanitizeFilename(name) {
|
|
203
|
+
return name.replace(/[/\\:*?"<>|]/g, "_").replace(/\s+/g, "_");
|
|
204
|
+
}
|
|
205
|
+
async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mimeType, headers) {
|
|
206
|
+
const dir = getAttachmentDir(rootDir, channelId);
|
|
207
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
208
|
+
const ts = Date.now();
|
|
209
|
+
const safeName = sanitizeFilename(filename);
|
|
210
|
+
const storedName = `${ts}-${safeName}`;
|
|
211
|
+
const fullPath = path6.join(dir, storedName);
|
|
212
|
+
const response = await fetch(url, { headers });
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Failed to download attachment from ${url}: ${response.status} ${response.statusText}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const body = response.body;
|
|
219
|
+
if (!body) {
|
|
220
|
+
throw new Error(`Empty response body when downloading ${url}`);
|
|
221
|
+
}
|
|
222
|
+
const nodeStream = Readable.fromWeb(body);
|
|
223
|
+
const writeStream = fs6.createWriteStream(fullPath);
|
|
224
|
+
await pipeline(nodeStream, writeStream);
|
|
225
|
+
const stats = fs6.statSync(fullPath);
|
|
226
|
+
const detectedMime = mimeType || response.headers.get("content-type")?.split(";")[0] || void 0;
|
|
227
|
+
return {
|
|
228
|
+
filename,
|
|
229
|
+
localPath: fullPath,
|
|
230
|
+
mimeType: detectedMime,
|
|
231
|
+
size: stats.size
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function isImageMime(mimeType) {
|
|
235
|
+
return !!mimeType && mimeType.startsWith("image/");
|
|
236
|
+
}
|
|
237
|
+
function formatSize(bytes) {
|
|
238
|
+
if (bytes === void 0 || bytes === null) return "";
|
|
239
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
240
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
241
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
242
|
+
}
|
|
243
|
+
function formatAttachmentsPrompt(attachments) {
|
|
244
|
+
if (attachments.length === 0) return "";
|
|
245
|
+
const lines = attachments.map((a) => {
|
|
246
|
+
const meta = [a.mimeType, formatSize(a.size)].filter(Boolean).join(", ");
|
|
247
|
+
return `- ${a.filename} (${meta}) \u2192 ${a.localPath}`;
|
|
248
|
+
});
|
|
249
|
+
return `[Attachments]
|
|
250
|
+
${lines.join("\n")}`;
|
|
251
|
+
}
|
|
252
|
+
function attachmentsToImageContent(attachments) {
|
|
253
|
+
return attachments.filter((a) => isImageMime(a.mimeType)).map((a) => {
|
|
254
|
+
const buffer = fs6.readFileSync(a.localPath);
|
|
255
|
+
return {
|
|
256
|
+
type: "image",
|
|
257
|
+
data: buffer.toString("base64"),
|
|
258
|
+
mimeType: a.mimeType || "image/png"
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
var ATTACHMENTS_DIR;
|
|
263
|
+
var init_attachment_utils = __esm({
|
|
264
|
+
"src/runtime/adapters/attachment-utils.ts"() {
|
|
265
|
+
"use strict";
|
|
266
|
+
ATTACHMENTS_DIR = "attachments";
|
|
174
267
|
}
|
|
175
268
|
});
|
|
176
269
|
|
|
@@ -2057,9 +2150,10 @@ import { createServer } from "http";
|
|
|
2057
2150
|
import { exec } from "child_process";
|
|
2058
2151
|
|
|
2059
2152
|
// src/runtime/agent.ts
|
|
2153
|
+
init_config();
|
|
2060
2154
|
init_attachment_utils();
|
|
2061
|
-
import
|
|
2062
|
-
import
|
|
2155
|
+
import path8 from "path";
|
|
2156
|
+
import fs8 from "fs";
|
|
2063
2157
|
import { fileURLToPath } from "url";
|
|
2064
2158
|
import {
|
|
2065
2159
|
AuthStorage,
|
|
@@ -2071,8 +2165,8 @@ import {
|
|
|
2071
2165
|
} from "@mariozechner/pi-coding-agent";
|
|
2072
2166
|
|
|
2073
2167
|
// src/runtime/tools/send-file-tool.ts
|
|
2074
|
-
import
|
|
2075
|
-
import
|
|
2168
|
+
import fs7 from "fs";
|
|
2169
|
+
import path7 from "path";
|
|
2076
2170
|
|
|
2077
2171
|
// node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
2078
2172
|
var value_exports = {};
|
|
@@ -4712,7 +4806,7 @@ var MIME_BY_EXT = {
|
|
|
4712
4806
|
".ogg": "audio/ogg"
|
|
4713
4807
|
};
|
|
4714
4808
|
function detectMimeType(filePath) {
|
|
4715
|
-
const ext =
|
|
4809
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
4716
4810
|
return MIME_BY_EXT[ext];
|
|
4717
4811
|
}
|
|
4718
4812
|
function createSendFileTool(fileOutputCallbackRef) {
|
|
@@ -4724,13 +4818,13 @@ function createSendFileTool(fileOutputCallbackRef) {
|
|
|
4724
4818
|
parameters: SendFileParams,
|
|
4725
4819
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
4726
4820
|
const { filePath, caption } = params;
|
|
4727
|
-
if (!
|
|
4821
|
+
if (!fs7.existsSync(filePath)) {
|
|
4728
4822
|
return {
|
|
4729
4823
|
content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
|
|
4730
4824
|
details: void 0
|
|
4731
4825
|
};
|
|
4732
4826
|
}
|
|
4733
|
-
const stats =
|
|
4827
|
+
const stats = fs7.statSync(filePath);
|
|
4734
4828
|
if (!stats.isFile()) {
|
|
4735
4829
|
return {
|
|
4736
4830
|
content: [
|
|
@@ -4739,7 +4833,7 @@ function createSendFileTool(fileOutputCallbackRef) {
|
|
|
4739
4833
|
details: void 0
|
|
4740
4834
|
};
|
|
4741
4835
|
}
|
|
4742
|
-
const filename =
|
|
4836
|
+
const filename = path7.basename(filePath);
|
|
4743
4837
|
const mimeType = detectMimeType(filePath);
|
|
4744
4838
|
const callback = fileOutputCallbackRef.current;
|
|
4745
4839
|
if (callback) {
|
|
@@ -4925,24 +5019,24 @@ var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
|
|
|
4925
5019
|
new URL("../templates/builtin-skills/skill-creator", import.meta.url)
|
|
4926
5020
|
);
|
|
4927
5021
|
function materializeBuiltinSkillCreator(rootDir, skillsPath) {
|
|
4928
|
-
if (!
|
|
5022
|
+
if (!fs8.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
|
|
4929
5023
|
log(
|
|
4930
5024
|
`[PackAgent] Built-in skill-creator template missing: ${BUILTIN_SKILL_CREATOR_TEMPLATE_DIR}`
|
|
4931
5025
|
);
|
|
4932
5026
|
return null;
|
|
4933
5027
|
}
|
|
4934
|
-
const packConfigPath =
|
|
4935
|
-
const skillDir =
|
|
4936
|
-
const skillPath =
|
|
5028
|
+
const packConfigPath = path8.resolve(rootDir, "skillpack.json");
|
|
5029
|
+
const skillDir = path8.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
|
|
5030
|
+
const skillPath = path8.join(skillDir, "SKILL.md");
|
|
4937
5031
|
const renderTemplate = (content) => content.replaceAll("{{SKILLS_PATH}}", skillsPath).replaceAll("{{PACK_CONFIG_PATH}}", packConfigPath);
|
|
4938
5032
|
const copyDir = (srcDir, destDir) => {
|
|
4939
|
-
|
|
4940
|
-
for (const entry of
|
|
5033
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
5034
|
+
for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
|
|
4941
5035
|
if (entry.name === ".DS_Store") {
|
|
4942
5036
|
continue;
|
|
4943
5037
|
}
|
|
4944
|
-
const srcPath =
|
|
4945
|
-
const destPath =
|
|
5038
|
+
const srcPath = path8.join(srcDir, entry.name);
|
|
5039
|
+
const destPath = path8.join(destDir, entry.name);
|
|
4946
5040
|
if (entry.isDirectory()) {
|
|
4947
5041
|
copyDir(srcPath, destPath);
|
|
4948
5042
|
continue;
|
|
@@ -4951,17 +5045,17 @@ function materializeBuiltinSkillCreator(rootDir, skillsPath) {
|
|
|
4951
5045
|
continue;
|
|
4952
5046
|
}
|
|
4953
5047
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".py")) {
|
|
4954
|
-
const content =
|
|
4955
|
-
|
|
5048
|
+
const content = fs8.readFileSync(srcPath, "utf-8");
|
|
5049
|
+
fs8.writeFileSync(destPath, renderTemplate(content), "utf-8");
|
|
4956
5050
|
continue;
|
|
4957
5051
|
}
|
|
4958
|
-
|
|
5052
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
4959
5053
|
}
|
|
4960
5054
|
};
|
|
4961
|
-
if (!
|
|
5055
|
+
if (!fs8.existsSync(skillDir)) {
|
|
4962
5056
|
copyDir(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR, skillDir);
|
|
4963
5057
|
}
|
|
4964
|
-
if (!
|
|
5058
|
+
if (!fs8.existsSync(skillPath)) {
|
|
4965
5059
|
log(
|
|
4966
5060
|
`[PackAgent] Materialized built-in skill-creator but SKILL.md is missing: ${skillPath}`
|
|
4967
5061
|
);
|
|
@@ -5014,8 +5108,29 @@ var PackAgent = class {
|
|
|
5014
5108
|
current: null
|
|
5015
5109
|
};
|
|
5016
5110
|
schedulerRef = { current: null };
|
|
5111
|
+
authStorage;
|
|
5017
5112
|
constructor(options) {
|
|
5018
5113
|
this.options = options;
|
|
5114
|
+
const configPath = path8.resolve(options.rootDir, "data", "config.json");
|
|
5115
|
+
const backend = new ConfigFileAuthBackend(configPath);
|
|
5116
|
+
this.authStorage = AuthStorage.fromStorage(backend);
|
|
5117
|
+
const providerMeta = SUPPORTED_PROVIDERS[options.provider];
|
|
5118
|
+
if (providerMeta?.authType === "api_key" && options.apiKey) {
|
|
5119
|
+
this.authStorage.setRuntimeApiKey(options.provider, options.apiKey);
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
/** Get the shared AuthStorage instance (used by OAuth API endpoints) */
|
|
5123
|
+
getAuthStorage() {
|
|
5124
|
+
return this.authStorage;
|
|
5125
|
+
}
|
|
5126
|
+
/** Update runtime auth when provider/apiKey changes */
|
|
5127
|
+
updateAuth(provider, apiKey) {
|
|
5128
|
+
this.authStorage.removeRuntimeApiKey(this.options.provider);
|
|
5129
|
+
this.options.provider = provider;
|
|
5130
|
+
if (apiKey) {
|
|
5131
|
+
this.options.apiKey = apiKey;
|
|
5132
|
+
this.authStorage.setRuntimeApiKey(provider, apiKey);
|
|
5133
|
+
}
|
|
5019
5134
|
}
|
|
5020
5135
|
/**
|
|
5021
5136
|
* Inject scheduler reference (called by server.ts after adapter init).
|
|
@@ -5039,35 +5154,32 @@ var PackAgent = class {
|
|
|
5039
5154
|
const pendingCreation = this.pendingSessionCreations.get(channelId);
|
|
5040
5155
|
if (pendingCreation) return pendingCreation;
|
|
5041
5156
|
const createSessionPromise = (async () => {
|
|
5042
|
-
const {
|
|
5043
|
-
const authStorage =
|
|
5044
|
-
[provider]: { type: "api_key", key: apiKey }
|
|
5045
|
-
});
|
|
5046
|
-
authStorage.setRuntimeApiKey(provider, apiKey);
|
|
5157
|
+
const { rootDir, provider, modelId, baseUrl } = this.options;
|
|
5158
|
+
const authStorage = this.authStorage;
|
|
5047
5159
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
5048
5160
|
const resolvedModel = modelRegistry.find(provider, modelId);
|
|
5049
5161
|
const model = resolvedModel && baseUrl ? { ...resolvedModel, baseUrl } : resolvedModel;
|
|
5050
5162
|
if (resolvedModel && baseUrl) {
|
|
5051
5163
|
log(`[PackAgent] Overriding ${provider}/${modelId} baseUrl -> ${baseUrl}`);
|
|
5052
5164
|
}
|
|
5053
|
-
const sessionDir =
|
|
5165
|
+
const sessionDir = path8.resolve(
|
|
5054
5166
|
rootDir,
|
|
5055
5167
|
"data",
|
|
5056
5168
|
"sessions",
|
|
5057
5169
|
channelId
|
|
5058
5170
|
);
|
|
5059
|
-
|
|
5171
|
+
fs8.mkdirSync(sessionDir, { recursive: true });
|
|
5060
5172
|
const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
|
|
5061
5173
|
log(`[PackAgent] Session dir: ${sessionDir}`);
|
|
5062
|
-
const workspaceDir =
|
|
5174
|
+
const workspaceDir = path8.resolve(
|
|
5063
5175
|
rootDir,
|
|
5064
5176
|
"data",
|
|
5065
5177
|
"workspaces",
|
|
5066
5178
|
channelId
|
|
5067
5179
|
);
|
|
5068
|
-
|
|
5180
|
+
fs8.mkdirSync(workspaceDir, { recursive: true });
|
|
5069
5181
|
log(`[PackAgent] Workspace dir: ${workspaceDir}`);
|
|
5070
|
-
const skillsPath =
|
|
5182
|
+
const skillsPath = path8.resolve(rootDir, "skills");
|
|
5071
5183
|
log(`[PackAgent] Loading skills from: ${skillsPath}`);
|
|
5072
5184
|
const materializedSkillCreator = materializeBuiltinSkillCreator(
|
|
5073
5185
|
rootDir,
|
|
@@ -5249,9 +5361,9 @@ ${text}`;
|
|
|
5249
5361
|
this.channels.delete(channelId);
|
|
5250
5362
|
}
|
|
5251
5363
|
const { rootDir } = this.options;
|
|
5252
|
-
const sessionDir =
|
|
5253
|
-
if (
|
|
5254
|
-
|
|
5364
|
+
const sessionDir = path8.resolve(rootDir, "data", "sessions", channelId);
|
|
5365
|
+
if (fs8.existsSync(sessionDir)) {
|
|
5366
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
5255
5367
|
log(`[PackAgent] Cleared session dir: ${sessionDir}`);
|
|
5256
5368
|
}
|
|
5257
5369
|
return {
|
|
@@ -5340,6 +5452,9 @@ var WebAdapter = class {
|
|
|
5340
5452
|
app.get("/api/config", (_req, res) => {
|
|
5341
5453
|
const config = getPackConfig(rootDir);
|
|
5342
5454
|
const conf = configManager.getConfig();
|
|
5455
|
+
const currentProvider2 = conf.provider || "openai";
|
|
5456
|
+
const providerMeta = SUPPORTED_PROVIDERS[currentProvider2];
|
|
5457
|
+
const oauthConnected = providerMeta?.authType === "oauth" ? agent.getAuthStorage().hasAuth(currentProvider2) : false;
|
|
5343
5458
|
res.json({
|
|
5344
5459
|
name: config.name,
|
|
5345
5460
|
description: config.description,
|
|
@@ -5347,9 +5462,11 @@ var WebAdapter = class {
|
|
|
5347
5462
|
skills: config.skills || [],
|
|
5348
5463
|
hasApiKey: !!conf.apiKey,
|
|
5349
5464
|
apiKey: conf.apiKey || "",
|
|
5350
|
-
provider:
|
|
5465
|
+
provider: currentProvider2,
|
|
5351
5466
|
baseUrl: conf.baseUrl || "",
|
|
5352
|
-
adapters: conf.adapters || {}
|
|
5467
|
+
adapters: conf.adapters || {},
|
|
5468
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
5469
|
+
oauthConnected
|
|
5353
5470
|
});
|
|
5354
5471
|
});
|
|
5355
5472
|
app.get("/api/skills", (_req, res) => {
|
|
@@ -5375,16 +5492,62 @@ var WebAdapter = class {
|
|
|
5375
5492
|
updates.adapters = adapters;
|
|
5376
5493
|
}
|
|
5377
5494
|
configManager.save(rootDir, updates);
|
|
5378
|
-
|
|
5379
|
-
const
|
|
5495
|
+
agent.updateAuth(currentProvider, apiKey);
|
|
5496
|
+
const afterConfig = configManager.getConfig();
|
|
5497
|
+
const requiresRestart = getRuntimeConfigSignature(beforeConfig) !== getRuntimeConfigSignature(afterConfig);
|
|
5380
5498
|
res.json({
|
|
5381
|
-
|
|
5382
|
-
provider: newConf.provider,
|
|
5383
|
-
baseUrl: newConf.baseUrl || "",
|
|
5384
|
-
adapters: newConf.adapters,
|
|
5499
|
+
...afterConfig,
|
|
5385
5500
|
requiresRestart
|
|
5386
5501
|
});
|
|
5387
5502
|
});
|
|
5503
|
+
app.post("/api/oauth/login", async (req, res) => {
|
|
5504
|
+
const { provider } = req.body;
|
|
5505
|
+
const meta = SUPPORTED_PROVIDERS[provider];
|
|
5506
|
+
if (!meta || meta.authType !== "oauth") {
|
|
5507
|
+
return res.status(400).json({ error: "Provider does not support OAuth" });
|
|
5508
|
+
}
|
|
5509
|
+
try {
|
|
5510
|
+
const authStorage = agent.getAuthStorage();
|
|
5511
|
+
let authUrl = "";
|
|
5512
|
+
const loginPromise = authStorage.login(provider, {
|
|
5513
|
+
onAuth: (info) => {
|
|
5514
|
+
authUrl = info.url;
|
|
5515
|
+
},
|
|
5516
|
+
onPrompt: async (prompt) => {
|
|
5517
|
+
return "";
|
|
5518
|
+
},
|
|
5519
|
+
onProgress: (msg) => {
|
|
5520
|
+
console.log(`[OAuth] ${provider} login progress: ${msg}`);
|
|
5521
|
+
}
|
|
5522
|
+
});
|
|
5523
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
5524
|
+
if (authUrl) {
|
|
5525
|
+
res.json({ status: "pending", authUrl });
|
|
5526
|
+
} else {
|
|
5527
|
+
res.json({ status: "pending" });
|
|
5528
|
+
}
|
|
5529
|
+
loginPromise.catch((err) => {
|
|
5530
|
+
console.error(`[OAuth] ${provider} login error:`, err);
|
|
5531
|
+
});
|
|
5532
|
+
} catch (err) {
|
|
5533
|
+
res.status(500).json({ error: String(err) });
|
|
5534
|
+
}
|
|
5535
|
+
});
|
|
5536
|
+
app.get("/api/oauth/status", (_req, res) => {
|
|
5537
|
+
const conf = configManager.getConfig();
|
|
5538
|
+
const provider = conf.provider || "openai";
|
|
5539
|
+
const meta = SUPPORTED_PROVIDERS[provider];
|
|
5540
|
+
if (!meta || meta.authType !== "oauth") {
|
|
5541
|
+
return res.json({ connected: false });
|
|
5542
|
+
}
|
|
5543
|
+
const connected = agent.getAuthStorage().hasAuth(provider);
|
|
5544
|
+
res.json({ connected, provider });
|
|
5545
|
+
});
|
|
5546
|
+
app.post("/api/oauth/logout", (req, res) => {
|
|
5547
|
+
const { provider } = req.body;
|
|
5548
|
+
agent.getAuthStorage().logout(provider);
|
|
5549
|
+
res.json({ success: true });
|
|
5550
|
+
});
|
|
5388
5551
|
app.post("/api/runtime/restart", async (_req, res) => {
|
|
5389
5552
|
const result = await lifecycle.requestRestart("web");
|
|
5390
5553
|
res.status(202).json(result);
|
|
@@ -5508,8 +5671,10 @@ var WebAdapter = class {
|
|
|
5508
5671
|
`http://${request.headers.host || "127.0.0.1"}`
|
|
5509
5672
|
);
|
|
5510
5673
|
const _reqProvider = url.searchParams.get("provider") || currentProvider;
|
|
5511
|
-
|
|
5512
|
-
|
|
5674
|
+
const providerMeta = SUPPORTED_PROVIDERS[_reqProvider];
|
|
5675
|
+
const hasAuth = providerMeta?.authType === "oauth" ? agent.getAuthStorage().hasAuth(_reqProvider) : !!apiKey;
|
|
5676
|
+
if (!hasAuth) {
|
|
5677
|
+
ws.send(JSON.stringify({ error: "Please configure authentication first" }));
|
|
5513
5678
|
ws.close();
|
|
5514
5679
|
return;
|
|
5515
5680
|
}
|
|
@@ -5819,7 +5984,7 @@ async function startServer(options) {
|
|
|
5819
5984
|
const canonicalRootDir = canonicalizeDir(rootDir);
|
|
5820
5985
|
const packConfig = loadConfig(canonicalRootDir);
|
|
5821
5986
|
const baseUrl = dataConfig.baseUrl?.trim() || void 0;
|
|
5822
|
-
const modelId = provider
|
|
5987
|
+
const modelId = SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId;
|
|
5823
5988
|
const packageRoot = path12.resolve(__dirname, "..");
|
|
5824
5989
|
const webDir = fs13.existsSync(path12.join(rootDir, "web")) ? path12.join(rootDir, "web") : path12.join(packageRoot, "web");
|
|
5825
5990
|
const app = express();
|
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.openai.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,69 @@ 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
|
+
if (baseUrlInput) {
|
|
163
|
+
baseUrlInput.placeholder =
|
|
164
|
+
meta.baseUrlPlaceholder || "https://api.openai.com/v1";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Update placeholder
|
|
168
|
+
if (apiKeyInput) {
|
|
169
|
+
apiKeyInput.placeholder = meta.placeholder || "sk-...";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
105
172
|
}
|
|
106
173
|
|
|
107
174
|
async function handleSave() {
|
|
108
175
|
const key = apiKeyInput.value.trim();
|
|
109
176
|
const provider = providerSelect.value;
|
|
177
|
+
const baseUrl = baseUrlInput.value.trim();
|
|
110
178
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
179
|
+
// If OAuth is selected, we don't handle it here but it shouldn't happen as save button is hidden
|
|
180
|
+
const meta = state.config?.supportedProviders?.[provider];
|
|
181
|
+
if (meta?.authType === "oauth") return;
|
|
115
182
|
|
|
116
183
|
const updates = { provider };
|
|
117
|
-
if (
|
|
184
|
+
if (baseUrl !== state.config.baseUrl) {
|
|
185
|
+
updates.baseUrl = baseUrl;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (key && key !== "***************************************************" && key !== state.config.apiKey) {
|
|
118
189
|
updates.key = key;
|
|
119
190
|
}
|
|
120
191
|
|
|
@@ -123,33 +194,21 @@ async function handleSave() {
|
|
|
123
194
|
const res = await saveConfigData(updates);
|
|
124
195
|
|
|
125
196
|
state.config.provider = res.provider;
|
|
197
|
+
state.config.baseUrl = res.baseUrl || "";
|
|
126
198
|
if (updates.key) {
|
|
127
199
|
state.config.hasApiKey = true;
|
|
128
200
|
state.config.apiKey = updates.key;
|
|
129
201
|
}
|
|
130
202
|
|
|
131
|
-
|
|
132
|
-
apiKeyInput.value = state.config.apiKey;
|
|
133
|
-
} else if (state.config.hasApiKey) {
|
|
134
|
-
apiKeyInput.value = "***************************************************";
|
|
135
|
-
} else {
|
|
136
|
-
apiKeyInput.value = "";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
203
|
+
populateForm();
|
|
140
204
|
state.restartRequired = !!res.requiresRestart;
|
|
141
|
-
|
|
142
205
|
updateApiKeyButton();
|
|
143
206
|
|
|
144
207
|
if (res.requiresRestart) {
|
|
145
|
-
setStatus(
|
|
146
|
-
"API key saved. Restart service to apply changes.",
|
|
147
|
-
"warning",
|
|
148
|
-
);
|
|
208
|
+
setStatus("Settings saved. Restart service to apply changes.", "warning");
|
|
149
209
|
updateRestartButton(true);
|
|
150
210
|
} else {
|
|
151
|
-
setStatus("
|
|
152
|
-
// 延迟关闭让用户看到成功消息
|
|
211
|
+
setStatus("Settings saved successfully", "success");
|
|
153
212
|
setTimeout(() => close(), 1200);
|
|
154
213
|
}
|
|
155
214
|
} catch (err) {
|
|
@@ -159,6 +218,107 @@ async function handleSave() {
|
|
|
159
218
|
}
|
|
160
219
|
}
|
|
161
220
|
|
|
221
|
+
async function handleOAuthLogin() {
|
|
222
|
+
const provider = providerSelect.value;
|
|
223
|
+
oauthLoginBtn.disabled = true;
|
|
224
|
+
setStatus("Starting OAuth login...", "warning");
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Ensure provider is saved first
|
|
228
|
+
await saveConfigData({ provider });
|
|
229
|
+
|
|
230
|
+
const res = await fetch("/api/oauth/login", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: { "Content-Type": "application/json" },
|
|
233
|
+
body: JSON.stringify({ provider }),
|
|
234
|
+
});
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
|
|
237
|
+
if (data.authUrl) {
|
|
238
|
+
window.open(data.authUrl, "_blank");
|
|
239
|
+
setStatus("Waiting for authorization in browser...", "warning");
|
|
240
|
+
pollOAuthStatus();
|
|
241
|
+
} else {
|
|
242
|
+
setStatus("OAuth process started. Check terminal for URL if browser didn't open.", "warning");
|
|
243
|
+
pollOAuthStatus();
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
setStatus("Login failed: " + err.message, "error");
|
|
247
|
+
oauthLoginBtn.disabled = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function handleOAuthLogout() {
|
|
252
|
+
const provider = providerSelect.value;
|
|
253
|
+
try {
|
|
254
|
+
await fetch("/api/oauth/logout", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "Content-Type": "application/json" },
|
|
257
|
+
body: JSON.stringify({ provider }),
|
|
258
|
+
});
|
|
259
|
+
updateOAuthUI(false);
|
|
260
|
+
state.config.oauthConnected = false;
|
|
261
|
+
updateApiKeyButton();
|
|
262
|
+
setStatus("Logged out successfully", "success");
|
|
263
|
+
} catch (err) {
|
|
264
|
+
setStatus("Logout failed: " + err.message, "error");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function checkOAuthStatus() {
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch("/api/oauth/status");
|
|
271
|
+
const { connected } = await res.json();
|
|
272
|
+
updateOAuthUI(connected);
|
|
273
|
+
state.config.oauthConnected = connected;
|
|
274
|
+
updateApiKeyButton();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error("Failed to check OAuth status:", err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function pollOAuthStatus() {
|
|
281
|
+
const interval = setInterval(async () => {
|
|
282
|
+
const res = await fetch("/api/oauth/status");
|
|
283
|
+
const { connected } = await res.json();
|
|
284
|
+
if (connected) {
|
|
285
|
+
clearInterval(interval);
|
|
286
|
+
updateOAuthUI(true);
|
|
287
|
+
state.config.oauthConnected = true;
|
|
288
|
+
state.restartRequired = true;
|
|
289
|
+
updateApiKeyButton();
|
|
290
|
+
setStatus("Connected successfully!", "success");
|
|
291
|
+
updateRestartButton(true);
|
|
292
|
+
}
|
|
293
|
+
}, 2000);
|
|
294
|
+
|
|
295
|
+
// Timeout after 5 minutes
|
|
296
|
+
setTimeout(() => clearInterval(interval), 300000);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function updateOAuthUI(connected) {
|
|
300
|
+
if (connected) {
|
|
301
|
+
if (oauthStatusIndicator) {
|
|
302
|
+
oauthStatusIndicator.classList.remove("oauth-disconnected");
|
|
303
|
+
oauthStatusIndicator.classList.add("oauth-connected");
|
|
304
|
+
}
|
|
305
|
+
if (oauthStatusText) oauthStatusText.textContent = "Connected";
|
|
306
|
+
if (oauthLoginBtn) oauthLoginBtn.style.display = "none";
|
|
307
|
+
if (oauthLogoutBtn) oauthLogoutBtn.style.display = "";
|
|
308
|
+
} else {
|
|
309
|
+
if (oauthStatusIndicator) {
|
|
310
|
+
oauthStatusIndicator.classList.add("oauth-disconnected");
|
|
311
|
+
oauthStatusIndicator.classList.remove("oauth-connected");
|
|
312
|
+
}
|
|
313
|
+
if (oauthStatusText) oauthStatusText.textContent = "Not Connected";
|
|
314
|
+
if (oauthLoginBtn) {
|
|
315
|
+
oauthLoginBtn.style.display = "";
|
|
316
|
+
oauthLoginBtn.disabled = false;
|
|
317
|
+
}
|
|
318
|
+
if (oauthLogoutBtn) oauthLogoutBtn.style.display = "none";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
162
322
|
async function handleRestart() {
|
|
163
323
|
if (!restartBtn) return;
|
|
164
324
|
|
package/web/js/main.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { state, loadConfig } from "./config.js";
|
|
2
|
-
import { initSettings } from "./settings.js";
|
|
3
2
|
import { initApiKeyDialog, updateApiKeyButton } from "./api-key-dialog.js";
|
|
4
3
|
import { initChatAppsDialog, updateChatAppsButton } from "./chat-apps-dialog.js";
|
|
5
4
|
import { initChat, showWelcome } from "./chat.js";
|
|
@@ -42,7 +41,6 @@ async function init() {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
// Initialize all dialog modules
|
|
45
|
-
initSettings();
|
|
46
44
|
initApiKeyDialog();
|
|
47
45
|
initChatAppsDialog();
|
|
48
46
|
initChat();
|
package/web/styles.css
CHANGED
|
@@ -72,13 +72,6 @@ body {
|
|
|
72
72
|
line-height: 1.4;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
.sidebar-settings-section {
|
|
76
|
-
padding: 16px 20px;
|
|
77
|
-
border-top: 1px solid var(--border-color);
|
|
78
|
-
background: var(--bg-secondary);
|
|
79
|
-
flex-shrink: 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
75
|
.sidebar-skills-section {
|
|
83
76
|
padding: 16px 20px;
|
|
84
77
|
flex: 1;
|
|
@@ -874,26 +867,6 @@ body {
|
|
|
874
867
|
width: 440px;
|
|
875
868
|
}
|
|
876
869
|
|
|
877
|
-
/* ---- Settings Modal ---- */
|
|
878
|
-
.settings-trigger-btn {
|
|
879
|
-
display: flex;
|
|
880
|
-
align-items: center;
|
|
881
|
-
justify-content: center;
|
|
882
|
-
gap: 8px;
|
|
883
|
-
width: 100%;
|
|
884
|
-
padding: 12px;
|
|
885
|
-
background: var(--bg-tertiary);
|
|
886
|
-
border: 1px solid var(--border-color);
|
|
887
|
-
border-radius: var(--radius-sm);
|
|
888
|
-
color: var(--text-primary);
|
|
889
|
-
font-weight: 500;
|
|
890
|
-
cursor: pointer;
|
|
891
|
-
transition: all 0.2s ease;
|
|
892
|
-
}
|
|
893
|
-
.settings-trigger-btn:hover {
|
|
894
|
-
background: #ebebeb;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
870
|
.settings-modal {
|
|
898
871
|
margin: auto;
|
|
899
872
|
padding: 0;
|
|
@@ -1073,3 +1046,68 @@ body {
|
|
|
1073
1046
|
transform: none;
|
|
1074
1047
|
box-shadow: none;
|
|
1075
1048
|
}
|
|
1049
|
+
.label-hint {
|
|
1050
|
+
color: var(--text-secondary);
|
|
1051
|
+
font-size: 11px;
|
|
1052
|
+
font-weight: 400;
|
|
1053
|
+
margin-left: 4px;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/* OAuth UI */
|
|
1057
|
+
#apikey-oauth-section {
|
|
1058
|
+
padding: 8px 0;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.oauth-status-card {
|
|
1062
|
+
background: var(--bg-tertiary);
|
|
1063
|
+
border: 1px solid var(--border-color);
|
|
1064
|
+
border-radius: var(--radius-sm);
|
|
1065
|
+
padding: 16px;
|
|
1066
|
+
margin-bottom: 16px;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
#oauth-status-indicator {
|
|
1070
|
+
display: flex;
|
|
1071
|
+
align-items: center;
|
|
1072
|
+
gap: 8px;
|
|
1073
|
+
font-size: 13px;
|
|
1074
|
+
font-weight: 600;
|
|
1075
|
+
margin-bottom: 8px;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.oauth-status-dot {
|
|
1079
|
+
width: 8px;
|
|
1080
|
+
height: 8px;
|
|
1081
|
+
border-radius: 50%;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.oauth-disconnected .oauth-status-dot {
|
|
1085
|
+
background-color: #94a3b8;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.oauth-connected .oauth-status-dot {
|
|
1089
|
+
background-color: #22c55e;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.oauth-disconnected #oauth-status-text {
|
|
1093
|
+
color: var(--text-secondary);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.oauth-connected #oauth-status-text {
|
|
1097
|
+
color: #16a34a;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.oauth-hint {
|
|
1101
|
+
font-size: 12px;
|
|
1102
|
+
color: var(--text-muted);
|
|
1103
|
+
line-height: 1.5;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.oauth-btn {
|
|
1107
|
+
width: 100%;
|
|
1108
|
+
margin-top: 8px;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.oauth-btn svg {
|
|
1112
|
+
margin-right: 8px;
|
|
1113
|
+
}
|
package/web/js/settings.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { state } from "./config.js";
|
|
2
|
-
import { restartRuntime, saveConfigData } from "./api.js";
|
|
3
|
-
|
|
4
|
-
// DOM Elements
|
|
5
|
-
let dialog;
|
|
6
|
-
let settingsBtn;
|
|
7
|
-
let closeBtn;
|
|
8
|
-
let saveBtn;
|
|
9
|
-
let providerSelect;
|
|
10
|
-
let apiKeyInput;
|
|
11
|
-
let telegramTokenInput;
|
|
12
|
-
let slackBotTokenInput;
|
|
13
|
-
let slackAppTokenInput;
|
|
14
|
-
let keyStatus;
|
|
15
|
-
let restartBtn;
|
|
16
|
-
|
|
17
|
-
export function initSettings() {
|
|
18
|
-
dialog = document.getElementById("settings-dialog");
|
|
19
|
-
settingsBtn = document.getElementById("open-settings-btn");
|
|
20
|
-
closeBtn = document.getElementById("close-settings-btn");
|
|
21
|
-
saveBtn = document.getElementById("save-settings-btn");
|
|
22
|
-
|
|
23
|
-
providerSelect = document.getElementById("provider-select");
|
|
24
|
-
apiKeyInput = document.getElementById("api-key-input");
|
|
25
|
-
telegramTokenInput = document.getElementById("telegram-token-input");
|
|
26
|
-
slackBotTokenInput = document.getElementById("slack-bot-token-input");
|
|
27
|
-
slackAppTokenInput = document.getElementById("slack-app-token-input");
|
|
28
|
-
keyStatus = document.getElementById("key-status");
|
|
29
|
-
restartBtn = document.getElementById("restart-service-btn");
|
|
30
|
-
|
|
31
|
-
if (!dialog) return;
|
|
32
|
-
|
|
33
|
-
// Open/Close dialog
|
|
34
|
-
if (settingsBtn) {
|
|
35
|
-
settingsBtn.addEventListener("click", () => {
|
|
36
|
-
populateForm();
|
|
37
|
-
dialog.showModal();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (closeBtn) {
|
|
42
|
-
closeBtn.addEventListener("click", () => {
|
|
43
|
-
dialog.close();
|
|
44
|
-
keyStatus.textContent = ""; // clear status on close
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Save Settings
|
|
49
|
-
if (saveBtn) {
|
|
50
|
-
saveBtn.addEventListener("click", handleSave);
|
|
51
|
-
}
|
|
52
|
-
if (restartBtn) {
|
|
53
|
-
restartBtn.addEventListener("click", handleRestart);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Placeholder logic
|
|
57
|
-
if (providerSelect) {
|
|
58
|
-
providerSelect.addEventListener("change", updatePlaceholder);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function updatePlaceholder() {
|
|
63
|
-
const p = providerSelect.value;
|
|
64
|
-
if (p === "openai") apiKeyInput.placeholder = "sk-proj-...";
|
|
65
|
-
else if (p === "anthropic") apiKeyInput.placeholder = "sk-ant-api03-...";
|
|
66
|
-
else apiKeyInput.placeholder = "sk-...";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function populateForm() {
|
|
70
|
-
const config = state.config;
|
|
71
|
-
if (!config) return;
|
|
72
|
-
|
|
73
|
-
if (state.restartRequired) {
|
|
74
|
-
setStatus(
|
|
75
|
-
"Settings saved. Restart service to apply changes.",
|
|
76
|
-
"warning",
|
|
77
|
-
);
|
|
78
|
-
updateRestartButton(true);
|
|
79
|
-
} else {
|
|
80
|
-
setStatus("", "");
|
|
81
|
-
updateRestartButton(false);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (config.hasApiKey && config.apiKey) {
|
|
85
|
-
apiKeyInput.value = config.apiKey;
|
|
86
|
-
} else if (config.hasApiKey) {
|
|
87
|
-
apiKeyInput.value = "***************************************************";
|
|
88
|
-
} else {
|
|
89
|
-
apiKeyInput.value = "";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (config.provider) {
|
|
93
|
-
providerSelect.value = config.provider;
|
|
94
|
-
}
|
|
95
|
-
updatePlaceholder();
|
|
96
|
-
|
|
97
|
-
const adapters = config.adapters || {};
|
|
98
|
-
if (adapters.telegram && adapters.telegram.token) {
|
|
99
|
-
telegramTokenInput.value = adapters.telegram.token;
|
|
100
|
-
} else {
|
|
101
|
-
telegramTokenInput.value = "";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (adapters.slack) {
|
|
105
|
-
slackBotTokenInput.value = adapters.slack.botToken || "";
|
|
106
|
-
slackAppTokenInput.value = adapters.slack.appToken || "";
|
|
107
|
-
} else {
|
|
108
|
-
slackBotTokenInput.value = "";
|
|
109
|
-
slackAppTokenInput.value = "";
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function handleSave() {
|
|
114
|
-
const key = apiKeyInput.value.trim();
|
|
115
|
-
const provider = providerSelect.value;
|
|
116
|
-
const telegramToken = telegramTokenInput.value.trim();
|
|
117
|
-
const slackBotToken = slackBotTokenInput.value.trim();
|
|
118
|
-
const slackAppToken = slackAppTokenInput.value.trim();
|
|
119
|
-
|
|
120
|
-
const adapters = {};
|
|
121
|
-
if (telegramToken) adapters.telegram = { token: telegramToken };
|
|
122
|
-
if (slackBotToken || slackAppToken) {
|
|
123
|
-
adapters.slack = {
|
|
124
|
-
botToken: slackBotToken || undefined,
|
|
125
|
-
appToken: slackAppToken || undefined
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const updates = { provider, adapters };
|
|
130
|
-
if (key && key !== "***************************************************" && key !== state.config.apiKey) {
|
|
131
|
-
updates.key = key;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const res = await saveConfigData(updates);
|
|
136
|
-
|
|
137
|
-
// Update local config
|
|
138
|
-
state.config.provider = res.provider;
|
|
139
|
-
state.config.adapters = res.adapters;
|
|
140
|
-
if (updates.key) {
|
|
141
|
-
state.config.hasApiKey = true;
|
|
142
|
-
state.config.apiKey = updates.key;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (state.config.hasApiKey && state.config.apiKey) {
|
|
146
|
-
apiKeyInput.value = state.config.apiKey;
|
|
147
|
-
} else if (state.config.hasApiKey) {
|
|
148
|
-
apiKeyInput.value = "***************************************************";
|
|
149
|
-
} else {
|
|
150
|
-
apiKeyInput.value = "";
|
|
151
|
-
}
|
|
152
|
-
state.restartRequired = !!res.requiresRestart;
|
|
153
|
-
|
|
154
|
-
if (res.requiresRestart) {
|
|
155
|
-
setStatus(
|
|
156
|
-
"Settings saved. Restart service to apply changes.",
|
|
157
|
-
"warning",
|
|
158
|
-
);
|
|
159
|
-
updateRestartButton(true);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
setStatus("Settings saved", "success");
|
|
164
|
-
updateRestartButton(false);
|
|
165
|
-
|
|
166
|
-
} catch (err) {
|
|
167
|
-
setStatus("Save failed: " + err.message, "error");
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function handleRestart() {
|
|
172
|
-
if (!restartBtn) return;
|
|
173
|
-
|
|
174
|
-
restartBtn.disabled = true;
|
|
175
|
-
if (saveBtn) saveBtn.disabled = true;
|
|
176
|
-
setStatus("Restarting service...", "warning");
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
await restartRuntime();
|
|
180
|
-
setTimeout(() => {
|
|
181
|
-
window.location.reload();
|
|
182
|
-
}, 6000);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
if (saveBtn) saveBtn.disabled = false;
|
|
185
|
-
restartBtn.disabled = false;
|
|
186
|
-
setStatus("Restart failed: " + err.message, "error");
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function updateRestartButton(show) {
|
|
191
|
-
if (!restartBtn) return;
|
|
192
|
-
restartBtn.hidden = !show;
|
|
193
|
-
restartBtn.disabled = false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function setStatus(message, status) {
|
|
197
|
-
if (!keyStatus) return;
|
|
198
|
-
keyStatus.textContent = message;
|
|
199
|
-
keyStatus.className = status ? `status-text ${status}` : "status-text";
|
|
200
|
-
}
|