hermes-web-ui 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -12
- package/dist/assets/ChannelsView-BWEvTQwM.css +1 -0
- package/dist/assets/ChannelsView-sOGWQqBR.js +1 -0
- package/dist/assets/ChatView-BYMsYkQ1.css +1 -0
- package/dist/assets/ChatView-Ck4IHVIc.js +142 -0
- package/dist/assets/Close-B7KfM3jl.js +45 -0
- package/dist/assets/FormItem-BdZ_3RxK.js +110 -0
- package/dist/assets/Input-C6bxvAIj.js +234 -0
- package/dist/assets/InputNumber-eI175yNc.js +13 -0
- package/dist/assets/JobsView-DkwaDky6.css +1 -0
- package/dist/assets/JobsView-wXKPSc8W.js +2 -0
- package/dist/assets/LogsView-B2TeFIUX.css +1 -0
- package/dist/assets/LogsView-Ck9HLcnc.js +1 -0
- package/dist/assets/MarkdownRenderer-B_Fo6783.js +23 -0
- package/dist/assets/MemoryView-BUR_XL9x.js +5 -0
- package/dist/assets/MemoryView-CHeaa1-C.css +1 -0
- package/dist/assets/Modal-DYN0wyo2.js +232 -0
- package/dist/assets/ModelsView-CLdtVeiJ.css +1 -0
- package/dist/assets/ModelsView-CkEpCpW4.js +1 -0
- package/dist/assets/Popover-BgNi3Q2J.js +117 -0
- package/dist/assets/Select-DevZ62i5.js +340 -0
- package/dist/assets/SettingRow-HrI0c-1s.css +1 -0
- package/dist/assets/SettingRow-bBgfSbZT.js +102 -0
- package/dist/assets/SettingsView-Cl1ts-YY.css +1 -0
- package/dist/assets/SettingsView-D_qQMAqN.js +352 -0
- package/dist/assets/SkillsView-BvNhRbMq.css +1 -0
- package/dist/assets/SkillsView-C68Fr3t5.js +1 -0
- package/dist/assets/Spin-C1dDkPv8.js +43 -0
- package/dist/assets/Suffix-B6bqA1w3.js +101 -0
- package/dist/assets/Tag-BBWtv463.js +71 -0
- package/dist/assets/Tooltip-D-FlJlcu.js +1 -0
- package/dist/assets/_plugin-vue_export-helper-AI_aJZ2_.js +49 -0
- package/dist/assets/app-DYgUa8EE.js +1 -0
- package/dist/assets/app-DZPFLxKB.js +1 -0
- package/dist/assets/chat-C87W9H3D.js +6 -0
- package/dist/assets/{context-DW8F1iIn.js → context-aD0DoQuE.js} +14 -14
- package/dist/assets/{index-DwVgwUIX.css → index-B7NziQ6I.css} +1 -1
- package/dist/assets/index-BxFv1xVi.js +307 -0
- package/dist/assets/jobs-D_98yklp.js +1 -0
- package/dist/assets/pinia-B7LrvQDh.js +1 -0
- package/dist/assets/preload-helper-D4M6sveU.js +1 -0
- package/dist/assets/{skills-CkDtgKB5.js → skills-nROrsAxi.js} +1 -1
- package/dist/assets/use-message-rwmUYd8q.js +1 -0
- package/dist/assets/vue.runtime.esm-bundler-LsUadeZi.js +3 -0
- package/dist/index.html +16 -16
- package/dist/server/index.js +4 -0
- package/dist/server/routes/config.d.ts +2 -0
- package/dist/server/routes/config.js +314 -0
- package/dist/server/routes/filesystem.js +121 -22
- package/dist/server/routes/weixin.d.ts +2 -0
- package/dist/server/routes/weixin.js +131 -0
- package/dist/server/shared/providers.d.ts +13 -0
- package/dist/server/shared/providers.js +210 -0
- package/package.json +6 -1
- package/dist/assets/ChatView-DmXZ5Q3Z.js +0 -18
- package/dist/assets/ChatView-cCVO1N9F.css +0 -1
- package/dist/assets/Dropdown-8pId5Qsj.js +0 -125
- package/dist/assets/Input-BZKSQ403.js +0 -234
- package/dist/assets/JobsView-BhwwXuLt.css +0 -1
- package/dist/assets/JobsView-CQf4Y7Mw.js +0 -123
- package/dist/assets/LogsView-BN_TkDPi.css +0 -1
- package/dist/assets/LogsView-CKyL3UT5.js +0 -1
- package/dist/assets/MarkdownRenderer-DNP-kPA8.js +0 -23
- package/dist/assets/MemoryView-BBwqM3vf.js +0 -5
- package/dist/assets/MemoryView-CK0PemlP.css +0 -1
- package/dist/assets/Modal-BtRuxNI4.js +0 -232
- package/dist/assets/Popover-C2CJscsj.js +0 -161
- package/dist/assets/Select-CithN5ti.js +0 -410
- package/dist/assets/SettingsView-BzBMKaLz.css +0 -1
- package/dist/assets/SettingsView-D1hOGA0Z.js +0 -1005
- package/dist/assets/SkillsView-86Z-HE_X.css +0 -1
- package/dist/assets/SkillsView-BfnrX5TQ.js +0 -1
- package/dist/assets/Spin-7g8GCzi5.js +0 -43
- package/dist/assets/Suffix-DI9irQ4f.js +0 -101
- package/dist/assets/Tooltip-BdrvORgU.js +0 -1
- package/dist/assets/_plugin-vue_export-helper-BrYOyDjU.js +0 -47
- package/dist/assets/app-JRkV5-Ft.js +0 -1
- package/dist/assets/chat-Cgdof9SF.js +0 -6
- package/dist/assets/index-DP1JeABS.js +0 -307
- package/dist/assets/jobs-DJ8ETzok.js +0 -1
- package/dist/assets/keysOf-Dvq9k1rv.js +0 -1
- package/dist/assets/pinia-tE0RcsDr.js +0 -1
- package/dist/assets/runtime-core.esm-bundler-yNW65ghW.js +0 -1
- package/dist/assets/use-message-Btr-O4Ih.js +0 -1
- /package/dist/assets/{client-kwQ0ijpp.js → client-Df_SqoZl.js} +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.configRoutes = void 0;
|
|
7
|
+
const router_1 = __importDefault(require("@koa/router"));
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
|
+
const promises_2 = require("fs/promises");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
13
|
+
const hermes_cli_1 = require("../services/hermes-cli");
|
|
14
|
+
// Platform sections that require gateway restart after config change
|
|
15
|
+
const PLATFORM_SECTIONS = new Set([
|
|
16
|
+
'telegram', 'discord', 'slack', 'whatsapp', 'matrix',
|
|
17
|
+
'weixin', 'wecom', 'feishu', 'dingtalk',
|
|
18
|
+
]);
|
|
19
|
+
const configPath = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes/config.yaml');
|
|
20
|
+
const envPath = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes/.env');
|
|
21
|
+
// Env var → (platform, configPath in PlatformConfig) mapping
|
|
22
|
+
// Matches hermes _apply_env_overrides() in gateway/config.py
|
|
23
|
+
const envPlatformMap = {
|
|
24
|
+
TELEGRAM_BOT_TOKEN: ['telegram', 'token'],
|
|
25
|
+
DISCORD_BOT_TOKEN: ['discord', 'token'],
|
|
26
|
+
SLACK_BOT_TOKEN: ['slack', 'token'],
|
|
27
|
+
MATRIX_ACCESS_TOKEN: ['matrix', 'token'],
|
|
28
|
+
MATRIX_HOMESERVER: ['matrix', 'extra.homeserver'],
|
|
29
|
+
FEISHU_APP_ID: ['feishu', 'extra.app_id'],
|
|
30
|
+
FEISHU_APP_SECRET: ['feishu', 'extra.app_secret'],
|
|
31
|
+
DINGTALK_CLIENT_ID: ['dingtalk', 'extra.client_id'],
|
|
32
|
+
DINGTALK_CLIENT_SECRET: ['dingtalk', 'extra.client_secret'],
|
|
33
|
+
// DingTalk has no _apply_env_overrides entry in hermes;
|
|
34
|
+
// the adapter reads these env vars directly at runtime.
|
|
35
|
+
DINGTALK_APP_KEY: ['dingtalk', 'extra.app_key'],
|
|
36
|
+
WECOM_BOT_ID: ['wecom', 'extra.bot_id'],
|
|
37
|
+
WECOM_SECRET: ['wecom', 'extra.secret'],
|
|
38
|
+
WEIXIN_TOKEN: ['weixin', 'token'],
|
|
39
|
+
WEIXIN_ACCOUNT_ID: ['weixin', 'extra.account_id'],
|
|
40
|
+
WEIXIN_BASE_URL: ['weixin', 'extra.base_url'],
|
|
41
|
+
WHATSAPP_ENABLED: ['whatsapp', 'enabled'],
|
|
42
|
+
};
|
|
43
|
+
// Reverse map: (platform, configPath) → env var
|
|
44
|
+
const platformEnvMap = {};
|
|
45
|
+
for (const [envVar, [platform, configPath]] of Object.entries(envPlatformMap)) {
|
|
46
|
+
if (!platformEnvMap[platform])
|
|
47
|
+
platformEnvMap[platform] = {};
|
|
48
|
+
platformEnvMap[platform][configPath] = envVar;
|
|
49
|
+
}
|
|
50
|
+
function parseEnv(raw) {
|
|
51
|
+
const env = {};
|
|
52
|
+
for (const line of raw.split('\n')) {
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
55
|
+
continue;
|
|
56
|
+
const eqIdx = trimmed.indexOf('=');
|
|
57
|
+
if (eqIdx === -1)
|
|
58
|
+
continue;
|
|
59
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
60
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
61
|
+
if (val)
|
|
62
|
+
env[key] = val;
|
|
63
|
+
}
|
|
64
|
+
return env;
|
|
65
|
+
}
|
|
66
|
+
function setNested(obj, path, value) {
|
|
67
|
+
const parts = path.split('.');
|
|
68
|
+
let cur = obj;
|
|
69
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
70
|
+
if (!cur[parts[i]])
|
|
71
|
+
cur[parts[i]] = {};
|
|
72
|
+
cur = cur[parts[i]];
|
|
73
|
+
}
|
|
74
|
+
cur[parts[parts.length - 1]] = value;
|
|
75
|
+
}
|
|
76
|
+
function getNested(obj, path) {
|
|
77
|
+
const parts = path.split('.');
|
|
78
|
+
let cur = obj;
|
|
79
|
+
for (const p of parts) {
|
|
80
|
+
if (!cur || typeof cur !== 'object')
|
|
81
|
+
return undefined;
|
|
82
|
+
cur = cur[p];
|
|
83
|
+
}
|
|
84
|
+
return cur;
|
|
85
|
+
}
|
|
86
|
+
async function readEnvPlatforms() {
|
|
87
|
+
try {
|
|
88
|
+
const raw = await (0, promises_1.readFile)(envPath, 'utf-8');
|
|
89
|
+
const env = parseEnv(raw);
|
|
90
|
+
const platforms = {};
|
|
91
|
+
for (const [envKey, [platform, cfgPath]] of Object.entries(envPlatformMap)) {
|
|
92
|
+
const val = env[envKey];
|
|
93
|
+
if (val === undefined || val === '')
|
|
94
|
+
continue;
|
|
95
|
+
if (!platforms[platform])
|
|
96
|
+
platforms[platform] = {};
|
|
97
|
+
let finalVal = val;
|
|
98
|
+
if (cfgPath === 'enabled')
|
|
99
|
+
finalVal = val === 'true';
|
|
100
|
+
setNested(platforms[platform], cfgPath, finalVal);
|
|
101
|
+
}
|
|
102
|
+
return platforms;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Write a KEY=value to .env (matching hermes save_env_value behavior)
|
|
109
|
+
// If value is empty, remove the line instead
|
|
110
|
+
async function saveEnvValue(key, value) {
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = await (0, promises_1.readFile)(envPath, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
raw = '';
|
|
117
|
+
}
|
|
118
|
+
const remove = !value;
|
|
119
|
+
const lines = raw.split('\n');
|
|
120
|
+
let found = false;
|
|
121
|
+
const result = [];
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
if (trimmed.startsWith('#')) {
|
|
125
|
+
// Check if there's a commented-out version of this key
|
|
126
|
+
if (trimmed.startsWith(`# ${key}=`)) {
|
|
127
|
+
if (!remove) {
|
|
128
|
+
result.push(`${key}=${value}`);
|
|
129
|
+
}
|
|
130
|
+
found = true;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
result.push(line);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const eqIdx = trimmed.indexOf('=');
|
|
138
|
+
if (eqIdx !== -1 && trimmed.slice(0, eqIdx).trim() === key) {
|
|
139
|
+
if (!remove) {
|
|
140
|
+
result.push(`${key}=${value}`);
|
|
141
|
+
}
|
|
142
|
+
found = true;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
result.push(line);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!found && !remove) {
|
|
150
|
+
result.push(`${key}=${value}`);
|
|
151
|
+
}
|
|
152
|
+
// Remove trailing empty lines, keep exactly one trailing newline
|
|
153
|
+
let output = result.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '') + '\n';
|
|
154
|
+
await (0, promises_1.writeFile)(envPath, output, 'utf-8');
|
|
155
|
+
// Set permissions to 0600 (owner only), matching hermes behavior
|
|
156
|
+
try {
|
|
157
|
+
await (0, promises_2.chmod)(envPath, 0o600);
|
|
158
|
+
}
|
|
159
|
+
catch { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
async function readConfig() {
|
|
162
|
+
const raw = await (0, promises_1.readFile)(configPath, 'utf-8');
|
|
163
|
+
return js_yaml_1.default.load(raw) || {};
|
|
164
|
+
}
|
|
165
|
+
async function writeConfig(data) {
|
|
166
|
+
await (0, promises_1.copyFile)(configPath, configPath + '.bak');
|
|
167
|
+
const yamlStr = js_yaml_1.default.dump(data, {
|
|
168
|
+
lineWidth: -1,
|
|
169
|
+
noRefs: true,
|
|
170
|
+
quotingType: '"',
|
|
171
|
+
forceQuotes: false,
|
|
172
|
+
});
|
|
173
|
+
await (0, promises_1.writeFile)(configPath, yamlStr, 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
exports.configRoutes = new router_1.default();
|
|
176
|
+
// GET /api/config — read config sections
|
|
177
|
+
exports.configRoutes.get('/api/config', async (ctx) => {
|
|
178
|
+
try {
|
|
179
|
+
const config = await readConfig();
|
|
180
|
+
// Merge .env platform credentials into platforms section
|
|
181
|
+
const envPlatforms = await readEnvPlatforms();
|
|
182
|
+
if (Object.keys(envPlatforms).length > 0) {
|
|
183
|
+
// Deep-merge: env values fill in missing, don't overwrite config.yaml
|
|
184
|
+
const existing = config.platforms || {};
|
|
185
|
+
for (const [platform, vals] of Object.entries(envPlatforms)) {
|
|
186
|
+
existing[platform] = { ...(existing[platform] || {}), ...vals };
|
|
187
|
+
}
|
|
188
|
+
config.platforms = existing;
|
|
189
|
+
}
|
|
190
|
+
const { section, sections } = ctx.query;
|
|
191
|
+
if (section) {
|
|
192
|
+
ctx.body = { [section]: config[section] || {} };
|
|
193
|
+
}
|
|
194
|
+
else if (sections) {
|
|
195
|
+
const keys = sections.split(',');
|
|
196
|
+
const result = {};
|
|
197
|
+
for (const key of keys) {
|
|
198
|
+
result[key.trim()] = config[key.trim()] || {};
|
|
199
|
+
}
|
|
200
|
+
ctx.body = result;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
ctx.body = config;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
ctx.status = 500;
|
|
208
|
+
ctx.body = { error: err.message };
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// PUT /api/config — update a config section (writes to config.yaml)
|
|
212
|
+
exports.configRoutes.put('/api/config', async (ctx) => {
|
|
213
|
+
const { section, values } = ctx.request.body;
|
|
214
|
+
if (!section || !values) {
|
|
215
|
+
ctx.status = 400;
|
|
216
|
+
ctx.body = { error: 'Missing section or values' };
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const config = await readConfig();
|
|
221
|
+
config[section] = { ...(config[section] || {}), ...values };
|
|
222
|
+
await writeConfig(config);
|
|
223
|
+
// Restart gateway for platform/channel config changes
|
|
224
|
+
if (PLATFORM_SECTIONS.has(section)) {
|
|
225
|
+
await (0, hermes_cli_1.restartGateway)();
|
|
226
|
+
}
|
|
227
|
+
ctx.body = { success: true };
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
ctx.status = 500;
|
|
231
|
+
ctx.body = { error: err.message };
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// PUT /api/config/credentials — save platform credentials to .env
|
|
235
|
+
// Body: { platform: string, values: Record<string, any> }
|
|
236
|
+
// values keys match PlatformConfig paths: 'token', 'extra.app_id', 'extra.app_secret', etc.
|
|
237
|
+
exports.configRoutes.put('/api/config/credentials', async (ctx) => {
|
|
238
|
+
const { platform, values } = ctx.request.body;
|
|
239
|
+
if (!platform || !values) {
|
|
240
|
+
ctx.status = 400;
|
|
241
|
+
ctx.body = { error: 'Missing platform or values' };
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const envMap = platformEnvMap[platform];
|
|
246
|
+
if (!envMap) {
|
|
247
|
+
ctx.status = 400;
|
|
248
|
+
ctx.body = { error: `Unknown platform: ${platform}` };
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Also clean up config.yaml platforms.<platform> to keep in sync
|
|
252
|
+
const config = await readConfig();
|
|
253
|
+
let configChanged = false;
|
|
254
|
+
// Flatten nested values: { extra: { app_id: '' } } → { 'extra.app_id': '' }
|
|
255
|
+
const flatValues = {};
|
|
256
|
+
for (const [key, val] of Object.entries(values)) {
|
|
257
|
+
if (key === 'extra' && val && typeof val === 'object') {
|
|
258
|
+
for (const [subKey, subVal] of Object.entries(val)) {
|
|
259
|
+
flatValues[`extra.${subKey}`] = subVal;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
flatValues[key] = val;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const [cfgPath, val] of Object.entries(flatValues)) {
|
|
267
|
+
const envVar = envMap[cfgPath];
|
|
268
|
+
if (!envVar)
|
|
269
|
+
continue;
|
|
270
|
+
if (val === undefined || val === null || val === '') {
|
|
271
|
+
await saveEnvValue(envVar, '');
|
|
272
|
+
// Remove from config.yaml too
|
|
273
|
+
const parts = cfgPath.split('.');
|
|
274
|
+
let obj = config.platforms?.[platform];
|
|
275
|
+
if (obj) {
|
|
276
|
+
if (parts.length === 1) {
|
|
277
|
+
delete obj[parts[0]];
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
let cur = obj;
|
|
281
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
282
|
+
if (!cur[parts[i]])
|
|
283
|
+
break;
|
|
284
|
+
cur = cur[parts[i]];
|
|
285
|
+
}
|
|
286
|
+
delete cur[parts[parts.length - 1]];
|
|
287
|
+
// Clean up empty extra
|
|
288
|
+
if (obj.extra && Object.keys(obj.extra).length === 0)
|
|
289
|
+
delete obj.extra;
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(obj).length === 0) {
|
|
292
|
+
if (!config.platforms)
|
|
293
|
+
config.platforms = {};
|
|
294
|
+
delete config.platforms[platform];
|
|
295
|
+
}
|
|
296
|
+
configChanged = true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
await saveEnvValue(envVar, String(val));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (configChanged) {
|
|
304
|
+
await writeConfig(config);
|
|
305
|
+
}
|
|
306
|
+
// Restart gateway for platform credential changes
|
|
307
|
+
await (0, hermes_cli_1.restartGateway)();
|
|
308
|
+
ctx.body = { success: true };
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
ctx.status = 500;
|
|
312
|
+
ctx.body = { error: err.message };
|
|
313
|
+
}
|
|
314
|
+
});
|
|
@@ -41,9 +41,15 @@ async function fetchProviderModels(baseUrl, apiKey) {
|
|
|
41
41
|
return [];
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
// --- Hardcoded model catalogs (single source: src/shared/providers.ts) ---
|
|
45
|
+
const providers_1 = require("../shared/providers");
|
|
46
|
+
const PROVIDER_MODEL_CATALOG = (0, providers_1.buildProviderModelMap)();
|
|
44
47
|
exports.fsRoutes = new router_1.default();
|
|
45
48
|
const hermesDir = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes');
|
|
46
49
|
// --- Helpers ---
|
|
50
|
+
function escapeRegExp(s) {
|
|
51
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
}
|
|
47
53
|
function extractDescription(content) {
|
|
48
54
|
// SKILL.md format: YAML frontmatter between --- delimiters, then markdown body
|
|
49
55
|
// Extract first non-empty, non-frontmatter, non-heading line as description
|
|
@@ -313,19 +319,32 @@ exports.fsRoutes.get('/api/available-models', async (ctx) => {
|
|
|
313
319
|
token: entry.access_token,
|
|
314
320
|
});
|
|
315
321
|
}
|
|
316
|
-
//
|
|
317
|
-
const results = await Promise.allSettled(endpoints.map(async (ep) => {
|
|
318
|
-
const models = await fetchProviderModels(ep.base_url, ep.token);
|
|
319
|
-
return { ...ep, models };
|
|
320
|
-
}));
|
|
322
|
+
// Resolve models: hardcoded catalog first, live probe as fallback
|
|
321
323
|
const groups = [];
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
324
|
+
const liveEndpoints = [];
|
|
325
|
+
for (const ep of endpoints) {
|
|
326
|
+
const catalogModels = PROVIDER_MODEL_CATALOG[ep.key];
|
|
327
|
+
if (catalogModels && catalogModels.length > 0) {
|
|
328
|
+
groups.push({ provider: ep.key, label: ep.label, base_url: ep.base_url, models: catalogModels });
|
|
326
329
|
}
|
|
327
|
-
else
|
|
328
|
-
|
|
330
|
+
else {
|
|
331
|
+
liveEndpoints.push(ep);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Only probe endpoints not in the catalog
|
|
335
|
+
if (liveEndpoints.length > 0) {
|
|
336
|
+
const results = await Promise.allSettled(liveEndpoints.map(async (ep) => {
|
|
337
|
+
const models = await fetchProviderModels(ep.base_url, ep.token);
|
|
338
|
+
return { ...ep, models };
|
|
339
|
+
}));
|
|
340
|
+
for (const result of results) {
|
|
341
|
+
if (result.status === 'fulfilled' && result.value.models.length > 0) {
|
|
342
|
+
const { key, label, base_url, models } = result.value;
|
|
343
|
+
groups.push({ provider: key, label, base_url, models });
|
|
344
|
+
}
|
|
345
|
+
else if (result.status === 'rejected') {
|
|
346
|
+
console.error(`[available-models] Failed: ${result.reason?.message || result.reason}`);
|
|
347
|
+
}
|
|
329
348
|
}
|
|
330
349
|
}
|
|
331
350
|
// Fallback: if no providers returned models, fall back to config.yaml parsing
|
|
@@ -384,16 +403,22 @@ exports.fsRoutes.put('/api/config/model', async (ctx) => {
|
|
|
384
403
|
});
|
|
385
404
|
// POST /api/config/providers
|
|
386
405
|
exports.fsRoutes.post('/api/config/providers', async (ctx) => {
|
|
387
|
-
const { name, base_url, api_key, model } = ctx.request.body;
|
|
406
|
+
const { name, base_url, api_key, model, providerKey } = ctx.request.body;
|
|
388
407
|
if (!name || !base_url || !model) {
|
|
389
408
|
ctx.status = 400;
|
|
390
409
|
ctx.body = { error: 'Missing name, base_url, or model' };
|
|
391
410
|
return;
|
|
392
411
|
}
|
|
412
|
+
if (!api_key) {
|
|
413
|
+
ctx.status = 400;
|
|
414
|
+
ctx.body = { error: 'Missing API key' };
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
393
417
|
try {
|
|
418
|
+
// 1. Write to config.yaml custom_providers
|
|
394
419
|
await (0, promises_1.copyFile)(configPath, configPath + '.bak');
|
|
395
420
|
let yaml = await safeReadFile(configPath) || '';
|
|
396
|
-
const newEntry = `- name: ${name}\n base_url: ${base_url}\n api_key: ${api_key
|
|
421
|
+
const newEntry = `- name: ${name}\n base_url: ${base_url}\n api_key: ${api_key}\n model: ${model}\n`;
|
|
397
422
|
if (/^custom_providers:/m.test(yaml)) {
|
|
398
423
|
yaml = yaml.replace(/^(custom_providers:)/m, `$1\n${newEntry}`);
|
|
399
424
|
}
|
|
@@ -401,6 +426,32 @@ exports.fsRoutes.post('/api/config/providers', async (ctx) => {
|
|
|
401
426
|
yaml = yaml.trimEnd() + `\n\ncustom_providers:\n${newEntry}\n`;
|
|
402
427
|
}
|
|
403
428
|
await (0, promises_1.writeFile)(configPath, yaml, 'utf-8');
|
|
429
|
+
// 2. Write to auth.json credential_pool so GET /api/available-models sees it immediately
|
|
430
|
+
const poolKey = providerKey
|
|
431
|
+
|| `custom:${name.trim().toLowerCase().replace(/ /g, '-')}`;
|
|
432
|
+
const auth = await loadAuthJson() || { credential_pool: {} };
|
|
433
|
+
if (!auth.credential_pool)
|
|
434
|
+
auth.credential_pool = {};
|
|
435
|
+
// Don't overwrite existing entries for built-in providers
|
|
436
|
+
if (!auth.credential_pool[poolKey]) {
|
|
437
|
+
auth.credential_pool[poolKey] = [];
|
|
438
|
+
}
|
|
439
|
+
auth.credential_pool[poolKey].push({
|
|
440
|
+
id: `${poolKey}-${Date.now()}`,
|
|
441
|
+
label: name,
|
|
442
|
+
base_url,
|
|
443
|
+
access_token: api_key,
|
|
444
|
+
last_status: null,
|
|
445
|
+
});
|
|
446
|
+
await (0, promises_1.writeFile)(authPath, JSON.stringify(auth, null, 2) + '\n', 'utf-8');
|
|
447
|
+
// 3. Auto-switch model to the newly added provider
|
|
448
|
+
let yaml2 = await safeReadFile(configPath) || '';
|
|
449
|
+
const modelBlockMatch = yaml2.match(/^(model:\s*\n(?: .+\n)*)/m);
|
|
450
|
+
if (modelBlockMatch) {
|
|
451
|
+
const lines = [`model:`, ` default: ${model}`, ` provider: ${poolKey}`];
|
|
452
|
+
yaml2 = yaml2.replace(modelBlockMatch[1], lines.join('\n') + '\n');
|
|
453
|
+
await (0, promises_1.writeFile)(configPath, yaml2, 'utf-8');
|
|
454
|
+
}
|
|
404
455
|
ctx.body = { success: true };
|
|
405
456
|
}
|
|
406
457
|
catch (err) {
|
|
@@ -408,16 +459,64 @@ exports.fsRoutes.post('/api/config/providers', async (ctx) => {
|
|
|
408
459
|
ctx.body = { error: err.message };
|
|
409
460
|
}
|
|
410
461
|
});
|
|
411
|
-
// DELETE /api/config/providers/:
|
|
412
|
-
exports.fsRoutes.delete('/api/config/providers/:
|
|
413
|
-
const
|
|
462
|
+
// DELETE /api/config/providers/:poolKey
|
|
463
|
+
exports.fsRoutes.delete('/api/config/providers/:poolKey', async (ctx) => {
|
|
464
|
+
const poolKey = decodeURIComponent(ctx.params.poolKey);
|
|
414
465
|
try {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
466
|
+
const auth = await loadAuthJson();
|
|
467
|
+
if (!auth?.credential_pool) {
|
|
468
|
+
ctx.status = 404;
|
|
469
|
+
ctx.body = { error: 'No credential pool found' };
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const keys = Object.keys(auth.credential_pool);
|
|
473
|
+
// Guard: cannot delete the last provider
|
|
474
|
+
if (keys.length <= 1) {
|
|
475
|
+
ctx.status = 400;
|
|
476
|
+
ctx.body = { error: 'Cannot delete the last provider' };
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!(poolKey in auth.credential_pool)) {
|
|
480
|
+
ctx.status = 404;
|
|
481
|
+
ctx.body = { error: `Provider "${poolKey}" not found` };
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
// Check if this is the current active provider
|
|
485
|
+
const yaml = await safeReadFile(configPath) || '';
|
|
486
|
+
const providerMatch = yaml.match(/^ provider:\s*(.+)$/m);
|
|
487
|
+
const isCurrent = providerMatch && providerMatch[1].trim() === poolKey;
|
|
488
|
+
// Save base_url before deleting (needed for config.yaml cleanup)
|
|
489
|
+
const deletedBaseUrl = auth.credential_pool[poolKey]?.[0]?.base_url;
|
|
490
|
+
// 1. Delete from auth.json
|
|
491
|
+
delete auth.credential_pool[poolKey];
|
|
492
|
+
await (0, promises_1.writeFile)(authPath, JSON.stringify(auth, null, 2) + '\n', 'utf-8');
|
|
493
|
+
// 2. Remove matching entry from config.yaml custom_providers
|
|
494
|
+
// Use base_url to match — more reliable than name (preset key ≠ display name)
|
|
495
|
+
if (deletedBaseUrl) {
|
|
496
|
+
await (0, promises_1.copyFile)(configPath, configPath + '.bak');
|
|
497
|
+
let newYaml = await safeReadFile(configPath) || '';
|
|
498
|
+
const entryRegex = new RegExp(`^- name:.*\\n(?:[ \\t]+.*\\n)*? base_url:\\s*${escapeRegExp(deletedBaseUrl)}\\s*\\n(?:[ \\t]+.*\\n)*`, 'gm');
|
|
499
|
+
newYaml = newYaml.replace(entryRegex, '').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
500
|
+
await (0, promises_1.writeFile)(configPath, newYaml, 'utf-8');
|
|
501
|
+
}
|
|
502
|
+
// 3. If was the current provider, switch to first remaining
|
|
503
|
+
if (isCurrent) {
|
|
504
|
+
const remainingKeys = Object.keys(auth.credential_pool);
|
|
505
|
+
if (remainingKeys.length > 0) {
|
|
506
|
+
const fallback = remainingKeys[0];
|
|
507
|
+
const fallbackEntry = auth.credential_pool[fallback]?.[0];
|
|
508
|
+
const catalogModels = PROVIDER_MODEL_CATALOG[fallback] || [];
|
|
509
|
+
const fallbackModel = catalogModels[0] || fallbackEntry?.label || fallback;
|
|
510
|
+
await (0, promises_1.copyFile)(configPath, configPath + '.bak');
|
|
511
|
+
let newYaml = await safeReadFile(configPath) || '';
|
|
512
|
+
const modelBlockMatch = newYaml.match(/^(model:\s*\n(?: .+\n)*)/m);
|
|
513
|
+
if (modelBlockMatch) {
|
|
514
|
+
const lines = [`model:`, ` default: ${fallbackModel}`, ` provider: ${fallback}`];
|
|
515
|
+
newYaml = newYaml.replace(modelBlockMatch[1], lines.join('\n') + '\n');
|
|
516
|
+
await (0, promises_1.writeFile)(configPath, newYaml, 'utf-8');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
421
520
|
ctx.body = { success: true };
|
|
422
521
|
}
|
|
423
522
|
catch (err) {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.weixinRoutes = void 0;
|
|
7
|
+
const router_1 = __importDefault(require("@koa/router"));
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const promises_1 = require("fs/promises");
|
|
10
|
+
const promises_2 = require("fs/promises");
|
|
11
|
+
const path_1 = require("path");
|
|
12
|
+
const os_1 = require("os");
|
|
13
|
+
const hermes_cli_1 = require("../services/hermes-cli");
|
|
14
|
+
const envPath = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes/.env');
|
|
15
|
+
const ILINK_BASE = 'https://ilinkai.weixin.qq.com';
|
|
16
|
+
exports.weixinRoutes = new router_1.default();
|
|
17
|
+
// GET /api/weixin/qrcode — fetch QR code from Tencent iLink API
|
|
18
|
+
exports.weixinRoutes.get('/api/weixin/qrcode', async (ctx) => {
|
|
19
|
+
try {
|
|
20
|
+
const res = await axios_1.default.get(`${ILINK_BASE}/ilink/bot/get_bot_qrcode`, {
|
|
21
|
+
params: { bot_type: 3 },
|
|
22
|
+
timeout: 15000,
|
|
23
|
+
});
|
|
24
|
+
const data = res.data;
|
|
25
|
+
if (!data || !data.qrcode) {
|
|
26
|
+
ctx.status = 500;
|
|
27
|
+
ctx.body = { error: 'Failed to get QR code' };
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
ctx.body = {
|
|
31
|
+
qrcode: data.qrcode,
|
|
32
|
+
qrcode_url: data.qrcode_img_content,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
ctx.status = 500;
|
|
37
|
+
ctx.body = { error: err.message || 'Failed to connect to iLink API' };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// GET /api/weixin/qrcode/status — poll QR scan status
|
|
41
|
+
exports.weixinRoutes.get('/api/weixin/qrcode/status', async (ctx) => {
|
|
42
|
+
const qrcode = ctx.query.qrcode;
|
|
43
|
+
if (!qrcode) {
|
|
44
|
+
ctx.status = 400;
|
|
45
|
+
ctx.body = { error: 'Missing qrcode parameter' };
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const res = await axios_1.default.get(`${ILINK_BASE}/ilink/bot/get_qrcode_status`, {
|
|
50
|
+
params: { qrcode },
|
|
51
|
+
timeout: 35000,
|
|
52
|
+
});
|
|
53
|
+
const data = res.data;
|
|
54
|
+
const status = data?.status || 'wait';
|
|
55
|
+
ctx.body = { status };
|
|
56
|
+
// If confirmed, return credentials so frontend can save them
|
|
57
|
+
if (status === 'confirmed') {
|
|
58
|
+
ctx.body = {
|
|
59
|
+
status: 'confirmed',
|
|
60
|
+
account_id: data.ilink_bot_id,
|
|
61
|
+
token: data.bot_token,
|
|
62
|
+
base_url: data.baseurl,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
ctx.status = 500;
|
|
68
|
+
ctx.body = { error: err.message || 'Failed to poll QR status' };
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// POST /api/weixin/save — save weixin credentials to .env
|
|
72
|
+
exports.weixinRoutes.post('/api/weixin/save', async (ctx) => {
|
|
73
|
+
const { account_id, token, base_url } = ctx.request.body;
|
|
74
|
+
if (!account_id || !token) {
|
|
75
|
+
ctx.status = 400;
|
|
76
|
+
ctx.body = { error: 'Missing account_id or token' };
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = await (0, promises_1.readFile)(envPath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
raw = '';
|
|
86
|
+
}
|
|
87
|
+
const entries = {
|
|
88
|
+
WEIXIN_ACCOUNT_ID: account_id,
|
|
89
|
+
WEIXIN_TOKEN: token,
|
|
90
|
+
};
|
|
91
|
+
if (base_url)
|
|
92
|
+
entries.WEIXIN_BASE_URL = base_url;
|
|
93
|
+
const lines = raw.split('\n');
|
|
94
|
+
const existingKeys = new Set();
|
|
95
|
+
const result = [];
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (trimmed.startsWith('#')) {
|
|
99
|
+
result.push(line);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const eqIdx = trimmed.indexOf('=');
|
|
103
|
+
if (eqIdx !== -1) {
|
|
104
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
105
|
+
if (key in entries) {
|
|
106
|
+
result.push(`${key}=${entries[key]}`);
|
|
107
|
+
existingKeys.add(key);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
result.push(line);
|
|
112
|
+
}
|
|
113
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
114
|
+
if (!existingKeys.has(key)) {
|
|
115
|
+
result.push(`${key}=${val}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let output = result.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '') + '\n';
|
|
119
|
+
await (0, promises_1.writeFile)(envPath, output, 'utf-8');
|
|
120
|
+
try {
|
|
121
|
+
await (0, promises_2.chmod)(envPath, 0o600);
|
|
122
|
+
}
|
|
123
|
+
catch { /* ignore */ }
|
|
124
|
+
await (0, hermes_cli_1.restartGateway)();
|
|
125
|
+
ctx.body = { success: true };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
ctx.status = 500;
|
|
129
|
+
ctx.body = { error: err.message };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry — single source of truth for both frontend and backend.
|
|
3
|
+
* Synced from hermes-agent hermes_cli/models.py _PROVIDER_MODELS.
|
|
4
|
+
*/
|
|
5
|
+
export interface ProviderPreset {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
base_url: string;
|
|
9
|
+
models: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const PROVIDER_PRESETS: ProviderPreset[];
|
|
12
|
+
/** Build a Record<providerKey, models[]> for backend lookup */
|
|
13
|
+
export declare function buildProviderModelMap(): Record<string, string[]>;
|