@cremini/skillpack 1.2.0 → 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 +294 -131
- package/package.json +1 -1
- package/web/index.html +29 -62
- package/web/js/api-key-dialog.js +188 -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,46 @@ 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
|
+
supportsBaseUrl: true
|
|
27
|
+
},
|
|
28
|
+
anthropic: {
|
|
29
|
+
label: "Anthropic",
|
|
30
|
+
defaultModelId: "claude-opus-4-6",
|
|
31
|
+
authType: "api_key",
|
|
32
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
33
|
+
placeholder: "sk-ant-api03-...",
|
|
34
|
+
supportsBaseUrl: true
|
|
35
|
+
},
|
|
36
|
+
google: {
|
|
37
|
+
label: "Google (Gemini)",
|
|
38
|
+
defaultModelId: "gemini-2.5-pro",
|
|
39
|
+
authType: "api_key",
|
|
40
|
+
envKey: "GOOGLE_API_KEY",
|
|
41
|
+
placeholder: "AIza...",
|
|
42
|
+
supportsBaseUrl: false
|
|
43
|
+
},
|
|
44
|
+
"openai-codex": {
|
|
45
|
+
label: "OpenAI Codex",
|
|
46
|
+
defaultModelId: "gpt-5.4",
|
|
47
|
+
authType: "oauth",
|
|
48
|
+
oauthProviderId: "openai-codex",
|
|
49
|
+
supportsBaseUrl: false
|
|
50
|
+
}
|
|
51
|
+
};
|
|
95
52
|
ConfigManager = class _ConfigManager {
|
|
96
53
|
static instance;
|
|
97
54
|
configData = {};
|
|
@@ -105,10 +62,10 @@ var init_config = __esm({
|
|
|
105
62
|
return _ConfigManager.instance;
|
|
106
63
|
}
|
|
107
64
|
load(rootDir) {
|
|
108
|
-
this.configPath =
|
|
109
|
-
if (
|
|
65
|
+
this.configPath = path5.join(rootDir, "data", "config.json");
|
|
66
|
+
if (fs5.existsSync(this.configPath)) {
|
|
110
67
|
try {
|
|
111
|
-
this.configData = JSON.parse(
|
|
68
|
+
this.configData = JSON.parse(fs5.readFileSync(this.configPath, "utf-8"));
|
|
112
69
|
console.log(" Loaded config from data/config.json");
|
|
113
70
|
} catch (err) {
|
|
114
71
|
console.warn(" Warning: Failed to parse data/config.json:", err);
|
|
@@ -122,6 +79,9 @@ var init_config = __esm({
|
|
|
122
79
|
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
123
80
|
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
124
81
|
provider = "anthropic";
|
|
82
|
+
} else if (process.env.GOOGLE_API_KEY) {
|
|
83
|
+
apiKey = process.env.GOOGLE_API_KEY;
|
|
84
|
+
provider = "google";
|
|
125
85
|
}
|
|
126
86
|
}
|
|
127
87
|
this.configData.apiKey = apiKey;
|
|
@@ -133,12 +93,12 @@ var init_config = __esm({
|
|
|
133
93
|
return this.configData;
|
|
134
94
|
}
|
|
135
95
|
save(rootDir, updates) {
|
|
136
|
-
const configDir =
|
|
96
|
+
const configDir = path5.join(rootDir, "data");
|
|
137
97
|
if (!this.configPath) {
|
|
138
|
-
this.configPath =
|
|
98
|
+
this.configPath = path5.join(rootDir, "data", "config.json");
|
|
139
99
|
}
|
|
140
|
-
if (!
|
|
141
|
-
|
|
100
|
+
if (!fs5.existsSync(configDir)) {
|
|
101
|
+
fs5.mkdirSync(configDir, { recursive: true });
|
|
142
102
|
}
|
|
143
103
|
if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
|
|
144
104
|
if (updates.provider !== void 0) this.configData.provider = updates.provider;
|
|
@@ -160,7 +120,7 @@ var init_config = __esm({
|
|
|
160
120
|
this.configData.scheduledJobs = updates.scheduledJobs;
|
|
161
121
|
}
|
|
162
122
|
try {
|
|
163
|
-
|
|
123
|
+
fs5.writeFileSync(
|
|
164
124
|
this.configPath,
|
|
165
125
|
JSON.stringify(this.configData, null, 2),
|
|
166
126
|
"utf-8"
|
|
@@ -171,6 +131,137 @@ var init_config = __esm({
|
|
|
171
131
|
}
|
|
172
132
|
};
|
|
173
133
|
configManager = ConfigManager.getInstance();
|
|
134
|
+
ConfigFileAuthBackend = class {
|
|
135
|
+
constructor(configPath) {
|
|
136
|
+
this.configPath = configPath;
|
|
137
|
+
}
|
|
138
|
+
ensureFile() {
|
|
139
|
+
const dir = path5.dirname(this.configPath);
|
|
140
|
+
if (!fs5.existsSync(dir)) {
|
|
141
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
if (!fs5.existsSync(this.configPath)) {
|
|
144
|
+
fs5.writeFileSync(this.configPath, "{}", "utf-8");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
readAuthJson() {
|
|
148
|
+
this.ensureFile();
|
|
149
|
+
try {
|
|
150
|
+
const raw = fs5.readFileSync(this.configPath, "utf-8");
|
|
151
|
+
const config = JSON.parse(raw);
|
|
152
|
+
if (config._auth && typeof config._auth === "object") {
|
|
153
|
+
return JSON.stringify(config._auth);
|
|
154
|
+
}
|
|
155
|
+
return void 0;
|
|
156
|
+
} catch {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
writeAuthJson(authJson) {
|
|
161
|
+
this.ensureFile();
|
|
162
|
+
try {
|
|
163
|
+
const raw = fs5.readFileSync(this.configPath, "utf-8");
|
|
164
|
+
const config = JSON.parse(raw);
|
|
165
|
+
config._auth = JSON.parse(authJson);
|
|
166
|
+
fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
167
|
+
} catch {
|
|
168
|
+
const config = { _auth: JSON.parse(authJson) };
|
|
169
|
+
fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
withLock(fn) {
|
|
173
|
+
const current = this.readAuthJson();
|
|
174
|
+
const { result, next } = fn(current);
|
|
175
|
+
if (next !== void 0) {
|
|
176
|
+
this.writeAuthJson(next);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
async withLockAsync(fn) {
|
|
181
|
+
const current = this.readAuthJson();
|
|
182
|
+
const { result, next } = await fn(current);
|
|
183
|
+
if (next !== void 0) {
|
|
184
|
+
this.writeAuthJson(next);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// src/runtime/adapters/attachment-utils.ts
|
|
193
|
+
import fs6 from "fs";
|
|
194
|
+
import path6 from "path";
|
|
195
|
+
import { pipeline } from "stream/promises";
|
|
196
|
+
import { Readable } from "stream";
|
|
197
|
+
function getAttachmentDir(rootDir, channelId) {
|
|
198
|
+
return path6.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
|
|
199
|
+
}
|
|
200
|
+
function sanitizeFilename(name) {
|
|
201
|
+
return name.replace(/[/\\:*?"<>|]/g, "_").replace(/\s+/g, "_");
|
|
202
|
+
}
|
|
203
|
+
async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mimeType, headers) {
|
|
204
|
+
const dir = getAttachmentDir(rootDir, channelId);
|
|
205
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
206
|
+
const ts = Date.now();
|
|
207
|
+
const safeName = sanitizeFilename(filename);
|
|
208
|
+
const storedName = `${ts}-${safeName}`;
|
|
209
|
+
const fullPath = path6.join(dir, storedName);
|
|
210
|
+
const response = await fetch(url, { headers });
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Failed to download attachment from ${url}: ${response.status} ${response.statusText}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const body = response.body;
|
|
217
|
+
if (!body) {
|
|
218
|
+
throw new Error(`Empty response body when downloading ${url}`);
|
|
219
|
+
}
|
|
220
|
+
const nodeStream = Readable.fromWeb(body);
|
|
221
|
+
const writeStream = fs6.createWriteStream(fullPath);
|
|
222
|
+
await pipeline(nodeStream, writeStream);
|
|
223
|
+
const stats = fs6.statSync(fullPath);
|
|
224
|
+
const detectedMime = mimeType || response.headers.get("content-type")?.split(";")[0] || void 0;
|
|
225
|
+
return {
|
|
226
|
+
filename,
|
|
227
|
+
localPath: fullPath,
|
|
228
|
+
mimeType: detectedMime,
|
|
229
|
+
size: stats.size
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function isImageMime(mimeType) {
|
|
233
|
+
return !!mimeType && mimeType.startsWith("image/");
|
|
234
|
+
}
|
|
235
|
+
function formatSize(bytes) {
|
|
236
|
+
if (bytes === void 0 || bytes === null) return "";
|
|
237
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
238
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
239
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
240
|
+
}
|
|
241
|
+
function formatAttachmentsPrompt(attachments) {
|
|
242
|
+
if (attachments.length === 0) return "";
|
|
243
|
+
const lines = attachments.map((a) => {
|
|
244
|
+
const meta = [a.mimeType, formatSize(a.size)].filter(Boolean).join(", ");
|
|
245
|
+
return `- ${a.filename} (${meta}) \u2192 ${a.localPath}`;
|
|
246
|
+
});
|
|
247
|
+
return `[Attachments]
|
|
248
|
+
${lines.join("\n")}`;
|
|
249
|
+
}
|
|
250
|
+
function attachmentsToImageContent(attachments) {
|
|
251
|
+
return attachments.filter((a) => isImageMime(a.mimeType)).map((a) => {
|
|
252
|
+
const buffer = fs6.readFileSync(a.localPath);
|
|
253
|
+
return {
|
|
254
|
+
type: "image",
|
|
255
|
+
data: buffer.toString("base64"),
|
|
256
|
+
mimeType: a.mimeType || "image/png"
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
var ATTACHMENTS_DIR;
|
|
261
|
+
var init_attachment_utils = __esm({
|
|
262
|
+
"src/runtime/adapters/attachment-utils.ts"() {
|
|
263
|
+
"use strict";
|
|
264
|
+
ATTACHMENTS_DIR = "attachments";
|
|
174
265
|
}
|
|
175
266
|
});
|
|
176
267
|
|
|
@@ -2057,9 +2148,10 @@ import { createServer } from "http";
|
|
|
2057
2148
|
import { exec } from "child_process";
|
|
2058
2149
|
|
|
2059
2150
|
// src/runtime/agent.ts
|
|
2151
|
+
init_config();
|
|
2060
2152
|
init_attachment_utils();
|
|
2061
|
-
import
|
|
2062
|
-
import
|
|
2153
|
+
import path8 from "path";
|
|
2154
|
+
import fs8 from "fs";
|
|
2063
2155
|
import { fileURLToPath } from "url";
|
|
2064
2156
|
import {
|
|
2065
2157
|
AuthStorage,
|
|
@@ -2071,8 +2163,8 @@ import {
|
|
|
2071
2163
|
} from "@mariozechner/pi-coding-agent";
|
|
2072
2164
|
|
|
2073
2165
|
// src/runtime/tools/send-file-tool.ts
|
|
2074
|
-
import
|
|
2075
|
-
import
|
|
2166
|
+
import fs7 from "fs";
|
|
2167
|
+
import path7 from "path";
|
|
2076
2168
|
|
|
2077
2169
|
// node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
2078
2170
|
var value_exports = {};
|
|
@@ -4712,7 +4804,7 @@ var MIME_BY_EXT = {
|
|
|
4712
4804
|
".ogg": "audio/ogg"
|
|
4713
4805
|
};
|
|
4714
4806
|
function detectMimeType(filePath) {
|
|
4715
|
-
const ext =
|
|
4807
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
4716
4808
|
return MIME_BY_EXT[ext];
|
|
4717
4809
|
}
|
|
4718
4810
|
function createSendFileTool(fileOutputCallbackRef) {
|
|
@@ -4724,13 +4816,13 @@ function createSendFileTool(fileOutputCallbackRef) {
|
|
|
4724
4816
|
parameters: SendFileParams,
|
|
4725
4817
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
4726
4818
|
const { filePath, caption } = params;
|
|
4727
|
-
if (!
|
|
4819
|
+
if (!fs7.existsSync(filePath)) {
|
|
4728
4820
|
return {
|
|
4729
4821
|
content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
|
|
4730
4822
|
details: void 0
|
|
4731
4823
|
};
|
|
4732
4824
|
}
|
|
4733
|
-
const stats =
|
|
4825
|
+
const stats = fs7.statSync(filePath);
|
|
4734
4826
|
if (!stats.isFile()) {
|
|
4735
4827
|
return {
|
|
4736
4828
|
content: [
|
|
@@ -4739,7 +4831,7 @@ function createSendFileTool(fileOutputCallbackRef) {
|
|
|
4739
4831
|
details: void 0
|
|
4740
4832
|
};
|
|
4741
4833
|
}
|
|
4742
|
-
const filename =
|
|
4834
|
+
const filename = path7.basename(filePath);
|
|
4743
4835
|
const mimeType = detectMimeType(filePath);
|
|
4744
4836
|
const callback = fileOutputCallbackRef.current;
|
|
4745
4837
|
if (callback) {
|
|
@@ -4925,24 +5017,24 @@ var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
|
|
|
4925
5017
|
new URL("../templates/builtin-skills/skill-creator", import.meta.url)
|
|
4926
5018
|
);
|
|
4927
5019
|
function materializeBuiltinSkillCreator(rootDir, skillsPath) {
|
|
4928
|
-
if (!
|
|
5020
|
+
if (!fs8.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
|
|
4929
5021
|
log(
|
|
4930
5022
|
`[PackAgent] Built-in skill-creator template missing: ${BUILTIN_SKILL_CREATOR_TEMPLATE_DIR}`
|
|
4931
5023
|
);
|
|
4932
5024
|
return null;
|
|
4933
5025
|
}
|
|
4934
|
-
const packConfigPath =
|
|
4935
|
-
const skillDir =
|
|
4936
|
-
const skillPath =
|
|
5026
|
+
const packConfigPath = path8.resolve(rootDir, "skillpack.json");
|
|
5027
|
+
const skillDir = path8.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
|
|
5028
|
+
const skillPath = path8.join(skillDir, "SKILL.md");
|
|
4937
5029
|
const renderTemplate = (content) => content.replaceAll("{{SKILLS_PATH}}", skillsPath).replaceAll("{{PACK_CONFIG_PATH}}", packConfigPath);
|
|
4938
5030
|
const copyDir = (srcDir, destDir) => {
|
|
4939
|
-
|
|
4940
|
-
for (const entry of
|
|
5031
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
5032
|
+
for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
|
|
4941
5033
|
if (entry.name === ".DS_Store") {
|
|
4942
5034
|
continue;
|
|
4943
5035
|
}
|
|
4944
|
-
const srcPath =
|
|
4945
|
-
const destPath =
|
|
5036
|
+
const srcPath = path8.join(srcDir, entry.name);
|
|
5037
|
+
const destPath = path8.join(destDir, entry.name);
|
|
4946
5038
|
if (entry.isDirectory()) {
|
|
4947
5039
|
copyDir(srcPath, destPath);
|
|
4948
5040
|
continue;
|
|
@@ -4951,17 +5043,17 @@ function materializeBuiltinSkillCreator(rootDir, skillsPath) {
|
|
|
4951
5043
|
continue;
|
|
4952
5044
|
}
|
|
4953
5045
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".py")) {
|
|
4954
|
-
const content =
|
|
4955
|
-
|
|
5046
|
+
const content = fs8.readFileSync(srcPath, "utf-8");
|
|
5047
|
+
fs8.writeFileSync(destPath, renderTemplate(content), "utf-8");
|
|
4956
5048
|
continue;
|
|
4957
5049
|
}
|
|
4958
|
-
|
|
5050
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
4959
5051
|
}
|
|
4960
5052
|
};
|
|
4961
|
-
if (!
|
|
5053
|
+
if (!fs8.existsSync(skillDir)) {
|
|
4962
5054
|
copyDir(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR, skillDir);
|
|
4963
5055
|
}
|
|
4964
|
-
if (!
|
|
5056
|
+
if (!fs8.existsSync(skillPath)) {
|
|
4965
5057
|
log(
|
|
4966
5058
|
`[PackAgent] Materialized built-in skill-creator but SKILL.md is missing: ${skillPath}`
|
|
4967
5059
|
);
|
|
@@ -5014,8 +5106,29 @@ var PackAgent = class {
|
|
|
5014
5106
|
current: null
|
|
5015
5107
|
};
|
|
5016
5108
|
schedulerRef = { current: null };
|
|
5109
|
+
authStorage;
|
|
5017
5110
|
constructor(options) {
|
|
5018
5111
|
this.options = options;
|
|
5112
|
+
const configPath = path8.resolve(options.rootDir, "data", "config.json");
|
|
5113
|
+
const backend = new ConfigFileAuthBackend(configPath);
|
|
5114
|
+
this.authStorage = AuthStorage.fromStorage(backend);
|
|
5115
|
+
const providerMeta = SUPPORTED_PROVIDERS[options.provider];
|
|
5116
|
+
if (providerMeta?.authType === "api_key" && options.apiKey) {
|
|
5117
|
+
this.authStorage.setRuntimeApiKey(options.provider, options.apiKey);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
/** Get the shared AuthStorage instance (used by OAuth API endpoints) */
|
|
5121
|
+
getAuthStorage() {
|
|
5122
|
+
return this.authStorage;
|
|
5123
|
+
}
|
|
5124
|
+
/** Update runtime auth when provider/apiKey changes */
|
|
5125
|
+
updateAuth(provider, apiKey) {
|
|
5126
|
+
this.authStorage.removeRuntimeApiKey(this.options.provider);
|
|
5127
|
+
this.options.provider = provider;
|
|
5128
|
+
if (apiKey) {
|
|
5129
|
+
this.options.apiKey = apiKey;
|
|
5130
|
+
this.authStorage.setRuntimeApiKey(provider, apiKey);
|
|
5131
|
+
}
|
|
5019
5132
|
}
|
|
5020
5133
|
/**
|
|
5021
5134
|
* Inject scheduler reference (called by server.ts after adapter init).
|
|
@@ -5039,35 +5152,32 @@ var PackAgent = class {
|
|
|
5039
5152
|
const pendingCreation = this.pendingSessionCreations.get(channelId);
|
|
5040
5153
|
if (pendingCreation) return pendingCreation;
|
|
5041
5154
|
const createSessionPromise = (async () => {
|
|
5042
|
-
const {
|
|
5043
|
-
const authStorage =
|
|
5044
|
-
[provider]: { type: "api_key", key: apiKey }
|
|
5045
|
-
});
|
|
5046
|
-
authStorage.setRuntimeApiKey(provider, apiKey);
|
|
5155
|
+
const { rootDir, provider, modelId, baseUrl } = this.options;
|
|
5156
|
+
const authStorage = this.authStorage;
|
|
5047
5157
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
5048
5158
|
const resolvedModel = modelRegistry.find(provider, modelId);
|
|
5049
5159
|
const model = resolvedModel && baseUrl ? { ...resolvedModel, baseUrl } : resolvedModel;
|
|
5050
5160
|
if (resolvedModel && baseUrl) {
|
|
5051
5161
|
log(`[PackAgent] Overriding ${provider}/${modelId} baseUrl -> ${baseUrl}`);
|
|
5052
5162
|
}
|
|
5053
|
-
const sessionDir =
|
|
5163
|
+
const sessionDir = path8.resolve(
|
|
5054
5164
|
rootDir,
|
|
5055
5165
|
"data",
|
|
5056
5166
|
"sessions",
|
|
5057
5167
|
channelId
|
|
5058
5168
|
);
|
|
5059
|
-
|
|
5169
|
+
fs8.mkdirSync(sessionDir, { recursive: true });
|
|
5060
5170
|
const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
|
|
5061
5171
|
log(`[PackAgent] Session dir: ${sessionDir}`);
|
|
5062
|
-
const workspaceDir =
|
|
5172
|
+
const workspaceDir = path8.resolve(
|
|
5063
5173
|
rootDir,
|
|
5064
5174
|
"data",
|
|
5065
5175
|
"workspaces",
|
|
5066
5176
|
channelId
|
|
5067
5177
|
);
|
|
5068
|
-
|
|
5178
|
+
fs8.mkdirSync(workspaceDir, { recursive: true });
|
|
5069
5179
|
log(`[PackAgent] Workspace dir: ${workspaceDir}`);
|
|
5070
|
-
const skillsPath =
|
|
5180
|
+
const skillsPath = path8.resolve(rootDir, "skills");
|
|
5071
5181
|
log(`[PackAgent] Loading skills from: ${skillsPath}`);
|
|
5072
5182
|
const materializedSkillCreator = materializeBuiltinSkillCreator(
|
|
5073
5183
|
rootDir,
|
|
@@ -5249,9 +5359,9 @@ ${text}`;
|
|
|
5249
5359
|
this.channels.delete(channelId);
|
|
5250
5360
|
}
|
|
5251
5361
|
const { rootDir } = this.options;
|
|
5252
|
-
const sessionDir =
|
|
5253
|
-
if (
|
|
5254
|
-
|
|
5362
|
+
const sessionDir = path8.resolve(rootDir, "data", "sessions", channelId);
|
|
5363
|
+
if (fs8.existsSync(sessionDir)) {
|
|
5364
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
5255
5365
|
log(`[PackAgent] Cleared session dir: ${sessionDir}`);
|
|
5256
5366
|
}
|
|
5257
5367
|
return {
|
|
@@ -5340,6 +5450,9 @@ var WebAdapter = class {
|
|
|
5340
5450
|
app.get("/api/config", (_req, res) => {
|
|
5341
5451
|
const config = getPackConfig(rootDir);
|
|
5342
5452
|
const conf = configManager.getConfig();
|
|
5453
|
+
const currentProvider2 = conf.provider || "openai";
|
|
5454
|
+
const providerMeta = SUPPORTED_PROVIDERS[currentProvider2];
|
|
5455
|
+
const oauthConnected = providerMeta?.authType === "oauth" ? agent.getAuthStorage().hasAuth(currentProvider2) : false;
|
|
5343
5456
|
res.json({
|
|
5344
5457
|
name: config.name,
|
|
5345
5458
|
description: config.description,
|
|
@@ -5347,9 +5460,11 @@ var WebAdapter = class {
|
|
|
5347
5460
|
skills: config.skills || [],
|
|
5348
5461
|
hasApiKey: !!conf.apiKey,
|
|
5349
5462
|
apiKey: conf.apiKey || "",
|
|
5350
|
-
provider:
|
|
5463
|
+
provider: currentProvider2,
|
|
5351
5464
|
baseUrl: conf.baseUrl || "",
|
|
5352
|
-
adapters: conf.adapters || {}
|
|
5465
|
+
adapters: conf.adapters || {},
|
|
5466
|
+
supportedProviders: SUPPORTED_PROVIDERS,
|
|
5467
|
+
oauthConnected
|
|
5353
5468
|
});
|
|
5354
5469
|
});
|
|
5355
5470
|
app.get("/api/skills", (_req, res) => {
|
|
@@ -5375,16 +5490,62 @@ var WebAdapter = class {
|
|
|
5375
5490
|
updates.adapters = adapters;
|
|
5376
5491
|
}
|
|
5377
5492
|
configManager.save(rootDir, updates);
|
|
5378
|
-
|
|
5379
|
-
const
|
|
5493
|
+
agent.updateAuth(currentProvider, apiKey);
|
|
5494
|
+
const afterConfig = configManager.getConfig();
|
|
5495
|
+
const requiresRestart = getRuntimeConfigSignature(beforeConfig) !== getRuntimeConfigSignature(afterConfig);
|
|
5380
5496
|
res.json({
|
|
5381
|
-
|
|
5382
|
-
provider: newConf.provider,
|
|
5383
|
-
baseUrl: newConf.baseUrl || "",
|
|
5384
|
-
adapters: newConf.adapters,
|
|
5497
|
+
...afterConfig,
|
|
5385
5498
|
requiresRestart
|
|
5386
5499
|
});
|
|
5387
5500
|
});
|
|
5501
|
+
app.post("/api/oauth/login", async (req, res) => {
|
|
5502
|
+
const { provider } = req.body;
|
|
5503
|
+
const meta = SUPPORTED_PROVIDERS[provider];
|
|
5504
|
+
if (!meta || meta.authType !== "oauth") {
|
|
5505
|
+
return res.status(400).json({ error: "Provider does not support OAuth" });
|
|
5506
|
+
}
|
|
5507
|
+
try {
|
|
5508
|
+
const authStorage = agent.getAuthStorage();
|
|
5509
|
+
let authUrl = "";
|
|
5510
|
+
const loginPromise = authStorage.login(provider, {
|
|
5511
|
+
onAuth: (info) => {
|
|
5512
|
+
authUrl = info.url;
|
|
5513
|
+
},
|
|
5514
|
+
onPrompt: async (prompt) => {
|
|
5515
|
+
return "";
|
|
5516
|
+
},
|
|
5517
|
+
onProgress: (msg) => {
|
|
5518
|
+
console.log(`[OAuth] ${provider} login progress: ${msg}`);
|
|
5519
|
+
}
|
|
5520
|
+
});
|
|
5521
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
5522
|
+
if (authUrl) {
|
|
5523
|
+
res.json({ status: "pending", authUrl });
|
|
5524
|
+
} else {
|
|
5525
|
+
res.json({ status: "pending" });
|
|
5526
|
+
}
|
|
5527
|
+
loginPromise.catch((err) => {
|
|
5528
|
+
console.error(`[OAuth] ${provider} login error:`, err);
|
|
5529
|
+
});
|
|
5530
|
+
} catch (err) {
|
|
5531
|
+
res.status(500).json({ error: String(err) });
|
|
5532
|
+
}
|
|
5533
|
+
});
|
|
5534
|
+
app.get("/api/oauth/status", (_req, res) => {
|
|
5535
|
+
const conf = configManager.getConfig();
|
|
5536
|
+
const provider = conf.provider || "openai";
|
|
5537
|
+
const meta = SUPPORTED_PROVIDERS[provider];
|
|
5538
|
+
if (!meta || meta.authType !== "oauth") {
|
|
5539
|
+
return res.json({ connected: false });
|
|
5540
|
+
}
|
|
5541
|
+
const connected = agent.getAuthStorage().hasAuth(provider);
|
|
5542
|
+
res.json({ connected, provider });
|
|
5543
|
+
});
|
|
5544
|
+
app.post("/api/oauth/logout", (req, res) => {
|
|
5545
|
+
const { provider } = req.body;
|
|
5546
|
+
agent.getAuthStorage().logout(provider);
|
|
5547
|
+
res.json({ success: true });
|
|
5548
|
+
});
|
|
5388
5549
|
app.post("/api/runtime/restart", async (_req, res) => {
|
|
5389
5550
|
const result = await lifecycle.requestRestart("web");
|
|
5390
5551
|
res.status(202).json(result);
|
|
@@ -5508,8 +5669,10 @@ var WebAdapter = class {
|
|
|
5508
5669
|
`http://${request.headers.host || "127.0.0.1"}`
|
|
5509
5670
|
);
|
|
5510
5671
|
const _reqProvider = url.searchParams.get("provider") || currentProvider;
|
|
5511
|
-
|
|
5512
|
-
|
|
5672
|
+
const providerMeta = SUPPORTED_PROVIDERS[_reqProvider];
|
|
5673
|
+
const hasAuth = providerMeta?.authType === "oauth" ? agent.getAuthStorage().hasAuth(_reqProvider) : !!apiKey;
|
|
5674
|
+
if (!hasAuth) {
|
|
5675
|
+
ws.send(JSON.stringify({ error: "Please configure authentication first" }));
|
|
5513
5676
|
ws.close();
|
|
5514
5677
|
return;
|
|
5515
5678
|
}
|
|
@@ -5819,7 +5982,7 @@ async function startServer(options) {
|
|
|
5819
5982
|
const canonicalRootDir = canonicalizeDir(rootDir);
|
|
5820
5983
|
const packConfig = loadConfig(canonicalRootDir);
|
|
5821
5984
|
const baseUrl = dataConfig.baseUrl?.trim() || void 0;
|
|
5822
|
-
const modelId = provider
|
|
5985
|
+
const modelId = SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId;
|
|
5823
5986
|
const packageRoot = path12.resolve(__dirname, "..");
|
|
5824
5987
|
const webDir = fs13.existsSync(path12.join(rootDir, "web")) ? path12.join(rootDir, "web") : path12.join(packageRoot, "web");
|
|
5825
5988
|
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.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,33 +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
|
-
|
|
198
|
+
populateForm();
|
|
140
199
|
state.restartRequired = !!res.requiresRestart;
|
|
141
|
-
|
|
142
200
|
updateApiKeyButton();
|
|
143
201
|
|
|
144
202
|
if (res.requiresRestart) {
|
|
145
|
-
setStatus(
|
|
146
|
-
"API key saved. Restart service to apply changes.",
|
|
147
|
-
"warning",
|
|
148
|
-
);
|
|
203
|
+
setStatus("Settings saved. Restart service to apply changes.", "warning");
|
|
149
204
|
updateRestartButton(true);
|
|
150
205
|
} else {
|
|
151
|
-
setStatus("
|
|
152
|
-
// 延迟关闭让用户看到成功消息
|
|
206
|
+
setStatus("Settings saved successfully", "success");
|
|
153
207
|
setTimeout(() => close(), 1200);
|
|
154
208
|
}
|
|
155
209
|
} catch (err) {
|
|
@@ -159,6 +213,107 @@ async function handleSave() {
|
|
|
159
213
|
}
|
|
160
214
|
}
|
|
161
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
|
+
|
|
162
317
|
async function handleRestart() {
|
|
163
318
|
if (!restartBtn) return;
|
|
164
319
|
|
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
|
-
}
|