@neomei/opencode-feishu 0.4.2 → 0.6.0

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.
@@ -13,6 +13,8 @@ export class MessageHandler {
13
13
  fileDownloader;
14
14
  botName;
15
15
  availableCommands = new Map();
16
+ chatModelOverrides = new Map();
17
+ lastKnownModel;
16
18
  constructor(config, sessionManager, feishuApi, opencode, botName = 'opencode') {
17
19
  this.config = config;
18
20
  this.sessionManager = sessionManager;
@@ -60,6 +62,30 @@ export class MessageHandler {
60
62
  log.error({ err }, 'Failed to load available commands');
61
63
  }
62
64
  }
65
+ async syncModelOverride(chatId) {
66
+ try {
67
+ const fs = await import('fs');
68
+ const path = await import('path');
69
+ const configPath = path.join(this.opencode.getDirectory(), '.opencode', 'config.json');
70
+ if (!fs.existsSync(configPath))
71
+ return;
72
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
73
+ const currentModel = config?.model;
74
+ if (!currentModel)
75
+ return;
76
+ if (this.lastKnownModel !== currentModel) {
77
+ const [providerID, modelID] = currentModel.split('/');
78
+ if (providerID && modelID) {
79
+ this.chatModelOverrides.set(chatId, { providerID, modelID });
80
+ log.info({ chatId, model: currentModel }, 'Model changed, override set');
81
+ }
82
+ this.lastKnownModel = currentModel;
83
+ }
84
+ }
85
+ catch (err) {
86
+ log.warn({ err }, 'Failed to sync model override');
87
+ }
88
+ }
63
89
  async handleMessage(message) {
64
90
  try {
65
91
  log.info({
@@ -199,7 +225,7 @@ export class MessageHandler {
199
225
  // Handle admin restart commands - only exact slash commands
200
226
  const trimmedText = text.trim().toLowerCase();
201
227
  if (trimmedText === '/restart' || trimmedText === '/重启') {
202
- await this.handleRestartCommand(chatId, text.trim());
228
+ await this.handleRestartCommand(chatId);
203
229
  return;
204
230
  }
205
231
  // Check for pending interaction reply before proceeding with normal message flow
@@ -230,16 +256,8 @@ export class MessageHandler {
230
256
  return;
231
257
  }
232
258
  this.sessionManager.updateStatus(chatId, 'busy');
233
- // Show thinking card immediately before sending prompt
234
- if (this.config.showProcess !== 'none') {
235
- const thinkingCard = FeishuCard.createThinkingCard(this.botName);
236
- log.info({ chatId }, 'Sending thinking card');
237
- const msg = await this.feishuApi.sendCard(chatId, thinkingCard);
238
- log.info({ chatId, messageId: msg?.message_id }, 'Thinking card sent result');
239
- if (msg?.message_id) {
240
- this.sessionManager.setCurrentMessage(chatId, msg.message_id);
241
- }
242
- }
259
+ // Skip separate thinking card the streaming card (created on first flushCard)
260
+ // already shows "💭 点点思考中..." as its header with no blank body.
243
261
  // Send message to OpenCode
244
262
  try {
245
263
  const slashCommand = this.parseSlashCommand(text);
@@ -313,7 +331,13 @@ export class MessageHandler {
313
331
  `- drive.v1.media.uploadPrepare/uploadFinish — 上传文件\n` +
314
332
  `当用户请求创建飞书文档时,请直接调用 MCP 工具创建,不要在回复中询问。\n` +
315
333
  `重要:飞书文档的访问链接必须使用 https://www.feishu.cn/docx/ 域名,不要使用 https://open.feishu.cn/docx/ 域名。\n\n`;
316
- await this.opencode.sendPrompt(session.id, contextPrefix + text, files.length > 0 ? files : undefined, this.config.thinkingLanguage);
334
+ await this.syncModelOverride(chatId);
335
+ const modelOverride = this.chatModelOverrides.get(chatId);
336
+ if (modelOverride) {
337
+ session.currentModel = `${modelOverride.providerID}/${modelOverride.modelID}`;
338
+ log.info({ chatId, model: session.currentModel }, 'Sent prompt with model override');
339
+ }
340
+ await this.opencode.sendPrompt(session.id, contextPrefix + text, files.length > 0 ? files : undefined, this.config.thinkingLanguage, modelOverride);
317
341
  log.info('Prompt sent successfully');
318
342
  }
319
343
  }
@@ -464,6 +488,30 @@ export class MessageHandler {
464
488
  if (actionType === 'nav') {
465
489
  return this.handleNavigationCardAction(chatId, messageId, value);
466
490
  }
491
+ // Handle model selection
492
+ if (actionType === 'model') {
493
+ return this.handleModelCardAction(chatId, messageId, value, action.action.option);
494
+ }
495
+ // Handle agent selection
496
+ if (actionType === 'agent') {
497
+ return this.handleAgentCardAction(chatId, messageId, value, action.action.option);
498
+ }
499
+ // Handle session operations (switch/share/delete)
500
+ if (actionType === 'sess') {
501
+ return this.handleSessionCardAction(chatId, messageId, value);
502
+ }
503
+ // Handle status refresh
504
+ if (actionType === 'status') {
505
+ return this.handleStatusCardAction(chatId, messageId, value);
506
+ }
507
+ // Handle task control (interrupt/abort)
508
+ if (actionType === 'ctrl') {
509
+ return this.handleCtrlCardAction(chatId, messageId, value);
510
+ }
511
+ // Handle command execution
512
+ if (actionType === 'cmd') {
513
+ return this.handleCommandCardAction(chatId, messageId, value);
514
+ }
467
515
  if (actionType !== 'perm' && actionType !== 'q') {
468
516
  log.warn({ chatId, actionType, value }, 'Unknown card action type');
469
517
  return { toast: { type: 'error', content: '不支持的操作' } };
@@ -669,9 +717,259 @@ export class MessageHandler {
669
717
  return { toast: { type: 'error', content: '执行失败' } };
670
718
  }
671
719
  }
672
- /**
673
- * Handle list commands (models, agents, commands, sessions) by fetching data from OpenCode.
674
- */
720
+ async handleModelCardAction(chatId, messageId, value, option) {
721
+ let modelKey;
722
+ if (option) {
723
+ modelKey = option;
724
+ }
725
+ else {
726
+ const providerID = value.providerID;
727
+ const modelID = value.modelID;
728
+ if (!providerID || !modelID) {
729
+ log.warn({ chatId, messageId, value }, 'Model action missing providerID or modelID');
730
+ return { toast: { type: 'error', content: '无效的模型选择' } };
731
+ }
732
+ modelKey = `${providerID}/${modelID}`;
733
+ }
734
+ const [providerID, modelID] = modelKey.split('/');
735
+ if (!providerID || !modelID) {
736
+ log.warn({ chatId, messageId, modelKey }, 'Invalid model key format');
737
+ return { toast: { type: 'error', content: '无效的模型格式' } };
738
+ }
739
+ log.info({ chatId, messageId, modelKey }, 'Switching model');
740
+ const session = this.sessionManager.getSession(chatId);
741
+ if (session) {
742
+ session.currentModel = modelKey;
743
+ session.modelSelection = undefined;
744
+ session.interactionReplied = true;
745
+ if (messageId !== session.currentMessageId) {
746
+ this.sessionManager.setCurrentMessage(chatId, messageId);
747
+ }
748
+ }
749
+ try {
750
+ await this.opencode.updateConfig({ model: modelKey });
751
+ this.chatModelOverrides.set(chatId, { providerID, modelID });
752
+ log.info({ chatId, modelKey }, 'Model switched, override set for next prompt');
753
+ const doneCard = FeishuCard.createStreamingCard({
754
+ content: '',
755
+ botName: this.botName,
756
+ done: true,
757
+ currentModel: modelKey,
758
+ showProcess: this.config.showProcess || 'none',
759
+ });
760
+ return { toast: { type: 'success', content: `已切换到 ${modelKey}` }, card: doneCard };
761
+ }
762
+ catch (err) {
763
+ log.error({ err, chatId, modelKey }, 'Failed to switch model');
764
+ return { toast: { type: 'error', content: '切换模型失败' } };
765
+ }
766
+ }
767
+ async handleAgentCardAction(chatId, messageId, value, option) {
768
+ const agentName = option || value.name;
769
+ if (!agentName) {
770
+ log.warn({ chatId, messageId, value }, 'Agent action missing name');
771
+ return { toast: { type: 'error', content: '无效的代理选择' } };
772
+ }
773
+ log.info({ chatId, messageId, agentName }, 'Switching agent');
774
+ const session = this.sessionManager.getSession(chatId);
775
+ if (session) {
776
+ session.currentAgent = agentName;
777
+ session.agentSelection = undefined;
778
+ session.interactionReplied = true;
779
+ if (messageId !== session.currentMessageId) {
780
+ this.sessionManager.setCurrentMessage(chatId, messageId);
781
+ }
782
+ }
783
+ try {
784
+ await this.opencode.updateConfig({ default_agent: agentName });
785
+ log.info({ chatId, agentName }, 'Agent switched');
786
+ const doneCard = FeishuCard.createStreamingCard({
787
+ content: '',
788
+ botName: this.botName,
789
+ done: true,
790
+ currentAgent: agentName,
791
+ showProcess: this.config.showProcess || 'none',
792
+ });
793
+ return { toast: { type: 'success', content: `已切换到代理 ${agentName}` }, card: doneCard };
794
+ }
795
+ catch (err) {
796
+ log.error({ err, chatId, agentName }, 'Failed to switch agent');
797
+ return { toast: { type: 'error', content: '切换代理失败' } };
798
+ }
799
+ }
800
+ deduplicateAndSortSessions(sessions) {
801
+ let sessList = Array.isArray(sessions)
802
+ ? sessions.map((s) => ({ id: s.id, title: s.title, created: s.time?.created || 0, updated: s.time?.updated || 0 }))
803
+ : [];
804
+ const titleMap = new Map();
805
+ for (const sess of sessList) {
806
+ const key = sess.title || sess.id;
807
+ const existing = titleMap.get(key);
808
+ if (!existing || sess.updated > existing.updated) {
809
+ titleMap.set(key, sess);
810
+ }
811
+ }
812
+ sessList = Array.from(titleMap.values());
813
+ sessList.sort((a, b) => b.updated - a.updated);
814
+ return sessList.slice(0, 20);
815
+ }
816
+ async handleSessionCardAction(chatId, messageId, value) {
817
+ const op = value.op;
818
+ const sessionId = value.id;
819
+ const title = value.title;
820
+ if (!op || !sessionId) {
821
+ return { toast: { type: 'error', content: '无效操作' } };
822
+ }
823
+ log.info({ chatId, messageId, op, sessionId }, 'Session card action');
824
+ try {
825
+ if (op === 'switch') {
826
+ await this.opencode.selectSession(sessionId);
827
+ const displayTitle = title || sessionId.substring(0, 16);
828
+ const doneCard = FeishuCard.createSessionSwitchedCard(displayTitle);
829
+ return { toast: { type: 'success', content: `已切换到 ${displayTitle}` }, card: doneCard };
830
+ }
831
+ if (op === 'delete') {
832
+ await this.opencode.deleteSession(sessionId);
833
+ const sessions = await this.opencode.getSessions();
834
+ const sessList = this.deduplicateAndSortSessions(sessions);
835
+ const currentSession = this.sessionManager.getSession(chatId);
836
+ const card = FeishuCard.createSessionsCard({
837
+ sessions: sessList,
838
+ currentSessionId: currentSession?.id || '',
839
+ });
840
+ return { toast: { type: 'success', content: `已删除 ${title || sessionId.substring(0, 8)}` }, card };
841
+ }
842
+ return { toast: { type: 'error', content: `未知操作: ${op}` } };
843
+ }
844
+ catch (err) {
845
+ log.error({ err, chatId, op, sessionId }, 'Session card action failed');
846
+ return { toast: { type: 'error', content: `操作失败: ${err instanceof Error ? err.message : String(err)}` } };
847
+ }
848
+ }
849
+ async handleStatusCardAction(chatId, messageId, value) {
850
+ const op = value.op;
851
+ log.info({ chatId, messageId, op }, 'Status card action');
852
+ try {
853
+ let branch;
854
+ let commit;
855
+ let files = [];
856
+ try {
857
+ const vcsInfo = await this.opencode.getVcsInfo();
858
+ branch = vcsInfo?.branch;
859
+ commit = vcsInfo?.commit || vcsInfo?.hash;
860
+ }
861
+ catch { /* vcs not available */ }
862
+ try {
863
+ const fileStatus = await this.opencode.getStatus();
864
+ if (Array.isArray(fileStatus)) {
865
+ files = fileStatus;
866
+ }
867
+ else if (fileStatus?.files) {
868
+ files = fileStatus.files;
869
+ }
870
+ }
871
+ catch { /* file status not available */ }
872
+ const card = FeishuCard.createStatusCard({ branch, commit, files });
873
+ await this.feishuApi.updateCard(messageId, card);
874
+ return { toast: { type: 'success', content: '状态已刷新' } };
875
+ }
876
+ catch (err) {
877
+ log.error({ err, chatId }, 'Status refresh failed');
878
+ return { toast: { type: 'error', content: '刷新失败' } };
879
+ }
880
+ }
881
+ async handleCtrlCardAction(chatId, messageId, value) {
882
+ const op = value.op;
883
+ if (!op) {
884
+ return { toast: { type: 'error', content: '无效操作' } };
885
+ }
886
+ const session = this.sessionManager.getSession(chatId);
887
+ if (!session) {
888
+ return { toast: { type: 'error', content: '无活跃会话' } };
889
+ }
890
+ log.info({ chatId, messageId, op, sessionId: session.id }, 'Task control action');
891
+ try {
892
+ if (op === 'interrupt') {
893
+ await this.opencode.abortSession(session.id);
894
+ return { toast: { type: 'info', content: '已暂停当前任务' } };
895
+ }
896
+ if (op === 'abort') {
897
+ await this.opencode.abortSession(session.id);
898
+ return { toast: { type: 'warning', content: '已终止任务' } };
899
+ }
900
+ return { toast: { type: 'error', content: `未知操作: ${op}` } };
901
+ }
902
+ catch (err) {
903
+ log.error({ err, chatId, op }, 'Task control failed');
904
+ return { toast: { type: 'error', content: `操作失败: ${err instanceof Error ? err.message : String(err)}` } };
905
+ }
906
+ }
907
+ async handleCommandCardAction(chatId, messageId, value) {
908
+ const cmdName = value.name;
909
+ if (!cmdName) {
910
+ return { toast: { type: 'error', content: '无效命令' } };
911
+ }
912
+ log.info({ chatId, messageId, cmdName }, 'Command card action');
913
+ // Handle /restart specially
914
+ if (cmdName === 'restart') {
915
+ await this.handleRestartCommand(chatId);
916
+ return { toast: { type: 'success', content: '正在重启...' } };
917
+ }
918
+ // Auto-create session if needed
919
+ let session = this.sessionManager.getSession(chatId);
920
+ if (!session) {
921
+ try {
922
+ session = await this.sessionManager.getOrCreateSession(chatId, 'p2p');
923
+ }
924
+ catch (err) {
925
+ return { toast: { type: 'error', content: `创建会话失败: ${err instanceof Error ? err.message : String(err)}` } };
926
+ }
927
+ }
928
+ // List commands
929
+ const listCommands = ['models', 'agents', 'commands', 'sessions', 'status'];
930
+ if (listCommands.includes(cmdName)) {
931
+ try {
932
+ await this.handleListCommand(chatId, session.id, cmdName, '');
933
+ return { toast: { type: 'success', content: `已执行 /${cmdName}` } };
934
+ }
935
+ catch (err) {
936
+ return { toast: { type: 'error', content: `执行失败: ${err instanceof Error ? err.message : String(err)}` } };
937
+ }
938
+ }
939
+ // Handle /new - create a new session
940
+ if (cmdName === 'new') {
941
+ try {
942
+ const newSession = await this.opencode.createSession('Feishu p2p ' + chatId);
943
+ return { toast: { type: 'success', content: `已创建新会话: ${(newSession.id || '').substring(0, 12)}...` } };
944
+ }
945
+ catch (err) {
946
+ return { toast: { type: 'error', content: `创建会话失败: ${err instanceof Error ? err.message : String(err)}` } };
947
+ }
948
+ }
949
+ const shortcuts = {
950
+ 'share': 'session.share',
951
+ 'interrupt': 'session.interrupt',
952
+ 'compact': 'session.compact',
953
+ };
954
+ const fullCmdName = shortcuts[cmdName] || cmdName;
955
+ try {
956
+ const commandInfo = this.availableCommands.get(fullCmdName);
957
+ if (commandInfo?.type === 'tui') {
958
+ await this.opencode.executeTuiCommand(fullCmdName);
959
+ }
960
+ else if (commandInfo?.type === 'custom') {
961
+ await this.opencode.sendPrompt(session.id, `/${cmdName}`);
962
+ }
963
+ else {
964
+ await this.opencode.sendCommand(session.id, fullCmdName, '');
965
+ }
966
+ return { toast: { type: 'success', content: `已执行 /${cmdName}` } };
967
+ }
968
+ catch (err) {
969
+ log.error({ err, chatId, cmdName }, 'Command execution failed');
970
+ return { toast: { type: 'error', content: `执行失败: ${err instanceof Error ? err.message : String(err)}` } };
971
+ }
972
+ }
675
973
  /**
676
974
  * Handle list commands (models, agents, commands, sessions) by fetching data from OpenCode.
677
975
  */
@@ -681,74 +979,130 @@ export class MessageHandler {
681
979
  let message = '';
682
980
  switch (command) {
683
981
  case 'models': {
684
- const providers = await this.opencode.getProviders();
685
- message = '**🤖 可用模型列表**\n\n';
686
- if (providers && Array.isArray(providers)) {
687
- for (const provider of providers) {
688
- message += `**${provider.name || provider.id || 'Unknown'}**\n`;
689
- if (provider.models && Array.isArray(provider.models)) {
690
- for (const model of provider.models) {
691
- message += `- ${model.id || model.name || 'Unknown'}`;
692
- if (model.default)
693
- message += ' (默认)';
694
- message += '\n';
695
- }
696
- }
697
- message += '\n';
698
- }
982
+ const config = await this.opencode.getConfig();
983
+ const currentModel = config?.model || undefined;
984
+ const providerData = await this.opencode.listProviders();
985
+ const allProviders = providerData?.all || providerData?.providers || [];
986
+ const connected = new Set(providerData?.connected || []);
987
+ const cardProviders = allProviders
988
+ .filter((p) => connected.has(p.id))
989
+ .map((p) => ({
990
+ id: p.id,
991
+ name: p.name || p.id,
992
+ models: p.models
993
+ ? Object.entries(p.models).map(([id, m]) => ({
994
+ id,
995
+ name: m.name || id,
996
+ }))
997
+ : [],
998
+ }))
999
+ .filter((p) => p.models.length > 0);
1000
+ if (cardProviders.length === 0) {
1001
+ message = '暂无可用模型';
1002
+ await this.feishuApi.sendText(chatId, message);
699
1003
  }
700
1004
  else {
701
- message += '暂无模型信息\n';
1005
+ const session = this.sessionManager.getSession(chatId);
1006
+ if (session) {
1007
+ session.modelSelection = { providers: cardProviders, currentModel };
1008
+ }
1009
+ const card = FeishuCard.createStreamingCard({
1010
+ content: '',
1011
+ botName: this.botName,
1012
+ done: false,
1013
+ showProcess: this.config.showProcess || 'none',
1014
+ modelSelection: { providers: cardProviders, currentModel },
1015
+ });
1016
+ if (session?.currentMessageId) {
1017
+ await this.feishuApi.updateCard(session.currentMessageId, card);
1018
+ }
1019
+ else {
1020
+ const msg = await this.feishuApi.sendCard(chatId, card);
1021
+ if (msg?.message_id && session) {
1022
+ this.sessionManager.setCurrentMessage(chatId, msg.message_id);
1023
+ }
1024
+ }
702
1025
  }
703
- break;
1026
+ this.sessionManager.updateStatus(chatId, 'idle');
1027
+ log.info({ command }, `${command} rendered in streaming card`);
1028
+ return;
704
1029
  }
705
1030
  case 'agents': {
706
1031
  const agents = await this.opencode.getAgents();
707
- message = '**🎯 可用代理列表**\n\n';
708
- if (agents && Array.isArray(agents)) {
709
- for (const agent of agents) {
710
- message += `- ${agent.name || agent.id || 'Unknown'}`;
711
- if (agent.description)
712
- message += `: ${agent.description}`;
713
- message += '\n';
714
- }
1032
+ const agentList = Array.isArray(agents)
1033
+ ? agents.filter((a) => a.mode !== 'subagent' && !a.hidden)
1034
+ : [];
1035
+ if (agentList.length === 0) {
1036
+ message = '暂无可用代理';
1037
+ await this.feishuApi.sendText(chatId, message);
715
1038
  }
716
1039
  else {
717
- message += '暂无代理信息\n';
1040
+ const config = await this.opencode.getConfig();
1041
+ const currentAgent = config?.default_agent || config?.agent?.build?.name || 'build';
1042
+ const session = this.sessionManager.getSession(chatId);
1043
+ const agentItems = agentList.map((a) => ({
1044
+ name: a.name,
1045
+ description: a.description,
1046
+ mode: a.mode,
1047
+ }));
1048
+ if (session) {
1049
+ session.agentSelection = { agents: agentItems, currentAgent };
1050
+ }
1051
+ const card = FeishuCard.createStreamingCard({
1052
+ content: '',
1053
+ botName: this.botName,
1054
+ done: false,
1055
+ showProcess: this.config.showProcess || 'none',
1056
+ agentSelection: { agents: agentItems, currentAgent },
1057
+ });
1058
+ if (session?.currentMessageId) {
1059
+ await this.feishuApi.updateCard(session.currentMessageId, card);
1060
+ }
1061
+ else {
1062
+ const msg = await this.feishuApi.sendCard(chatId, card);
1063
+ if (msg?.message_id && session) {
1064
+ this.sessionManager.setCurrentMessage(chatId, msg.message_id);
1065
+ }
1066
+ }
718
1067
  }
719
- break;
1068
+ this.sessionManager.updateStatus(chatId, 'idle');
1069
+ log.info({ command }, `${command} rendered in streaming card`);
1070
+ return;
720
1071
  }
721
1072
  case 'commands': {
722
- const commands = await this.opencode.getCommands();
723
- message = '**⌨️ 可用命令列表**\n\n';
724
- if (commands && Array.isArray(commands)) {
725
- for (const cmd of commands) {
726
- message += `- /${cmd.name || cmd.id || 'Unknown'}`;
727
- if (cmd.description)
728
- message += `: ${cmd.description}`;
729
- message += '\n';
730
- }
731
- }
732
- else {
733
- message += '暂无命令信息\n';
734
- }
735
- break;
1073
+ const cmdList = [
1074
+ { name: 'models', description: '选择模型' },
1075
+ { name: 'agents', description: '选择代理' },
1076
+ { name: 'commands', description: '查看所有命令' },
1077
+ { name: 'sessions', description: '管理会话' },
1078
+ { name: 'status', description: '项目状态' },
1079
+ { name: 'new', description: '创建新会话' },
1080
+ { name: 'compact', description: '压缩上下文' },
1081
+ { name: 'restart', description: '重启服务' },
1082
+ { name: 'init', description: '初始化 AGENTS.md' },
1083
+ { name: 'review', description: '审查代码变更' },
1084
+ ];
1085
+ await this.feishuApi.sendCard(chatId, FeishuCard.createCommandsCard(cmdList));
1086
+ this.sessionManager.updateStatus(chatId, 'idle');
1087
+ this.sessionManager.clearCurrentMessage(chatId);
1088
+ return;
736
1089
  }
737
1090
  case 'sessions': {
738
1091
  const sessions = await this.opencode.getSessions();
739
- message = '**💬 会话列表**\n\n';
740
- if (sessions && Array.isArray(sessions)) {
741
- for (const sess of sessions) {
742
- message += `- ${sess.title || sess.id || 'Unknown'}`;
743
- if (sess.createdAt)
744
- message += ` (${new Date(sess.createdAt).toLocaleString()})`;
745
- message += '\n';
746
- }
1092
+ const sessList = this.deduplicateAndSortSessions(sessions);
1093
+ if (sessList.length === 0) {
1094
+ await this.feishuApi.sendText(chatId, '暂无会话');
747
1095
  }
748
1096
  else {
749
- message += '暂无会话信息\n';
1097
+ const currentSession = this.sessionManager.getSession(chatId);
1098
+ await this.feishuApi.sendCard(chatId, FeishuCard.createSessionsCard({
1099
+ sessions: sessList,
1100
+ currentSessionId: currentSession?.id || '',
1101
+ }));
750
1102
  }
751
- break;
1103
+ this.sessionManager.updateStatus(chatId, 'idle');
1104
+ this.sessionManager.clearCurrentMessage(chatId);
1105
+ return;
752
1106
  }
753
1107
  case 'tools': {
754
1108
  const tools = await this.opencode.getTools();
@@ -799,22 +1153,29 @@ export class MessageHandler {
799
1153
  break;
800
1154
  }
801
1155
  case 'status': {
802
- const status = await this.opencode.getStatus();
803
- message = '**📊 项目状态**\n\n';
804
- if (status) {
805
- message += `- 分支: ${status.branch || 'Unknown'}\n`;
806
- message += `- 提交: ${status.commit || 'Unknown'}\n`;
807
- if (status.files && Array.isArray(status.files)) {
808
- message += `- 变更文件: ${status.files.length}\n`;
809
- for (const file of status.files) {
810
- message += ` ${file.status || '?'} ${file.name || 'Unknown'}\n`;
811
- }
812
- }
1156
+ let branch;
1157
+ let commit;
1158
+ let files = [];
1159
+ try {
1160
+ const vcsInfo = await this.opencode.getVcsInfo();
1161
+ branch = vcsInfo?.branch;
1162
+ commit = vcsInfo?.commit || vcsInfo?.hash;
813
1163
  }
814
- else {
815
- message += '暂无状态信息\n';
1164
+ catch { /* vcs not available */ }
1165
+ try {
1166
+ const fileStatus = await this.opencode.getStatus();
1167
+ if (Array.isArray(fileStatus)) {
1168
+ files = fileStatus;
1169
+ }
1170
+ else if (fileStatus?.files) {
1171
+ files = fileStatus.files;
1172
+ }
816
1173
  }
817
- break;
1174
+ catch { /* file status not available */ }
1175
+ await this.feishuApi.sendCard(chatId, FeishuCard.createStatusCard({ branch, commit, files }));
1176
+ this.sessionManager.updateStatus(chatId, 'idle');
1177
+ this.sessionManager.clearCurrentMessage(chatId);
1178
+ return;
818
1179
  }
819
1180
  case 'config': {
820
1181
  const config = await this.opencode.getConfig();
@@ -892,84 +1253,54 @@ export class MessageHandler {
892
1253
  };
893
1254
  }
894
1255
  catch (err) {
895
- log.error({ err, messageId, fileKey }, `Failed to download ${typeLabel}`);
1256
+ log.error({ err, messageId, fileKey, resourceType }, `Failed to download ${typeLabel}`);
896
1257
  return { text: `[${typeLabel}消息(下载失败)]` };
897
1258
  }
898
1259
  }
899
1260
  /**
900
1261
  * Handle restart command from admin user.
901
- * Restarts opencode serve and/or the feishu plugin.
1262
+ * Simple implementation: send message and fire-and-forget the restart script.
902
1263
  */
903
- async handleRestartCommand(chatId, command) {
904
- const { execSync } = await import('child_process');
1264
+ async handleRestartCommand(chatId) {
1265
+ const { execSync, spawn } = await import('child_process');
1266
+ const path = await import('path');
905
1267
  try {
906
- await this.feishuApi.sendText(chatId, '🔄 正在重启服务...');
907
- // Restart opencode serve
908
- try {
909
- // Kill existing opencode serve processes (excluding current process)
910
- const currentPid = process.pid;
1268
+ await this.feishuApi.sendText(chatId, '🔄 正在重启 OpenCode 服务...');
1269
+ // Find the restart script
1270
+ const possiblePaths = [
1271
+ path.join(process.cwd(), 'connectors', 'feishu', 'restart-serve.sh'),
1272
+ path.join(this.opencode.getDirectory(), 'connectors', 'feishu', 'restart-serve.sh'),
1273
+ ];
1274
+ let scriptPath = '';
1275
+ for (const p of possiblePaths) {
911
1276
  try {
912
- execSync(`pgrep -f "opencode serve" | grep -v "${currentPid}" | xargs -r kill 2>/dev/null || true`, {
913
- stdio: 'ignore'
914
- });
1277
+ execSync(`test -f "${p}"`, { stdio: 'ignore' });
1278
+ scriptPath = p;
1279
+ break;
915
1280
  }
916
1281
  catch {
917
- // Ignore errors from pgrep/kill
918
- }
919
- await new Promise(resolve => setTimeout(resolve, 2000));
920
- execSync('nohup opencode serve --port 19876 > /tmp/opencode-serve.log 2>&1 &', {
921
- stdio: 'ignore'
922
- });
923
- await new Promise(resolve => setTimeout(resolve, 3000));
924
- // Verify opencode is running
925
- const isRunning = await fetch('http://localhost:19876', {
926
- method: 'GET',
927
- signal: AbortSignal.timeout(5000)
928
- }).then(r => r.ok || r.status === 404).catch(() => false);
929
- if (isRunning) {
930
- await this.feishuApi.sendText(chatId, '✅ OpenCode 服务已重启');
931
- }
932
- else {
933
- await this.feishuApi.sendText(chatId, '⚠️ OpenCode 服务重启失败,请检查日志');
1282
+ continue;
934
1283
  }
935
1284
  }
936
- catch (err) {
937
- log.error({ err }, 'Failed to restart opencode serve');
938
- await this.feishuApi.sendText(chatId, '❌ OpenCode 重启失败');
939
- }
940
- // Restart feishu plugin if requested
941
- if (command === '/restart') {
942
- await this.feishuApi.sendText(chatId, '🔄 正在重启飞书插件...');
943
- try {
944
- // Give time for message to be sent before restart
945
- setTimeout(() => {
946
- // Kill other opencode-feishu processes but exclude current process
947
- const currentPid = process.pid;
948
- try {
949
- execSync(`pgrep -f "opencode-feishu" | grep -v "${currentPid}" | xargs -r kill || true`, {
950
- stdio: 'ignore'
951
- });
952
- }
953
- catch {
954
- // Ignore errors from pgrep/kill
955
- }
956
- setTimeout(() => {
957
- execSync('nohup opencode-feishu start --daemon --serve > /tmp/feishu-plugin.log 2>&1 &', {
958
- stdio: 'ignore'
959
- });
960
- }, 2000);
961
- }, 1000);
962
- await this.feishuApi.sendText(chatId, '✅ 飞书插件重启指令已发送,插件将在几秒后恢复');
963
- }
964
- catch (err) {
965
- log.error({ err }, 'Failed to restart feishu plugin');
966
- await this.feishuApi.sendText(chatId, '❌ 飞书插件重启失败');
967
- }
1285
+ if (!scriptPath) {
1286
+ log.error('restart-serve.sh not found');
1287
+ await this.feishuApi.sendText(chatId, '❌ 找不到重启脚本 connectors/feishu/restart-serve.sh');
1288
+ return;
968
1289
  }
1290
+ // Fire-and-forget: spawn the restart script and don't wait for result
1291
+ const child = spawn('bash', [scriptPath, '19876'], {
1292
+ detached: true,
1293
+ stdio: 'ignore',
1294
+ });
1295
+ child.unref();
1296
+ log.info({ scriptPath }, 'Restart script spawned');
969
1297
  }
970
1298
  catch (err) {
971
1299
  log.error({ err }, 'Restart command failed');
972
- await this.feishuApi.sendText(chatId, '❌ 重启命令执行失败');
1300
+ try {
1301
+ await this.feishuApi.sendText(chatId, `❌ 重启失败:${err instanceof Error ? err.message : String(err)}`);
1302
+ }
1303
+ catch { /* ignore */ }
973
1304
  }
974
1305
  }
975
1306
  }