glad-web 1.0.45 → 1.0.46
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/lib/codex/structured-session.js +15 -3
- package/lib/commands/web.js +23 -4
- package/lib/config/manager.js +19 -0
- package/lib/server/routes/skillhub.js +104 -0
- package/lib/session/session-manager.js +71 -40
- package/lib/skillhub/client.js +121 -0
- package/lib/skillhub/settings-store.js +168 -0
- package/lib/skillhub/skill-installer.js +320 -0
- package/lib/web/bootstrap.js +34 -0
- package/lib/web/claude.js +29 -8
- package/lib/web/codex.js +5 -2
- package/lib/web/composer.js +30 -0
- package/lib/web/core.js +47 -35
- package/lib/web/index.html +43 -12
- package/lib/web/layout.js +4 -7
- package/lib/web/notifications.js +1 -0
- package/lib/web/session.js +3 -2
- package/lib/web/shell.js +17 -2
- package/lib/web/skillhub.js +197 -0
- package/lib/web/styles.css +35 -8
- package/package.json +6 -3
|
@@ -330,6 +330,11 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
330
330
|
this.timedInputs = new Map();
|
|
331
331
|
this.promptHistoryCache = null;
|
|
332
332
|
this.deferredWarnings = null;
|
|
333
|
+
this.activeSkill = options.activeSkill && options.activeSkill.name && options.activeSkill.path
|
|
334
|
+
? { name: String(options.activeSkill.name), path: String(options.activeSkill.path) }
|
|
335
|
+
: null;
|
|
336
|
+
this.extraSkillRoots = (Array.isArray(options.extraSkillRoots) ? options.extraSkillRoots : [])
|
|
337
|
+
.map(value => String(value || '').trim()).filter(Boolean);
|
|
333
338
|
}
|
|
334
339
|
|
|
335
340
|
toListItem() {
|
|
@@ -540,6 +545,9 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
540
545
|
}, { fatalOnTimeout: true });
|
|
541
546
|
}).then(async () => {
|
|
542
547
|
this.notify('initialized', {});
|
|
548
|
+
if (this.extraSkillRoots.length) {
|
|
549
|
+
await this.request('skills/extraRoots/set', { extraRoots: this.extraSkillRoots }, { fatalOnTimeout: true });
|
|
550
|
+
}
|
|
543
551
|
try { await this.refreshConfigDefaults(); } catch (error) { this.logger.debugInfo?.(`[codex-app-server] config/read failed: ${error.message}`); }
|
|
544
552
|
try { await this.refreshModels(); } catch (error) { this.logger.debugInfo?.(`[codex-app-server] model/list failed: ${error.message}`); }
|
|
545
553
|
resolve();
|
|
@@ -908,10 +916,11 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
908
916
|
|
|
909
917
|
async listSkills(forceReload = false) {
|
|
910
918
|
await this.ensureProcess();
|
|
911
|
-
const
|
|
919
|
+
const params = {
|
|
912
920
|
cwds: [this.workingDir],
|
|
913
921
|
forceReload: Boolean(forceReload)
|
|
914
|
-
}
|
|
922
|
+
};
|
|
923
|
+
const result = await this.request('skills/list', params);
|
|
915
924
|
const entries = Array.isArray(result?.data) ? result.data : [];
|
|
916
925
|
const entry = entries.find(item => item?.cwd === this.workingDir) || entries[0] || {};
|
|
917
926
|
return {
|
|
@@ -921,7 +930,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
921
930
|
}
|
|
922
931
|
|
|
923
932
|
async resolveSkillInputs(skills) {
|
|
924
|
-
const requested =
|
|
933
|
+
const requested = [
|
|
934
|
+
...(this.activeSkill ? [this.activeSkill] : []),
|
|
935
|
+
...(Array.isArray(skills) ? skills : [])
|
|
936
|
+
].slice(0, 8);
|
|
925
937
|
if (!requested.length) return [];
|
|
926
938
|
const available = await this.listSkills(false);
|
|
927
939
|
const allowed = new Map(available.skills.map(item => [`${item.name}\n${item.path}`, item]));
|
package/lib/commands/web.js
CHANGED
|
@@ -48,10 +48,14 @@ const registerWorkspaceRoutes = require('../server/routes/workspace');
|
|
|
48
48
|
const registerProviderRoutes = require('../server/routes/providers');
|
|
49
49
|
const registerNotificationRoutes = require('../server/routes/notifications');
|
|
50
50
|
const registerUsageRoutes = require('../server/routes/usage');
|
|
51
|
+
const registerSkillHubRoutes = require('../server/routes/skillhub');
|
|
51
52
|
const { UsageService } = require('../usage/usage-service');
|
|
52
53
|
const { ServerChanSettingsStore } = require('../notifications/serverchan-settings-store');
|
|
53
54
|
const ServerChanClient = require('../notifications/serverchan-client');
|
|
54
55
|
const NotificationService = require('../notifications/notification-service');
|
|
56
|
+
const { SkillHubSettingsStore } = require('../skillhub/settings-store');
|
|
57
|
+
const { SkillHubClient } = require('../skillhub/client');
|
|
58
|
+
const { SkillInstaller } = require('../skillhub/skill-installer');
|
|
55
59
|
|
|
56
60
|
async function webCommand(options) {
|
|
57
61
|
const port = parseInt(options.port) || 3000;
|
|
@@ -104,6 +108,10 @@ async function webCommand(options) {
|
|
|
104
108
|
logger
|
|
105
109
|
});
|
|
106
110
|
const usageService = new UsageService({ logger });
|
|
111
|
+
const skillHubSettings = new SkillHubSettingsStore();
|
|
112
|
+
const skillHubClient = new SkillHubClient({ settingsStore: skillHubSettings });
|
|
113
|
+
const skillInstaller = new SkillInstaller({ client: skillHubClient });
|
|
114
|
+
await skillInstaller.initialize();
|
|
107
115
|
sessionManager.on('output', ({ sessionId, data }) => {
|
|
108
116
|
broadcastToSession(sessionId, { type: 'output', data });
|
|
109
117
|
});
|
|
@@ -147,6 +155,12 @@ async function webCommand(options) {
|
|
|
147
155
|
notificationService
|
|
148
156
|
});
|
|
149
157
|
registerUsageRoutes(app, { usageService, sendJson: sendCompressedJson });
|
|
158
|
+
registerSkillHubRoutes(app, {
|
|
159
|
+
settingsStore: skillHubSettings,
|
|
160
|
+
client: skillHubClient,
|
|
161
|
+
installer: skillInstaller,
|
|
162
|
+
sessionManager
|
|
163
|
+
});
|
|
150
164
|
|
|
151
165
|
// API: List all active sessions
|
|
152
166
|
app.get('/api/sessions', (req, res) => {
|
|
@@ -310,7 +324,8 @@ async function webCommand(options) {
|
|
|
310
324
|
|
|
311
325
|
// API: Delete/Kill session
|
|
312
326
|
app.delete('/api/sessions/:id', async (req, res) => {
|
|
313
|
-
await sessionManager.kill(req.params.id);
|
|
327
|
+
const deleted = await sessionManager.kill(req.params.id);
|
|
328
|
+
if (!deleted) return res.status(404).json({ error: 'Session not found' });
|
|
314
329
|
res.json({ success: true });
|
|
315
330
|
});
|
|
316
331
|
|
|
@@ -509,12 +524,14 @@ async function webCommand(options) {
|
|
|
509
524
|
};
|
|
510
525
|
|
|
511
526
|
const webAssets = [
|
|
527
|
+
'bootstrap.js',
|
|
512
528
|
'gitgraph.js',
|
|
513
529
|
'styles.css',
|
|
514
530
|
'theme.js',
|
|
515
531
|
'core.js',
|
|
516
532
|
'layout.js',
|
|
517
533
|
'notifications.js',
|
|
534
|
+
'skillhub.js',
|
|
518
535
|
'claude.js',
|
|
519
536
|
'schedules.js',
|
|
520
537
|
'shell.js',
|
|
@@ -575,12 +592,14 @@ async function webCommand(options) {
|
|
|
575
592
|
console.log(chalk.gray(`Tips: Access from your phone via the Network URL above.\n`));
|
|
576
593
|
});
|
|
577
594
|
|
|
578
|
-
|
|
595
|
+
const shutdown = () => {
|
|
579
596
|
schedulerService.stop();
|
|
580
597
|
notificationService.stop();
|
|
581
598
|
sessionManager.killAll();
|
|
582
|
-
process.exit(0);
|
|
583
|
-
}
|
|
599
|
+
process.exit(0);
|
|
600
|
+
};
|
|
601
|
+
process.once('SIGINT', shutdown);
|
|
602
|
+
process.once('SIGTERM', shutdown);
|
|
584
603
|
}
|
|
585
604
|
|
|
586
605
|
module.exports = webCommand;
|
package/lib/config/manager.js
CHANGED
|
@@ -26,6 +26,25 @@ const schema = {
|
|
|
26
26
|
clientType: 'wechat'
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
|
+
skillHub: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
baseUrl: { type: 'string', default: '' },
|
|
33
|
+
token: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
ciphertext: { type: 'string', default: '' },
|
|
37
|
+
iv: { type: 'string', default: '' },
|
|
38
|
+
authTag: { type: 'string', default: '' }
|
|
39
|
+
},
|
|
40
|
+
default: { ciphertext: '', iv: '', authTag: '' }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
default: {
|
|
44
|
+
baseUrl: '',
|
|
45
|
+
token: { ciphertext: '', iv: '', authTag: '' }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
29
48
|
version: {
|
|
30
49
|
type: 'string',
|
|
31
50
|
default: '1.0.0'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
|
|
3
|
+
function statusCode(error) {
|
|
4
|
+
return Number(error?.statusCode) || 500;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function errorBody(error) {
|
|
8
|
+
return {
|
|
9
|
+
error: error?.message || 'SkillHub 操作失败',
|
|
10
|
+
...(error?.code ? { code: error.code } : {})
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function registerSkillHubRoutes(app, {
|
|
15
|
+
settingsStore,
|
|
16
|
+
client,
|
|
17
|
+
installer,
|
|
18
|
+
sessionManager
|
|
19
|
+
}) {
|
|
20
|
+
app.get('/api/skillhub/status', (_req, res) => {
|
|
21
|
+
res.json({ available: installer.available === true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.get('/api/skillhub/settings', (_req, res) => {
|
|
25
|
+
try { res.json(settingsStore.getPublic()); }
|
|
26
|
+
catch (error) { res.status(statusCode(error)).json(errorBody(error)); }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
app.put('/api/skillhub/settings', async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const resolved = settingsStore.resolve(req.body || {});
|
|
32
|
+
const user = await client.test(resolved);
|
|
33
|
+
const settings = settingsStore.save(resolved);
|
|
34
|
+
res.json({ success: true, settings, user });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
res.status(statusCode(error)).json(errorBody(error));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
app.delete('/api/skillhub/settings', (_req, res) => {
|
|
41
|
+
try { res.json({ success: true, settings: settingsStore.clear() }); }
|
|
42
|
+
catch (error) { res.status(statusCode(error)).json(errorBody(error)); }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.post('/api/skillhub/settings/test', async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const settings = settingsStore.resolve(req.body || {});
|
|
48
|
+
const user = await client.test(settings);
|
|
49
|
+
res.json({ success: true, user });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
res.status(statusCode(error)).json(errorBody(error));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
app.get('/api/skillhub/skills', async (_req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
installer.assertAvailable();
|
|
58
|
+
const skills = await client.listSkills();
|
|
59
|
+
res.json({ success: true, skills });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
res.status(statusCode(error)).json(errorBody(error));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
app.post('/api/skillhub/sessions', async (req, res) => {
|
|
66
|
+
const sessionId = uuidv4();
|
|
67
|
+
let created = false;
|
|
68
|
+
try {
|
|
69
|
+
if (req.body?.toolKey !== 'codex') {
|
|
70
|
+
const error = new Error('SkillHub Session 当前只支持 Codex');
|
|
71
|
+
error.statusCode = 400;
|
|
72
|
+
error.code = 'SKILLHUB_CODEX_ONLY';
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const activeSkill = await installer.prepare(sessionId, req.body?.skill || {});
|
|
76
|
+
const session = sessionManager.create({
|
|
77
|
+
id: sessionId,
|
|
78
|
+
toolKey: 'codex',
|
|
79
|
+
workingDirectory: req.body?.workingDirectory,
|
|
80
|
+
name: activeSkill.name,
|
|
81
|
+
codexOptions: {
|
|
82
|
+
activeSkill,
|
|
83
|
+
extraSkillRoots: [activeSkill.skillsRoot]
|
|
84
|
+
},
|
|
85
|
+
disposeResources: () => installer.cleanupSync(sessionId)
|
|
86
|
+
});
|
|
87
|
+
created = true;
|
|
88
|
+
const defaultPrompt = activeSkill.defaultPrompt
|
|
89
|
+
|| '请先用中文介绍这个 Skill 能完成什么、适合哪些任务,以及用户接下来应该如何使用。这一轮只做使用引导。';
|
|
90
|
+
const intro = `$${activeSkill.name}\n\n${defaultPrompt}`;
|
|
91
|
+
const started = await sessionManager.sendCodexInput(session.id, intro, [], [activeSkill], []);
|
|
92
|
+
if (!started) throw new Error('Codex Skill 引导会话启动失败');
|
|
93
|
+
res.status(201).json({ id: session.id, name: session.name });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (created) await sessionManager.kill(sessionId).catch(() => {});
|
|
96
|
+
else {
|
|
97
|
+
try { installer.cleanupSync(sessionId); } catch (_) { /* 目录可能尚未创建 */ }
|
|
98
|
+
}
|
|
99
|
+
res.status(statusCode(error)).json(errorBody(error));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = registerSkillHubRoutes;
|
|
@@ -38,6 +38,12 @@ function promptWithFileReferences(text, attachments) {
|
|
|
38
38
|
return prompt ? `${prompt}\n\n${note}` : note;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const STRUCTURED_SESSION_KINDS = new Set(['claude-structured', 'codex-structured']);
|
|
42
|
+
|
|
43
|
+
function isStructuredSession(session) {
|
|
44
|
+
return Boolean(session && STRUCTURED_SESSION_KINDS.has(session.kind));
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
class SessionManager extends EventEmitter {
|
|
42
48
|
constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient, claudeTranscriptRepository, codexImageStore, fileAttachmentStore, claudeForkSession } = {}) {
|
|
43
49
|
super();
|
|
@@ -77,7 +83,7 @@ class SessionManager extends EventEmitter {
|
|
|
77
83
|
startTime: session.startTime,
|
|
78
84
|
toolKey: session.tool.key,
|
|
79
85
|
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
80
|
-
mode:
|
|
86
|
+
mode: isStructuredSession(session) ? 'structured' : 'terminal',
|
|
81
87
|
hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
|
|
82
88
|
serverChanNotificationEnabled: Boolean(session.serverChanNotificationEnabled),
|
|
83
89
|
timedInputCount: session.timedInputs
|
|
@@ -94,7 +100,7 @@ class SessionManager extends EventEmitter {
|
|
|
94
100
|
return this.sessions.has(id);
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
create({ toolKey, workingDirectory, name, claudeOptions }) {
|
|
103
|
+
create({ id: requestedId, toolKey, workingDirectory, name, claudeOptions, codexOptions, disposeResources }) {
|
|
98
104
|
this.logger.info(`Creating session: toolKey=${toolKey || ''}, workingDirectory=${workingDirectory || '(default)'}`);
|
|
99
105
|
const tool = getToolByKey(toolKey);
|
|
100
106
|
if (!tool) {
|
|
@@ -108,7 +114,14 @@ class SessionManager extends EventEmitter {
|
|
|
108
114
|
return this.createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions });
|
|
109
115
|
}
|
|
110
116
|
if (tool.key === 'codex') {
|
|
111
|
-
return this.createCodexStructuredSession({
|
|
117
|
+
return this.createCodexStructuredSession({
|
|
118
|
+
id: requestedId,
|
|
119
|
+
tool,
|
|
120
|
+
workingDirectory,
|
|
121
|
+
name,
|
|
122
|
+
codexOptions,
|
|
123
|
+
disposeResources
|
|
124
|
+
});
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
const id = uuidv4();
|
|
@@ -210,7 +223,7 @@ class SessionManager extends EventEmitter {
|
|
|
210
223
|
return session;
|
|
211
224
|
}
|
|
212
225
|
|
|
213
|
-
createCodexStructuredSession({ tool, workingDirectory, name, codexOptions = {} }) {
|
|
226
|
+
createCodexStructuredSession({ id: requestedId, tool, workingDirectory, name, codexOptions = {}, disposeResources = null }) {
|
|
214
227
|
const sessionDir = workingDirectory && String(workingDirectory).trim()
|
|
215
228
|
? path.resolve(this.baseDir, String(workingDirectory).trim())
|
|
216
229
|
: this.baseDir;
|
|
@@ -219,12 +232,13 @@ class SessionManager extends EventEmitter {
|
|
|
219
232
|
err.statusCode = 400;
|
|
220
233
|
throw err;
|
|
221
234
|
}
|
|
222
|
-
const id = uuidv4();
|
|
235
|
+
const id = requestedId || uuidv4();
|
|
223
236
|
const session = new CodexStructuredSession({ id, tool, workingDir: sessionDir, name: name || tool.displayName, logger: this.logger, options: codexOptions });
|
|
224
237
|
session.imageAttachments = new Map();
|
|
225
238
|
session.imageUploads = new Map();
|
|
226
239
|
session.fileAttachments = new Map();
|
|
227
240
|
session.fileUploads = new Map();
|
|
241
|
+
session.disposeResources = typeof disposeResources === 'function' ? disposeResources : null;
|
|
228
242
|
this.sessions.set(id, session);
|
|
229
243
|
session.on('event', event => this.emit('codex-event', { sessionId: id, event, session }));
|
|
230
244
|
session.on('exit', () => this.handleExit(session));
|
|
@@ -239,13 +253,13 @@ class SessionManager extends EventEmitter {
|
|
|
239
253
|
const session = this.get(id);
|
|
240
254
|
if (!session) return false;
|
|
241
255
|
this.markSessionInput(session, data);
|
|
242
|
-
if (
|
|
256
|
+
if (isStructuredSession(session)) return session.write(data);
|
|
243
257
|
return session.ptyManager.write(data);
|
|
244
258
|
}
|
|
245
259
|
|
|
246
260
|
sendTerminalFileInput(id, text, fileAttachmentIds = []) {
|
|
247
261
|
const session = this.get(id);
|
|
248
|
-
if (!session ||
|
|
262
|
+
if (!session || isStructuredSession(session)) return false;
|
|
249
263
|
const files = this.getFileAttachments(id, fileAttachmentIds);
|
|
250
264
|
const prompt = promptWithFileReferences(text, files);
|
|
251
265
|
if (!prompt) return false;
|
|
@@ -519,7 +533,7 @@ class SessionManager extends EventEmitter {
|
|
|
519
533
|
redraw(id, cols, rows) {
|
|
520
534
|
const session = this.get(id);
|
|
521
535
|
if (!session) return false;
|
|
522
|
-
if (
|
|
536
|
+
if (isStructuredSession(session)) return true;
|
|
523
537
|
if (session.renderedHistory) {
|
|
524
538
|
session.renderedHistory.resize(cols, rows);
|
|
525
539
|
}
|
|
@@ -537,7 +551,7 @@ class SessionManager extends EventEmitter {
|
|
|
537
551
|
markCompletionRead(id) {
|
|
538
552
|
const session = this.get(id);
|
|
539
553
|
if (!session) return null;
|
|
540
|
-
if (
|
|
554
|
+
if (isStructuredSession(session)) {
|
|
541
555
|
session.markCompletionRead();
|
|
542
556
|
this.logSessionDiagnostics('completion-read', session, {}, { compact: true });
|
|
543
557
|
return session;
|
|
@@ -661,33 +675,57 @@ class SessionManager extends EventEmitter {
|
|
|
661
675
|
return true;
|
|
662
676
|
}
|
|
663
677
|
|
|
664
|
-
|
|
665
|
-
const session = this.get(id);
|
|
666
|
-
if (!session) return false;
|
|
678
|
+
quiesceSession(session) {
|
|
667
679
|
clearTimeout(session.completionTimer);
|
|
668
680
|
this.clearTimedInputs(session);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
disposeSessionResources(session) {
|
|
684
|
+
this.quiesceSession(session);
|
|
669
685
|
this.clearCodexImageUploads(session);
|
|
670
686
|
this.clearCodexImageAttachments(session);
|
|
671
|
-
this.
|
|
687
|
+
this.fileAttachmentStore.clear(session);
|
|
688
|
+
if (typeof session.disposeResources === 'function') {
|
|
689
|
+
try { session.disposeResources(); } catch (error) {
|
|
690
|
+
this.logger.error?.(`Failed to clean session resources ${session.id}: ${error.message}`);
|
|
691
|
+
}
|
|
692
|
+
session.disposeResources = null;
|
|
693
|
+
}
|
|
672
694
|
this.disposeSessionHistory(session);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async stopSessionRuntime(session) {
|
|
698
|
+
if (isStructuredSession(session)) {
|
|
699
|
+
await session.kill();
|
|
700
|
+
return;
|
|
676
701
|
}
|
|
677
702
|
session.ptyManager.kill();
|
|
678
|
-
|
|
679
|
-
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
removeSession(session) {
|
|
706
|
+
if (this.sessions.get(session.id) !== session) return false;
|
|
707
|
+
this.disposeSessionResources(session);
|
|
708
|
+
this.sessions.delete(session.id);
|
|
709
|
+
this.emit('exit', { sessionId: session.id, session });
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async kill(id) {
|
|
714
|
+
const session = this.get(id);
|
|
715
|
+
if (!session) return false;
|
|
716
|
+
this.logSessionDiagnostics('session-deleted', session, {}, { compact: true });
|
|
717
|
+
this.quiesceSession(session);
|
|
718
|
+
await this.stopSessionRuntime(session);
|
|
719
|
+
this.removeSession(session);
|
|
680
720
|
return true;
|
|
681
721
|
}
|
|
682
722
|
|
|
683
723
|
killAll() {
|
|
684
|
-
for (const session of this.sessions.values()) {
|
|
685
|
-
|
|
686
|
-
this.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.disposeSessionHistory(session);
|
|
690
|
-
session.ptyManager.kill();
|
|
724
|
+
for (const session of Array.from(this.sessions.values())) {
|
|
725
|
+
this.disposeSessionResources(session);
|
|
726
|
+
void this.stopSessionRuntime(session).catch(error => {
|
|
727
|
+
this.logger.error?.(`Failed to stop session ${session.id}: ${error.message}`);
|
|
728
|
+
});
|
|
691
729
|
}
|
|
692
730
|
this.sessions.clear();
|
|
693
731
|
}
|
|
@@ -695,7 +733,7 @@ class SessionManager extends EventEmitter {
|
|
|
695
733
|
getHistory(id) {
|
|
696
734
|
const session = this.get(id);
|
|
697
735
|
if (!session) return null;
|
|
698
|
-
if (
|
|
736
|
+
if (isStructuredSession(session)) return session.getHistory();
|
|
699
737
|
const historySource = session.renderedHistory || session.textHistory;
|
|
700
738
|
return {
|
|
701
739
|
success: true,
|
|
@@ -710,7 +748,7 @@ class SessionManager extends EventEmitter {
|
|
|
710
748
|
getCatchupOutput(id) {
|
|
711
749
|
const session = this.get(id);
|
|
712
750
|
if (!session) return null;
|
|
713
|
-
if (
|
|
751
|
+
if (isStructuredSession(session)) return session.getCatchupOutput();
|
|
714
752
|
|
|
715
753
|
const bufferHistory = session.buffer.getAfter(0);
|
|
716
754
|
if (bufferHistory.length > 0) {
|
|
@@ -849,22 +887,15 @@ class SessionManager extends EventEmitter {
|
|
|
849
887
|
}
|
|
850
888
|
|
|
851
889
|
handleExit(session) {
|
|
852
|
-
if (
|
|
890
|
+
if (this.sessions.get(session.id) !== session) return;
|
|
853
891
|
this.logger.info(`Session ${session.id} (${session.name}) exited.`);
|
|
854
|
-
|
|
855
|
-
this.clearTimedInputs(session);
|
|
856
|
-
this.clearCodexImageUploads(session);
|
|
857
|
-
this.clearCodexImageAttachments(session);
|
|
858
|
-
this.fileAttachmentStore.clear(session);
|
|
859
|
-
this.disposeSessionHistory(session);
|
|
860
|
-
this.sessions.delete(session.id);
|
|
861
|
-
this.emit('exit', { sessionId: session.id, session });
|
|
892
|
+
this.removeSession(session);
|
|
862
893
|
}
|
|
863
894
|
|
|
864
895
|
markSessionInput(session, data) {
|
|
865
896
|
if (typeof data !== 'string' || data.length === 0) return;
|
|
866
897
|
session.inputSeq = (session.inputSeq || 0) + 1;
|
|
867
|
-
if (
|
|
898
|
+
if (isStructuredSession(session)) {
|
|
868
899
|
session.hasUnreadCompletion = false;
|
|
869
900
|
this.logSessionDiagnostics('session-input', session, {
|
|
870
901
|
inputSeq: session.inputSeq,
|
|
@@ -883,7 +914,7 @@ class SessionManager extends EventEmitter {
|
|
|
883
914
|
}
|
|
884
915
|
|
|
885
916
|
disposeSessionHistory(session) {
|
|
886
|
-
if (
|
|
917
|
+
if (isStructuredSession(session)) return;
|
|
887
918
|
if (session.renderedHistory) {
|
|
888
919
|
session.renderedHistory.dispose();
|
|
889
920
|
session.renderedHistory = null;
|
|
@@ -907,7 +938,7 @@ class SessionManager extends EventEmitter {
|
|
|
907
938
|
}
|
|
908
939
|
|
|
909
940
|
getSessionDiagnostics(session, extra = {}) {
|
|
910
|
-
if (
|
|
941
|
+
if (isStructuredSession(session)) {
|
|
911
942
|
return {
|
|
912
943
|
sessionId: session.id,
|
|
913
944
|
sessionName: session.name,
|
|
@@ -936,7 +967,7 @@ class SessionManager extends EventEmitter {
|
|
|
936
967
|
}
|
|
937
968
|
|
|
938
969
|
getCompactSessionDiagnostics(session, extra = {}) {
|
|
939
|
-
if (
|
|
970
|
+
if (isStructuredSession(session)) {
|
|
940
971
|
return this.getSessionDiagnostics(session, extra);
|
|
941
972
|
}
|
|
942
973
|
const buffer = session.buffer.getDebugSnapshot();
|
|
@@ -0,0 +1,121 @@
|
|
|
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 };
|