@ian2018cs/agenthub 0.1.46 → 0.1.48
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/package.json +2 -2
- package/server/database/db.js +11 -2
- package/server/routes/skills.js +14 -2
- package/server/services/feishu/card-builder.js +107 -3
- package/server/services/feishu/command-handler.js +29 -1
- package/server/services/feishu/feishu-engine.js +20 -0
- package/server/services/feishu/sdk-bridge.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian2018cs/agenthub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "A web-based UI for AI Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
54
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.70",
|
|
55
55
|
"@codemirror/lang-css": "^6.3.1",
|
|
56
56
|
"@codemirror/lang-html": "^6.4.9",
|
|
57
57
|
"@codemirror/lang-javascript": "^6.2.4",
|
package/server/database/db.js
CHANGED
|
@@ -239,6 +239,13 @@ const runMigrations = () => {
|
|
|
239
239
|
// 列已存在,忽略
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// 为已有 feishu_session_state 表补充 claude_model 列
|
|
243
|
+
try {
|
|
244
|
+
db.exec("ALTER TABLE feishu_session_state ADD COLUMN claude_model TEXT NOT NULL DEFAULT 'sonnet'");
|
|
245
|
+
} catch (_) {
|
|
246
|
+
// 列已存在,忽略
|
|
247
|
+
}
|
|
248
|
+
|
|
242
249
|
// 飞书已处理消息 ID 去重表(持久化,防止服务重启后重复处理)
|
|
243
250
|
db.exec(`
|
|
244
251
|
CREATE TABLE IF NOT EXISTS feishu_processed_messages (
|
|
@@ -1180,19 +1187,21 @@ const feishuDb = {
|
|
|
1180
1187
|
if ('claude_session_id' in updates) { fields.push('claude_session_id = ?'); values.push(updates.claude_session_id); }
|
|
1181
1188
|
if ('cwd' in updates) { fields.push('cwd = ?'); values.push(updates.cwd); }
|
|
1182
1189
|
if ('permission_mode' in updates) { fields.push('permission_mode = ?'); values.push(updates.permission_mode); }
|
|
1190
|
+
if ('claude_model' in updates) { fields.push('claude_model = ?'); values.push(updates.claude_model); }
|
|
1183
1191
|
if ('chat_id' in updates) { fields.push('chat_id = ?'); values.push(updates.chat_id); }
|
|
1184
1192
|
fields.push('updated_at = CURRENT_TIMESTAMP');
|
|
1185
1193
|
values.push(feishuOpenId);
|
|
1186
1194
|
db.prepare(`UPDATE feishu_session_state SET ${fields.join(', ')} WHERE feishu_open_id = ?`).run(...values);
|
|
1187
1195
|
} else {
|
|
1188
1196
|
db.prepare(`
|
|
1189
|
-
INSERT INTO feishu_session_state (feishu_open_id, claude_session_id, cwd, permission_mode, chat_id)
|
|
1190
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1197
|
+
INSERT INTO feishu_session_state (feishu_open_id, claude_session_id, cwd, permission_mode, claude_model, chat_id)
|
|
1198
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1191
1199
|
`).run(
|
|
1192
1200
|
feishuOpenId,
|
|
1193
1201
|
updates.claude_session_id ?? null,
|
|
1194
1202
|
updates.cwd ?? '',
|
|
1195
1203
|
updates.permission_mode ?? 'default',
|
|
1204
|
+
updates.claude_model ?? 'sonnet',
|
|
1196
1205
|
updates.chat_id ?? null
|
|
1197
1206
|
);
|
|
1198
1207
|
}
|
package/server/routes/skills.js
CHANGED
|
@@ -44,9 +44,21 @@ async function parseSkillMetadata(skillPath) {
|
|
|
44
44
|
if (fmMatch) {
|
|
45
45
|
const fm = fmMatch[1];
|
|
46
46
|
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
47
|
-
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
48
47
|
if (nameMatch) title = nameMatch[1].trim();
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
// Handle YAML block scalars: `description: >` (folded) or `description: |` (literal)
|
|
50
|
+
const descBlockMatch = fm.match(/^description:\s*[>|]\s*\n((?:[ \t]+[^\n]*\n?)*)/m);
|
|
51
|
+
if (descBlockMatch) {
|
|
52
|
+
description = descBlockMatch[1]
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map(line => line.trimStart())
|
|
55
|
+
.filter(line => line.length > 0)
|
|
56
|
+
.join(' ')
|
|
57
|
+
.trim();
|
|
58
|
+
} else {
|
|
59
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
60
|
+
if (descMatch) description = descMatch[1].trim();
|
|
61
|
+
}
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
return { title, description };
|
|
@@ -11,6 +11,13 @@ const MODE_LABELS = {
|
|
|
11
11
|
bypassPermissions: 'YOLO 模式',
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
const MODEL_OPTIONS = [
|
|
15
|
+
{ key: 'sonnet', label: 'Sonnet', desc: '均衡性能,适合大多数任务' },
|
|
16
|
+
{ key: 'opus', label: 'Opus', desc: '最强能力,复杂任务首选' },
|
|
17
|
+
{ key: 'haiku', label: 'Haiku', desc: '极速轻量,简单任务专用' },
|
|
18
|
+
];
|
|
19
|
+
const MODEL_LABELS = Object.fromEntries(MODEL_OPTIONS.map(m => [m.key, m.label]));
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* 隐藏用户项目目录前缀,只保留相对项目路径。
|
|
16
23
|
* 例:/data/user-projects/597ef057-.../test → test
|
|
@@ -265,16 +272,18 @@ function buildModeSelectCardDirect(currentMode) {
|
|
|
265
272
|
|
|
266
273
|
/**
|
|
267
274
|
* 状态卡片(直接 JSON)
|
|
268
|
-
* 显示当前项目、路径、会话 ID
|
|
275
|
+
* 显示当前项目、路径、会话 ID、权限模式、模型,底部提供「新建会话」「选择会话」「切换模型」「切换模式」快捷按钮
|
|
269
276
|
*/
|
|
270
277
|
function buildStatusCardDirect(state) {
|
|
271
|
-
const { cwd, claude_session_id, permission_mode } = state || {};
|
|
278
|
+
const { cwd, claude_session_id, permission_mode, claude_model } = state || {};
|
|
272
279
|
const projectPath = cwd || '';
|
|
273
280
|
const displayPath = stripProjectPrefix(projectPath);
|
|
274
281
|
const projectName = displayPath ? displayPath.split('/').filter(Boolean).pop() || displayPath : '未设置';
|
|
275
282
|
const sessionId = claude_session_id ? claude_session_id.slice(0, 8) : '新建';
|
|
276
283
|
const mode = permission_mode || 'default';
|
|
277
284
|
const modeLabel = MODE_LABELS[mode] || mode;
|
|
285
|
+
const model = claude_model || 'sonnet';
|
|
286
|
+
const modelLabel = MODEL_LABELS[model] || model;
|
|
278
287
|
|
|
279
288
|
const card = {
|
|
280
289
|
schema: '2.0',
|
|
@@ -311,6 +320,7 @@ function buildStatusCardDirect(state) {
|
|
|
311
320
|
elements: [
|
|
312
321
|
{ tag: 'markdown', content: `**会话**\n\`${sessionId}\`` },
|
|
313
322
|
{ tag: 'markdown', content: `**模式**\n${modeLabel}` },
|
|
323
|
+
{ tag: 'markdown', content: `**模型**\n${modelLabel}` },
|
|
314
324
|
],
|
|
315
325
|
},
|
|
316
326
|
],
|
|
@@ -337,6 +347,34 @@ function buildStatusCardDirect(state) {
|
|
|
337
347
|
behaviors: [{ type: 'callback', value: { action: 'new_session' } }],
|
|
338
348
|
}],
|
|
339
349
|
},
|
|
350
|
+
{
|
|
351
|
+
tag: 'column',
|
|
352
|
+
width: 'auto',
|
|
353
|
+
vertical_spacing: '4px',
|
|
354
|
+
horizontal_align: 'left',
|
|
355
|
+
vertical_align: 'center',
|
|
356
|
+
elements: [{
|
|
357
|
+
tag: 'button',
|
|
358
|
+
type: 'default',
|
|
359
|
+
width: 'default',
|
|
360
|
+
text: { tag: 'plain_text', content: '选择会话' },
|
|
361
|
+
behaviors: [{ type: 'callback', value: { action: 'show_sessions' } }],
|
|
362
|
+
}],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
tag: 'column',
|
|
366
|
+
width: 'auto',
|
|
367
|
+
vertical_spacing: '4px',
|
|
368
|
+
horizontal_align: 'left',
|
|
369
|
+
vertical_align: 'center',
|
|
370
|
+
elements: [{
|
|
371
|
+
tag: 'button',
|
|
372
|
+
type: 'default',
|
|
373
|
+
width: 'default',
|
|
374
|
+
text: { tag: 'plain_text', content: '切换模型' },
|
|
375
|
+
behaviors: [{ type: 'callback', value: { action: 'show_model_select' } }],
|
|
376
|
+
}],
|
|
377
|
+
},
|
|
340
378
|
{
|
|
341
379
|
tag: 'column',
|
|
342
380
|
width: 'auto',
|
|
@@ -366,7 +404,71 @@ function buildStatusCardDirect(state) {
|
|
|
366
404
|
}
|
|
367
405
|
|
|
368
406
|
/**
|
|
369
|
-
*
|
|
407
|
+
* 模型选择卡片(直接 JSON)
|
|
408
|
+
* 每个模型渲染为一行:左侧显示名称与说明,右侧显示切换按钮
|
|
409
|
+
*/
|
|
410
|
+
function buildModelSelectCardDirect(currentModel) {
|
|
411
|
+
const current = currentModel || 'sonnet';
|
|
412
|
+
|
|
413
|
+
const rows = [];
|
|
414
|
+
MODEL_OPTIONS.forEach((m, i) => {
|
|
415
|
+
const isCurrent = m.key === current;
|
|
416
|
+
rows.push({
|
|
417
|
+
tag: 'column_set',
|
|
418
|
+
flex_mode: 'stretch',
|
|
419
|
+
horizontal_spacing: '12px',
|
|
420
|
+
horizontal_align: 'left',
|
|
421
|
+
margin: '4px 0px 4px 0px',
|
|
422
|
+
columns: [
|
|
423
|
+
{
|
|
424
|
+
tag: 'column',
|
|
425
|
+
width: 'weighted',
|
|
426
|
+
weight: 1,
|
|
427
|
+
vertical_spacing: '4px',
|
|
428
|
+
horizontal_align: 'left',
|
|
429
|
+
vertical_align: 'center',
|
|
430
|
+
elements: [{
|
|
431
|
+
tag: 'markdown',
|
|
432
|
+
content: `${isCurrent ? '✅ ' : ''}**${m.label}**\n${m.desc}`,
|
|
433
|
+
}],
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
tag: 'column',
|
|
437
|
+
width: 'auto',
|
|
438
|
+
vertical_spacing: '4px',
|
|
439
|
+
horizontal_align: 'right',
|
|
440
|
+
vertical_align: 'center',
|
|
441
|
+
elements: [{
|
|
442
|
+
tag: 'button',
|
|
443
|
+
type: isCurrent ? 'primary_filled' : 'default',
|
|
444
|
+
width: 'default',
|
|
445
|
+
margin: '0px 0px 0px 0px',
|
|
446
|
+
text: { tag: 'plain_text', content: isCurrent ? '当前' : '切换' },
|
|
447
|
+
disabled: isCurrent,
|
|
448
|
+
behaviors: [{ type: 'callback', value: { action: 'set_claude_model', model: m.key } }],
|
|
449
|
+
}],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
});
|
|
453
|
+
if (i < MODEL_OPTIONS.length - 1) rows.push({ tag: 'hr' });
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const card = {
|
|
457
|
+
schema: '2.0',
|
|
458
|
+
config: { update_multi: true },
|
|
459
|
+
body: { direction: 'vertical', elements: rows },
|
|
460
|
+
header: {
|
|
461
|
+
title: { tag: 'plain_text', content: '模型选择' },
|
|
462
|
+
subtitle: { tag: 'plain_text', content: `当前:${MODEL_LABELS[current] || current}` },
|
|
463
|
+
template: 'purple',
|
|
464
|
+
icon: { tag: 'standard_icon', token: 'robot_outlined' },
|
|
465
|
+
padding: '12px 8px 12px 8px',
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
return JSON.stringify(card);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
370
472
|
* 使用下拉选择组件(select_static),用户从下拉框选取后点击「确认」提交。
|
|
371
473
|
*
|
|
372
474
|
* @param {string} title 卡片标题
|
|
@@ -929,10 +1031,12 @@ function splitMessage(text, maxLen = 4000) {
|
|
|
929
1031
|
|
|
930
1032
|
export {
|
|
931
1033
|
MODE_LABELS,
|
|
1034
|
+
MODEL_LABELS,
|
|
932
1035
|
stripProjectPrefix,
|
|
933
1036
|
buildToolApprovalCardDirect,
|
|
934
1037
|
buildToolApprovalResultCardDirect,
|
|
935
1038
|
buildModeSelectCardDirect,
|
|
1039
|
+
buildModelSelectCardDirect,
|
|
936
1040
|
buildStatusCardDirect,
|
|
937
1041
|
buildListCardDirect,
|
|
938
1042
|
buildDeleteListCardDirect,
|
|
@@ -28,8 +28,10 @@ import { getProjects, getSessions } from '../../projects.js';
|
|
|
28
28
|
import { JWT_SECRET } from '../../middleware/auth.js';
|
|
29
29
|
import {
|
|
30
30
|
MODE_LABELS,
|
|
31
|
+
MODEL_LABELS,
|
|
31
32
|
stripProjectPrefix,
|
|
32
33
|
buildModeSelectCardDirect,
|
|
34
|
+
buildModelSelectCardDirect,
|
|
33
35
|
buildStatusCardDirect,
|
|
34
36
|
buildListCardDirect,
|
|
35
37
|
buildDeleteListCardDirect,
|
|
@@ -71,6 +73,7 @@ export class CommandHandler {
|
|
|
71
73
|
case '/project': return this._handleProject(feishuOpenId, chatId, messageId, args);
|
|
72
74
|
case '/delete': return this._handleDeleteSession(feishuOpenId, chatId, messageId);
|
|
73
75
|
case '/mode': return this._handleMode(feishuOpenId, chatId, messageId, args[0]);
|
|
76
|
+
case '/model': return this._handleModel(feishuOpenId, chatId, messageId, args[0]);
|
|
74
77
|
case '/stop': return this._handleStop(feishuOpenId, chatId, messageId);
|
|
75
78
|
case '/help': return this._handleHelp(feishuOpenId, chatId, messageId);
|
|
76
79
|
default:
|
|
@@ -378,10 +381,35 @@ export class CommandHandler {
|
|
|
378
381
|
return true;
|
|
379
382
|
}
|
|
380
383
|
|
|
381
|
-
async
|
|
384
|
+
async _handleModel(feishuOpenId, chatId, messageId, arg) {
|
|
382
385
|
const binding = feishuDb.getBinding(feishuOpenId);
|
|
383
386
|
if (!binding) { return this._requireAuth(chatId, messageId); }
|
|
384
387
|
|
|
388
|
+
const state = feishuDb.getSessionState(feishuOpenId) || {};
|
|
389
|
+
|
|
390
|
+
if (!arg) {
|
|
391
|
+
const card = buildModelSelectCardDirect(state.claude_model || 'sonnet');
|
|
392
|
+
await this._replyCard(chatId, messageId, card, 'interactive');
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const VALID_MODELS = new Set(['sonnet', 'opus', 'haiku']);
|
|
397
|
+
if (!VALID_MODELS.has(arg)) {
|
|
398
|
+
await this._reply(chatId, messageId,
|
|
399
|
+
`❌ 不支持的模型 \`${arg}\`\n可用:${[...VALID_MODELS].join(', ')}`
|
|
400
|
+
);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
feishuDb.updateSessionState(feishuOpenId, { claude_model: arg });
|
|
405
|
+
await this._reply(chatId, messageId,
|
|
406
|
+
`✅ 已切换到 **${MODEL_LABELS[arg]}** (\`${arg}\`)`
|
|
407
|
+
);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async _handleDeleteSession(feishuOpenId, chatId, messageId) {
|
|
412
|
+
|
|
385
413
|
const state = feishuDb.getSessionState(feishuOpenId) || {};
|
|
386
414
|
const cwd = state.cwd;
|
|
387
415
|
|
|
@@ -16,6 +16,7 @@ import { resolveToolApproval } from '../../claude-sdk.js';
|
|
|
16
16
|
import {
|
|
17
17
|
buildToolApprovalResultCardDirect,
|
|
18
18
|
buildModeSelectCardDirect,
|
|
19
|
+
buildModelSelectCardDirect,
|
|
19
20
|
buildStatusCardDirect,
|
|
20
21
|
buildListCardDirect,
|
|
21
22
|
buildDeleteResultCard,
|
|
@@ -275,6 +276,25 @@ export class FeishuEngine {
|
|
|
275
276
|
return toCardResponse(cardStr);
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
case 'show_model_select': {
|
|
280
|
+
const state = feishuDb.getSessionState(feishuOpenId) || {};
|
|
281
|
+
const cardStr = buildModelSelectCardDirect(state.claude_model || 'sonnet');
|
|
282
|
+
return toCardResponse(cardStr);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'set_claude_model': {
|
|
286
|
+
const model = action.model;
|
|
287
|
+
const validModels = new Set(['sonnet', 'opus', 'haiku']);
|
|
288
|
+
if (!validModels.has(model)) return {};
|
|
289
|
+
feishuDb.updateSessionState(feishuOpenId, { claude_model: model });
|
|
290
|
+
const cardStr = buildModelSelectCardDirect(model);
|
|
291
|
+
return toCardResponse(cardStr);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
case 'show_sessions':
|
|
295
|
+
await this.commandHandler._handleList(feishuOpenId, feishuOpenId, null);
|
|
296
|
+
return {};
|
|
297
|
+
|
|
278
298
|
case 'new_session': {
|
|
279
299
|
feishuDb.clearSession(feishuOpenId);
|
|
280
300
|
const state = feishuDb.getSessionState(feishuOpenId) || {};
|
|
@@ -426,7 +426,7 @@ async function runQuery({
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
const { claude_session_id, cwd, permission_mode } = state || {};
|
|
429
|
+
const { claude_session_id, cwd, permission_mode, claude_model } = state || {};
|
|
430
430
|
|
|
431
431
|
const writer = new FakeSendWriter({
|
|
432
432
|
feishuOpenId,
|
|
@@ -449,6 +449,7 @@ async function runQuery({
|
|
|
449
449
|
userUuid,
|
|
450
450
|
cwd: cwd || undefined,
|
|
451
451
|
permissionMode: permission_mode || 'default',
|
|
452
|
+
model: claude_model || undefined,
|
|
452
453
|
images: imageOptions,
|
|
453
454
|
toolsSettings: { allowedTools: [], disallowedTools: [], skipPermissions: false },
|
|
454
455
|
appendSystemPrompt: FEISHU_SYSTEM_PROMPT,
|