@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian2018cs/agenthub",
3
- "version": "0.1.46",
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.69",
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",
@@ -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
  }
@@ -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
- if (descMatch) description = descMatch[1].trim();
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
- * 列表卡片(直接 JSON)
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 _handleDeleteSession(feishuOpenId, chatId, messageId) {
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,