@neomei/opencode-feishu 0.4.2 → 0.6.1
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/dist/core/message-handler.d.ts +12 -4
- package/dist/core/message-handler.d.ts.map +1 -1
- package/dist/core/message-handler.js +492 -143
- package/dist/core/message-handler.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +3 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/feishu/api.d.ts +1 -1
- package/dist/feishu/api.d.ts.map +1 -1
- package/dist/feishu/api.js +6 -6
- package/dist/feishu/api.js.map +1 -1
- package/dist/feishu/card.d.ts +59 -0
- package/dist/feishu/card.d.ts.map +1 -1
- package/dist/feishu/card.js +327 -48
- package/dist/feishu/card.js.map +1 -1
- package/dist/feishu/event-source.d.ts.map +1 -1
- package/dist/feishu/event-source.js +21 -1
- package/dist/feishu/event-source.js.map +1 -1
- package/dist/opencode/client.d.ts +13 -1
- package/dist/opencode/client.d.ts.map +1 -1
- package/dist/opencode/client.js +84 -3
- package/dist/opencode/client.js.map +1 -1
- package/dist/opencode/event-handler.d.ts.map +1 -1
- package/dist/opencode/event-handler.js +37 -3
- package/dist/opencode/event-handler.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
228
|
+
await this.handleRestartCommand(chatId, 'all');
|
|
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
|
-
//
|
|
234
|
-
|
|
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.
|
|
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
|
-
|
|
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, 'all');
|
|
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
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
740
|
-
if (
|
|
741
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
-
|
|
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,72 @@ 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
|
-
*
|
|
1262
|
+
* Supports: 'serve' (default), 'feishu', 'all'.
|
|
1263
|
+
* Uses fire-and-forget detached spawn so the restart script can outlive this process.
|
|
902
1264
|
*/
|
|
903
|
-
async handleRestartCommand(chatId,
|
|
904
|
-
const { execSync } = await import('child_process');
|
|
1265
|
+
async handleRestartCommand(chatId, target = 'serve') {
|
|
1266
|
+
const { execSync, spawn } = await import('child_process');
|
|
1267
|
+
const path = await import('path');
|
|
1268
|
+
const targetLabels = {
|
|
1269
|
+
serve: 'OpenCode 服务',
|
|
1270
|
+
feishu: '飞书连接器',
|
|
1271
|
+
all: '全部服务',
|
|
1272
|
+
};
|
|
1273
|
+
const label = targetLabels[target] || target;
|
|
1274
|
+
// Map target to script name
|
|
1275
|
+
const scriptNames = {
|
|
1276
|
+
serve: 'restart-serve.sh',
|
|
1277
|
+
feishu: 'restart-feishu.sh',
|
|
1278
|
+
all: 'restart-all.sh',
|
|
1279
|
+
};
|
|
1280
|
+
const scriptName = scriptNames[target];
|
|
1281
|
+
if (!scriptName) {
|
|
1282
|
+
await this.feishuApi.sendText(chatId, `❌ 不支持的重启目标: ${target}`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
905
1285
|
try {
|
|
906
|
-
await this.feishuApi.sendText(chatId,
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1286
|
+
await this.feishuApi.sendText(chatId, `🔄 正在重启 ${label}...`);
|
|
1287
|
+
// Find the restart script
|
|
1288
|
+
const possiblePaths = [
|
|
1289
|
+
path.join(process.cwd(), 'connectors', 'feishu', scriptName),
|
|
1290
|
+
path.join(this.opencode.getDirectory(), 'connectors', 'feishu', scriptName),
|
|
1291
|
+
];
|
|
1292
|
+
let scriptPath = '';
|
|
1293
|
+
for (const p of possiblePaths) {
|
|
911
1294
|
try {
|
|
912
|
-
execSync(`
|
|
913
|
-
|
|
914
|
-
|
|
1295
|
+
execSync(`test -f "${p}"`, { stdio: 'ignore' });
|
|
1296
|
+
scriptPath = p;
|
|
1297
|
+
break;
|
|
915
1298
|
}
|
|
916
1299
|
catch {
|
|
917
|
-
|
|
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 服务重启失败,请检查日志');
|
|
1300
|
+
continue;
|
|
934
1301
|
}
|
|
935
1302
|
}
|
|
936
|
-
|
|
937
|
-
log.error({
|
|
938
|
-
await this.feishuApi.sendText(chatId,
|
|
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
|
-
}
|
|
1303
|
+
if (!scriptPath) {
|
|
1304
|
+
log.error(`${scriptName} not found`);
|
|
1305
|
+
await this.feishuApi.sendText(chatId, `❌ 找不到重启脚本 connectors/feishu/${scriptName}`);
|
|
1306
|
+
return;
|
|
968
1307
|
}
|
|
1308
|
+
// Fire-and-forget: spawn the restart script and don't wait for result
|
|
1309
|
+
const child = spawn('bash', [scriptPath, '19876'], {
|
|
1310
|
+
detached: true,
|
|
1311
|
+
stdio: 'ignore',
|
|
1312
|
+
});
|
|
1313
|
+
child.unref();
|
|
1314
|
+
log.info({ scriptPath, target }, 'Restart script spawned');
|
|
969
1315
|
}
|
|
970
1316
|
catch (err) {
|
|
971
|
-
log.error({ err }, 'Restart command failed');
|
|
972
|
-
|
|
1317
|
+
log.error({ err, target }, 'Restart command failed');
|
|
1318
|
+
try {
|
|
1319
|
+
await this.feishuApi.sendText(chatId, `❌ 重启失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1320
|
+
}
|
|
1321
|
+
catch { /* ignore */ }
|
|
973
1322
|
}
|
|
974
1323
|
}
|
|
975
1324
|
}
|