glad-web 1.0.46 → 2.0.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/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -61
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1590
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -605
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -108
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/skillhub.js +0 -104
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1032
- package/lib/session/text-history.js +0 -274
- package/lib/skillhub/client.js +0 -121
- package/lib/skillhub/settings-store.js +0 -168
- package/lib/skillhub/skill-installer.js +0 -320
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/bootstrap.js +0 -34
- package/lib/web/claude.js +0 -1150
- package/lib/web/codex.js +0 -1045
- package/lib/web/composer.js +0 -493
- package/lib/web/core.js +0 -385
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -547
- package/lib/web/layout.js +0 -69
- package/lib/web/notifications.js +0 -164
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -361
- package/lib/web/shell.js +0 -74
- package/lib/web/skillhub.js +0 -197
- package/lib/web/styles.css +0 -932
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- package/scripts/check-syntax.js +0 -26
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
class TextHistory {
|
|
2
|
-
constructor(options = {}) {
|
|
3
|
-
this.maxBytes = options.maxBytes || 5 * 1024 * 1024;
|
|
4
|
-
this.debugLabel = options.debugLabel || 'session';
|
|
5
|
-
this.lines = [''];
|
|
6
|
-
this.row = 0;
|
|
7
|
-
this.col = 0;
|
|
8
|
-
this.updatedAt = Date.now();
|
|
9
|
-
this.truncated = false;
|
|
10
|
-
this.totalWrites = 0;
|
|
11
|
-
this.totalBytes = 0;
|
|
12
|
-
this.escapeCount = 0;
|
|
13
|
-
this.clearEvents = 0;
|
|
14
|
-
this.eraseLineEvents = 0;
|
|
15
|
-
this.cursorMoveEvents = 0;
|
|
16
|
-
this.trimEvents = 0;
|
|
17
|
-
this.lastEvents = [];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
write(data) {
|
|
21
|
-
if (!data) return;
|
|
22
|
-
const text = String(data);
|
|
23
|
-
this.totalWrites += 1;
|
|
24
|
-
this.totalBytes += Buffer.byteLength(text, 'utf8');
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < text.length; i++) {
|
|
27
|
-
const ch = text[i];
|
|
28
|
-
|
|
29
|
-
if (ch === '\x1b') {
|
|
30
|
-
this.escapeCount += 1;
|
|
31
|
-
i = this.skipEscape(text, i);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (ch === '\r') {
|
|
36
|
-
this.col = 0;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (ch === '\n') {
|
|
41
|
-
this.newLine();
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (ch === '\b') {
|
|
46
|
-
this.col = Math.max(0, this.col - 1);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (ch === '\t') {
|
|
51
|
-
const spaces = 4 - (this.col % 4);
|
|
52
|
-
for (let j = 0; j < spaces; j++) this.writeChar(' ');
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (ch >= ' ' || ch === '\u00a0') {
|
|
57
|
-
this.writeChar(ch);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.updatedAt = Date.now();
|
|
62
|
-
this.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
toJSON() {
|
|
66
|
-
return {
|
|
67
|
-
text: this.toString(),
|
|
68
|
-
updatedAt: this.updatedAt,
|
|
69
|
-
truncated: this.truncated,
|
|
70
|
-
bytes: Buffer.byteLength(this.toString(), 'utf8'),
|
|
71
|
-
lines: this.lines.length
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
getDebugSnapshot(options = {}) {
|
|
76
|
-
const tailLines = options.tailLines || 12;
|
|
77
|
-
return {
|
|
78
|
-
label: this.debugLabel,
|
|
79
|
-
updatedAt: this.updatedAt,
|
|
80
|
-
truncated: this.truncated,
|
|
81
|
-
row: this.row,
|
|
82
|
-
col: this.col,
|
|
83
|
-
lines: this.lines.length,
|
|
84
|
-
bytes: Buffer.byteLength(this.lines.join('\n'), 'utf8'),
|
|
85
|
-
totalWrites: this.totalWrites,
|
|
86
|
-
totalBytes: this.totalBytes,
|
|
87
|
-
escapeCount: this.escapeCount,
|
|
88
|
-
clearEvents: this.clearEvents,
|
|
89
|
-
eraseLineEvents: this.eraseLineEvents,
|
|
90
|
-
cursorMoveEvents: this.cursorMoveEvents,
|
|
91
|
-
trimEvents: this.trimEvents,
|
|
92
|
-
lastEvents: [...this.lastEvents],
|
|
93
|
-
tailPreview: this.previewText(this.lines.slice(-tailLines).join('\n'))
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
toString() {
|
|
98
|
-
return this.lines.join('\n').replace(/\s+$/g, '');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
writeChar(ch) {
|
|
102
|
-
this.ensureRow();
|
|
103
|
-
const line = this.lines[this.row] || '';
|
|
104
|
-
const padded = line.length < this.col ? line + ' '.repeat(this.col - line.length) : line;
|
|
105
|
-
this.lines[this.row] = padded.slice(0, this.col) + ch + padded.slice(this.col + 1);
|
|
106
|
-
this.col += 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
newLine() {
|
|
110
|
-
this.row += 1;
|
|
111
|
-
this.col = 0;
|
|
112
|
-
this.ensureRow();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
ensureRow() {
|
|
116
|
-
while (this.row >= this.lines.length) {
|
|
117
|
-
this.lines.push('');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
skipEscape(text, index) {
|
|
122
|
-
const next = text[index + 1];
|
|
123
|
-
|
|
124
|
-
if (next === ']') {
|
|
125
|
-
return this.skipOsc(text, index + 2);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (next === '[') {
|
|
129
|
-
return this.handleCsi(text, index + 2);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return Math.min(index + 1, text.length - 1);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
skipOsc(text, index) {
|
|
136
|
-
for (let i = index; i < text.length; i++) {
|
|
137
|
-
if (text[i] === '\x07') return i;
|
|
138
|
-
if (text[i] === '\x1b' && text[i + 1] === '\\') return i + 1;
|
|
139
|
-
}
|
|
140
|
-
return text.length - 1;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
handleCsi(text, index) {
|
|
144
|
-
let i = index;
|
|
145
|
-
while (i < text.length && !/[A-Za-z@`~]/.test(text[i])) {
|
|
146
|
-
i++;
|
|
147
|
-
}
|
|
148
|
-
if (i >= text.length) return text.length - 1;
|
|
149
|
-
|
|
150
|
-
const params = text.slice(index, i);
|
|
151
|
-
const command = text[i];
|
|
152
|
-
this.applyCsi(params, command);
|
|
153
|
-
return i;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
applyCsi(params, command) {
|
|
157
|
-
const values = params
|
|
158
|
-
.replace(/[?>!]/g, '')
|
|
159
|
-
.split(';')
|
|
160
|
-
.filter(Boolean)
|
|
161
|
-
.map(value => Number.parseInt(value, 10))
|
|
162
|
-
.map(value => Number.isFinite(value) ? value : 0);
|
|
163
|
-
const first = values[0] || 0;
|
|
164
|
-
|
|
165
|
-
if (command === 'K') {
|
|
166
|
-
this.eraseLineEvents += 1;
|
|
167
|
-
this.recordEvent(`CSI K(${first})`);
|
|
168
|
-
this.eraseLine(first);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (command === 'J') {
|
|
173
|
-
if (first === 2 || first === 3) {
|
|
174
|
-
this.clearEvents += 1;
|
|
175
|
-
this.recordEvent(`CSI J(${first}) ignored-clear`);
|
|
176
|
-
this.startFreshLineAfterClear();
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (command === 'H' || command === 'f') {
|
|
182
|
-
this.cursorMoveEvents += 1;
|
|
183
|
-
this.recordEvent(`CSI ${command}(${params || ''}) ignored-cursor`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (command === 'A') {
|
|
188
|
-
this.cursorMoveEvents += 1;
|
|
189
|
-
this.recordEvent(`CSI A(${first || 1}) ignored-cursor`);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (command === 'B') {
|
|
194
|
-
this.cursorMoveEvents += 1;
|
|
195
|
-
this.recordEvent(`CSI B(${first || 1}) ignored-cursor`);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (command === 'C') {
|
|
200
|
-
this.cursorMoveEvents += 1;
|
|
201
|
-
this.recordEvent(`CSI C(${first || 1}) ignored-cursor`);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (command === 'D') {
|
|
206
|
-
this.cursorMoveEvents += 1;
|
|
207
|
-
this.recordEvent(`CSI D(${first || 1}) ignored-cursor`);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (command === 'G') {
|
|
212
|
-
this.cursorMoveEvents += 1;
|
|
213
|
-
this.recordEvent(`CSI G(${first || 1}) ignored-cursor`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
eraseLine(mode) {
|
|
218
|
-
this.ensureRow();
|
|
219
|
-
const line = this.lines[this.row] || '';
|
|
220
|
-
if (mode === 1) {
|
|
221
|
-
this.lines[this.row] = ' '.repeat(Math.min(this.col, line.length)) + line.slice(this.col);
|
|
222
|
-
} else if (mode === 2) {
|
|
223
|
-
this.lines[this.row] = '';
|
|
224
|
-
this.col = 0;
|
|
225
|
-
} else {
|
|
226
|
-
this.lines[this.row] = line.slice(0, this.col);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
startFreshLineAfterClear() {
|
|
231
|
-
const hasContent = this.lines.some(line => line.length > 0);
|
|
232
|
-
const currentLine = this.lines[this.row] || '';
|
|
233
|
-
if (hasContent && currentLine.length > 0) this.newLine();
|
|
234
|
-
this.row = this.lines.length - 1;
|
|
235
|
-
this.col = 0;
|
|
236
|
-
this.lines[this.row] = '';
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
trim() {
|
|
240
|
-
let bytes = Buffer.byteLength(this.lines.join('\n'), 'utf8');
|
|
241
|
-
while (bytes > this.maxBytes && this.lines.length > 1) {
|
|
242
|
-
const removed = this.lines.shift();
|
|
243
|
-
bytes -= Buffer.byteLength(removed, 'utf8') + 1;
|
|
244
|
-
this.row = Math.max(0, this.row - 1);
|
|
245
|
-
this.truncated = true;
|
|
246
|
-
this.trimEvents += 1;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (bytes > this.maxBytes && this.lines.length === 1) {
|
|
250
|
-
const keepChars = Math.floor(this.maxBytes / 2);
|
|
251
|
-
this.lines[0] = this.lines[0].slice(-keepChars);
|
|
252
|
-
this.col = Math.min(this.col, this.lines[0].length);
|
|
253
|
-
this.truncated = true;
|
|
254
|
-
this.trimEvents += 1;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
recordEvent(message) {
|
|
259
|
-
this.lastEvents.push(`${new Date().toISOString()} ${message}`);
|
|
260
|
-
if (this.lastEvents.length > 25) this.lastEvents.shift();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
previewText(text, maxChars = 400) {
|
|
264
|
-
if (!text) return '';
|
|
265
|
-
const normalized = String(text)
|
|
266
|
-
.replace(/\r/g, '\\r')
|
|
267
|
-
.replace(/\n/g, '\\n')
|
|
268
|
-
.replace(/\t/g, '\\t')
|
|
269
|
-
.replace(/\x1b/g, '\\x1b');
|
|
270
|
-
return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
module.exports = TextHistory;
|
package/lib/skillhub/client.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
2
|
-
const MAX_BUNDLE_BYTES = 20 * 1024 * 1024;
|
|
3
|
-
|
|
4
|
-
function clientProblem(message, statusCode = 502, code = 'SKILLHUB_REQUEST_FAILED') {
|
|
5
|
-
const error = new Error(message);
|
|
6
|
-
error.statusCode = statusCode;
|
|
7
|
-
error.code = code;
|
|
8
|
-
return error;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
class SkillHubClient {
|
|
12
|
-
constructor({ settingsStore, fetchImpl = global.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
13
|
-
this.settingsStore = settingsStore;
|
|
14
|
-
this.fetchImpl = fetchImpl;
|
|
15
|
-
this.timeoutMs = timeoutMs;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async request(pathname, { method = 'GET', settings = null, body = null, accept = 'application/json' } = {}) {
|
|
19
|
-
const current = settings || this.settingsStore.resolve();
|
|
20
|
-
const base = `${current.baseUrl.replace(/\/$/, '')}/`;
|
|
21
|
-
const path = String(pathname || '').replace(/^\//, '');
|
|
22
|
-
const url = new URL(path, base);
|
|
23
|
-
const controller = new AbortController();
|
|
24
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
25
|
-
try {
|
|
26
|
-
const response = await this.fetchImpl(url, {
|
|
27
|
-
method,
|
|
28
|
-
redirect: 'error',
|
|
29
|
-
signal: controller.signal,
|
|
30
|
-
headers: {
|
|
31
|
-
Accept: accept,
|
|
32
|
-
Authorization: `Bearer ${current.token}`,
|
|
33
|
-
...(body ? { 'Content-Type': 'application/json' } : {})
|
|
34
|
-
},
|
|
35
|
-
...(body ? { body: JSON.stringify(body) } : {})
|
|
36
|
-
});
|
|
37
|
-
if (!response.ok) {
|
|
38
|
-
let detail = '';
|
|
39
|
-
try {
|
|
40
|
-
const payload = await response.json();
|
|
41
|
-
detail = payload?.error?.message || payload?.error || payload?.message || '';
|
|
42
|
-
} catch (_) { /* response body is not JSON */ }
|
|
43
|
-
const statusCode = response.status === 401 || response.status === 403 ? response.status : 502;
|
|
44
|
-
const code = response.status === 401 ? 'SKILLHUB_UNAUTHORIZED'
|
|
45
|
-
: response.status === 403 ? 'SKILLHUB_FORBIDDEN' : 'SKILLHUB_BAD_RESPONSE';
|
|
46
|
-
throw clientProblem(detail || `SkillHub 返回 HTTP ${response.status}`, statusCode, code);
|
|
47
|
-
}
|
|
48
|
-
return response;
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (error.statusCode) throw error;
|
|
51
|
-
if (error.name === 'AbortError') throw clientProblem('SkillHub 请求超时', 504, 'SKILLHUB_TIMEOUT');
|
|
52
|
-
throw clientProblem(`无法连接 SkillHub:${error.message}`);
|
|
53
|
-
} finally {
|
|
54
|
-
clearTimeout(timer);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async test(settings) {
|
|
59
|
-
const response = await this.request('/api/v1/whoami', { settings });
|
|
60
|
-
return response.json();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async listSkills() {
|
|
64
|
-
const items = [];
|
|
65
|
-
let cursor = '';
|
|
66
|
-
for (let page = 0; page < 100; page += 1) {
|
|
67
|
-
const query = new URLSearchParams({ limit: '100', order: 'updated_at_desc' });
|
|
68
|
-
if (cursor) query.set('cursor', cursor);
|
|
69
|
-
const response = await this.request(`/api/runtime/skills?${query}`);
|
|
70
|
-
const payload = await response.json();
|
|
71
|
-
if (!Array.isArray(payload?.data)) throw clientProblem('SkillHub Skill 列表格式无效');
|
|
72
|
-
items.push(...payload.data);
|
|
73
|
-
cursor = String(payload.nextCursor || '');
|
|
74
|
-
if (!cursor) return items;
|
|
75
|
-
}
|
|
76
|
-
throw clientProblem('SkillHub Skill 列表分页过多');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async getSkill({ id, version, digest }) {
|
|
80
|
-
const query = new URLSearchParams({ include: 'manifest,skillMd' });
|
|
81
|
-
if (version) query.set('version', version);
|
|
82
|
-
if (digest) query.set('digest', digest);
|
|
83
|
-
const response = await this.request(`/api/runtime/skills/by-id/${encodeURIComponent(id)}?${query}`);
|
|
84
|
-
return response.json();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async downloadBundle({ id, version, digest }) {
|
|
88
|
-
const query = new URLSearchParams({ id, format: 'zip' });
|
|
89
|
-
if (version) query.set('version', version);
|
|
90
|
-
if (digest) query.set('digest', digest);
|
|
91
|
-
const response = await this.request(`/api/runtime/skills/bundle?${query}`, {
|
|
92
|
-
accept: 'application/zip'
|
|
93
|
-
});
|
|
94
|
-
const declared = Number(response.headers.get('content-length') || 0);
|
|
95
|
-
if (declared > MAX_BUNDLE_BYTES) {
|
|
96
|
-
throw clientProblem('Skill bundle 超过 20 MB', 413, 'SKILLHUB_BUNDLE_TOO_LARGE');
|
|
97
|
-
}
|
|
98
|
-
if (!response.body) throw clientProblem('SkillHub 返回了空 bundle');
|
|
99
|
-
const reader = response.body.getReader();
|
|
100
|
-
const chunks = [];
|
|
101
|
-
let total = 0;
|
|
102
|
-
while (true) {
|
|
103
|
-
const { done, value } = await reader.read();
|
|
104
|
-
if (done) break;
|
|
105
|
-
total += value.byteLength;
|
|
106
|
-
if (total > MAX_BUNDLE_BYTES) {
|
|
107
|
-
await reader.cancel();
|
|
108
|
-
throw clientProblem('Skill bundle 超过 20 MB', 413, 'SKILLHUB_BUNDLE_TOO_LARGE');
|
|
109
|
-
}
|
|
110
|
-
chunks.push(Buffer.from(value));
|
|
111
|
-
}
|
|
112
|
-
const buffer = Buffer.concat(chunks, total);
|
|
113
|
-
return {
|
|
114
|
-
buffer,
|
|
115
|
-
digest: response.headers.get('x-saker-skill-digest') || '',
|
|
116
|
-
sha256: response.headers.get('x-saker-bundle-sha256') || ''
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
module.exports = { SkillHubClient, MAX_BUNDLE_BYTES };
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const {
|
|
4
|
-
getConfig,
|
|
5
|
-
setConfig,
|
|
6
|
-
getConfigPath
|
|
7
|
-
} = require('../config/manager');
|
|
8
|
-
|
|
9
|
-
function problem(message, statusCode = 400, code = 'SKILLHUB_INVALID_SETTINGS') {
|
|
10
|
-
const error = new Error(message);
|
|
11
|
-
error.statusCode = statusCode;
|
|
12
|
-
error.code = code;
|
|
13
|
-
return error;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizeBaseUrl(value) {
|
|
17
|
-
const raw = String(value || '').trim();
|
|
18
|
-
if (!raw || raw.length > 2048) throw problem('请输入有效的 SkillHub 地址');
|
|
19
|
-
let url;
|
|
20
|
-
try { url = new URL(raw); } catch (_) { throw problem('请输入有效的 SkillHub 地址'); }
|
|
21
|
-
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password
|
|
22
|
-
|| url.search || url.hash) {
|
|
23
|
-
throw problem('SkillHub 地址格式无效');
|
|
24
|
-
}
|
|
25
|
-
const localHosts = new Set(['skillhub', 'localhost', '127.0.0.1', '::1']);
|
|
26
|
-
if (url.protocol === 'http:' && !localHosts.has(url.hostname.toLowerCase())) {
|
|
27
|
-
throw problem('远程 SkillHub 必须使用 HTTPS');
|
|
28
|
-
}
|
|
29
|
-
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
30
|
-
return url.toString().replace(/\/$/, '');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeToken(value) {
|
|
34
|
-
const token = String(value || '').trim();
|
|
35
|
-
if (!token || token.length < 12 || token.length > 2048 || /\s/.test(token)) {
|
|
36
|
-
throw problem('请输入有效的 SkillHub API Token');
|
|
37
|
-
}
|
|
38
|
-
return token;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function maskToken(token) {
|
|
42
|
-
if (!token) return '';
|
|
43
|
-
return `${token.slice(0, Math.min(7, token.length))}${'•'.repeat(10)}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function decodeKey(content) {
|
|
47
|
-
const raw = Buffer.isBuffer(content) ? content : Buffer.from(String(content || ''), 'utf8');
|
|
48
|
-
const text = raw.toString('utf8').trim();
|
|
49
|
-
if (/^[0-9a-f]{64}$/i.test(text)) return Buffer.from(text, 'hex');
|
|
50
|
-
if (/^[A-Za-z0-9+/]{43}=$/.test(text)) return Buffer.from(text, 'base64');
|
|
51
|
-
if (raw.length === 32) return raw;
|
|
52
|
-
throw problem('SkillHub Token 加密密钥必须是 32 字节', 500, 'SKILLHUB_KEY_INVALID');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
class SkillHubSettingsStore {
|
|
56
|
-
constructor({
|
|
57
|
-
readConfig = getConfig,
|
|
58
|
-
writeConfig = setConfig,
|
|
59
|
-
configPath = getConfigPath,
|
|
60
|
-
keyFile = process.env.GLAD_SKILLHUB_KEY_FILE || '',
|
|
61
|
-
readFile = fs.readFileSync,
|
|
62
|
-
chmod = fs.chmodSync
|
|
63
|
-
} = {}) {
|
|
64
|
-
this.readConfig = readConfig;
|
|
65
|
-
this.writeConfig = writeConfig;
|
|
66
|
-
this.configPath = configPath;
|
|
67
|
-
this.keyFile = keyFile;
|
|
68
|
-
this.readFile = readFile;
|
|
69
|
-
this.chmod = chmod;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
key() {
|
|
73
|
-
if (!this.keyFile) {
|
|
74
|
-
throw problem('Glad 未配置 SkillHub Token 加密密钥', 503, 'SKILLHUB_KEY_MISSING');
|
|
75
|
-
}
|
|
76
|
-
try { return decodeKey(this.readFile(this.keyFile)); }
|
|
77
|
-
catch (error) {
|
|
78
|
-
if (error.code === 'SKILLHUB_KEY_INVALID') throw error;
|
|
79
|
-
throw problem('Glad 无法读取 SkillHub Token 加密密钥', 503, 'SKILLHUB_KEY_UNREADABLE');
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
encrypt(token) {
|
|
84
|
-
const iv = crypto.randomBytes(12);
|
|
85
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', this.key(), iv);
|
|
86
|
-
const ciphertext = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
|
|
87
|
-
return {
|
|
88
|
-
ciphertext: ciphertext.toString('base64'),
|
|
89
|
-
iv: iv.toString('base64'),
|
|
90
|
-
authTag: cipher.getAuthTag().toString('base64')
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
decrypt(envelope) {
|
|
95
|
-
if (!envelope?.ciphertext || !envelope?.iv || !envelope?.authTag) return '';
|
|
96
|
-
try {
|
|
97
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', this.key(), Buffer.from(envelope.iv, 'base64'));
|
|
98
|
-
decipher.setAuthTag(Buffer.from(envelope.authTag, 'base64'));
|
|
99
|
-
return Buffer.concat([
|
|
100
|
-
decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
|
|
101
|
-
decipher.final()
|
|
102
|
-
]).toString('utf8');
|
|
103
|
-
} catch (_) {
|
|
104
|
-
throw problem('SkillHub Token 解密失败,请重新配置', 503, 'SKILLHUB_TOKEN_DECRYPT_FAILED');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
get() {
|
|
109
|
-
const stored = this.readConfig('skillHub') || {};
|
|
110
|
-
const baseUrl = String(stored.baseUrl || '').trim();
|
|
111
|
-
const token = this.decrypt(stored.token || {});
|
|
112
|
-
return { baseUrl, token };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
getPublic() {
|
|
116
|
-
const settings = this.get();
|
|
117
|
-
return {
|
|
118
|
-
configured: Boolean(settings.baseUrl && settings.token),
|
|
119
|
-
baseUrl: settings.baseUrl,
|
|
120
|
-
maskedToken: maskToken(settings.token)
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
resolve(input = {}) {
|
|
125
|
-
const existing = this.get();
|
|
126
|
-
return {
|
|
127
|
-
baseUrl: normalizeBaseUrl(input.baseUrl ?? existing.baseUrl),
|
|
128
|
-
token: input.token == null || String(input.token).trim() === ''
|
|
129
|
-
? normalizeToken(existing.token)
|
|
130
|
-
: normalizeToken(input.token)
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
save(input = {}) {
|
|
135
|
-
const settings = this.resolve(input);
|
|
136
|
-
this.writeConfig('skillHub', {
|
|
137
|
-
baseUrl: settings.baseUrl,
|
|
138
|
-
token: this.encrypt(settings.token)
|
|
139
|
-
});
|
|
140
|
-
this.restrictConfigFile();
|
|
141
|
-
return this.getPublic();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
clear() {
|
|
145
|
-
this.writeConfig('skillHub', {
|
|
146
|
-
baseUrl: '',
|
|
147
|
-
token: { ciphertext: '', iv: '', authTag: '' }
|
|
148
|
-
});
|
|
149
|
-
this.restrictConfigFile();
|
|
150
|
-
return { configured: false, baseUrl: '', maskedToken: '' };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
restrictConfigFile() {
|
|
154
|
-
try {
|
|
155
|
-
const target = typeof this.configPath === 'function' ? this.configPath() : this.configPath;
|
|
156
|
-
if (target && fs.existsSync(target)) this.chmod(target, 0o600);
|
|
157
|
-
} catch (_) {
|
|
158
|
-
// 部分文件系统不支持 chmod,配置仍可使用。
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
module.exports = {
|
|
164
|
-
SkillHubSettingsStore,
|
|
165
|
-
normalizeBaseUrl,
|
|
166
|
-
normalizeToken,
|
|
167
|
-
maskToken
|
|
168
|
-
};
|