@neomei/opencode-feishu 0.2.6 → 0.2.8

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.
Files changed (49) hide show
  1. package/dist/core/config.d.ts +3 -3
  2. package/dist/core/config.js +1 -1
  3. package/dist/core/config.js.map +1 -1
  4. package/dist/core/message-handler.d.ts +25 -11
  5. package/dist/core/message-handler.d.ts.map +1 -1
  6. package/dist/core/message-handler.js +589 -102
  7. package/dist/core/message-handler.js.map +1 -1
  8. package/dist/core/session-manager.d.ts +1 -1
  9. package/dist/core/session-manager.d.ts.map +1 -1
  10. package/dist/core/session-manager.js +18 -2
  11. package/dist/core/session-manager.js.map +1 -1
  12. package/dist/core/types.d.ts +35 -2
  13. package/dist/core/types.d.ts.map +1 -1
  14. package/dist/feishu/api.d.ts +6 -0
  15. package/dist/feishu/api.d.ts.map +1 -1
  16. package/dist/feishu/api.js +71 -6
  17. package/dist/feishu/api.js.map +1 -1
  18. package/dist/feishu/card.d.ts +6 -5
  19. package/dist/feishu/card.d.ts.map +1 -1
  20. package/dist/feishu/card.js +185 -120
  21. package/dist/feishu/card.js.map +1 -1
  22. package/dist/feishu/event-source.d.ts +6 -0
  23. package/dist/feishu/event-source.d.ts.map +1 -1
  24. package/dist/feishu/event-source.js +54 -0
  25. package/dist/feishu/event-source.js.map +1 -1
  26. package/dist/opencode/client.d.ts +11 -0
  27. package/dist/opencode/client.d.ts.map +1 -1
  28. package/dist/opencode/client.js +120 -9
  29. package/dist/opencode/client.js.map +1 -1
  30. package/dist/opencode/event-handler.d.ts +14 -1
  31. package/dist/opencode/event-handler.d.ts.map +1 -1
  32. package/dist/opencode/event-handler.js +171 -25
  33. package/dist/opencode/event-handler.js.map +1 -1
  34. package/dist/plugin.d.ts.map +1 -1
  35. package/dist/plugin.js +10 -3
  36. package/dist/plugin.js.map +1 -1
  37. package/dist/services/doc-service.d.ts +11 -45
  38. package/dist/services/doc-service.d.ts.map +1 -1
  39. package/dist/services/doc-service.js +72 -174
  40. package/dist/services/doc-service.js.map +1 -1
  41. package/dist/services/im-service.d.ts.map +1 -1
  42. package/dist/services/im-service.js +10 -6
  43. package/dist/services/im-service.js.map +1 -1
  44. package/dist/standalone.d.ts.map +1 -1
  45. package/dist/standalone.js +10 -3
  46. package/dist/standalone.js.map +1 -1
  47. package/dist/types/extended.d.ts +2 -1
  48. package/dist/types/extended.d.ts.map +1 -1
  49. package/package.json +5 -5
@@ -18,12 +18,28 @@ export class OpenCodeClient {
18
18
  }
19
19
  this.client = createOpencodeClient(config);
20
20
  }
21
+ formatError(error) {
22
+ if (typeof error === 'string')
23
+ return error;
24
+ if (error instanceof Error)
25
+ return error.message;
26
+ try {
27
+ // Try to stringify with all own property names (including non-enumerable)
28
+ const props = error && typeof error === 'object'
29
+ ? Object.getOwnPropertyNames(error)
30
+ : undefined;
31
+ return JSON.stringify(error, props);
32
+ }
33
+ catch {
34
+ return String(error);
35
+ }
36
+ }
21
37
  async createSession(title) {
22
38
  const { data, error } = await this.client.session.create({
23
39
  title: title || 'Feishu Chat',
24
40
  });
25
41
  if (error) {
26
- throw new Error(`Failed to create session: ${error}`);
42
+ throw new Error(`Failed to create session: ${this.formatError(error)}`);
27
43
  }
28
44
  return data;
29
45
  }
@@ -41,12 +57,14 @@ export class OpenCodeClient {
41
57
  });
42
58
  }
43
59
  }
44
- const { data, error } = await this.client.session.prompt({
60
+ // Use promptAsync so the call returns immediately instead of blocking
61
+ // until the AI finishes. Responses arrive through the event stream.
62
+ const { data, error } = await this.client.session.promptAsync({
45
63
  sessionID: sessionId,
46
64
  parts,
47
65
  });
48
66
  if (error) {
49
- throw new Error(`Failed to send prompt: ${error}`);
67
+ throw new Error(`Failed to send prompt: ${this.formatError(error)}`);
50
68
  }
51
69
  return data;
52
70
  }
@@ -57,14 +75,14 @@ export class OpenCodeClient {
57
75
  async getSessionStatus() {
58
76
  const { data, error } = await this.client.session.status({});
59
77
  if (error) {
60
- throw new Error(`Failed to get session status: ${error}`);
78
+ throw new Error(`Failed to get session status: ${this.formatError(error)}`);
61
79
  }
62
80
  return data || {};
63
81
  }
64
82
  async listSessions() {
65
83
  const { data, error } = await this.client.session.list({});
66
84
  if (error) {
67
- throw new Error(`Failed to list sessions: ${error}`);
85
+ throw new Error(`Failed to list sessions: ${this.formatError(error)}`);
68
86
  }
69
87
  return data || [];
70
88
  }
@@ -95,7 +113,7 @@ export class OpenCodeClient {
95
113
  reply,
96
114
  });
97
115
  if (error) {
98
- throw new Error(`Failed to reply to permission: ${error}`);
116
+ throw new Error(`Failed to reply to permission: ${this.formatError(error)}`);
99
117
  }
100
118
  return !!data;
101
119
  }
@@ -105,7 +123,7 @@ export class OpenCodeClient {
105
123
  answers,
106
124
  });
107
125
  if (error) {
108
- throw new Error(`Failed to reply to question: ${error}`);
126
+ throw new Error(`Failed to reply to question: ${this.formatError(error)}`);
109
127
  }
110
128
  return !!data;
111
129
  }
@@ -113,10 +131,103 @@ export class OpenCodeClient {
113
131
  const { data, error } = await this.client.session.command({
114
132
  sessionID: sessionId,
115
133
  command,
116
- arguments: args,
134
+ arguments: args || '',
135
+ });
136
+ if (error) {
137
+ // Pass the original error object so the caller can extract meaningful messages
138
+ throw error;
139
+ }
140
+ return data;
141
+ }
142
+ async executeTuiCommand(command) {
143
+ const { data, error } = await this.client.tui.executeCommand({
144
+ directory: this.directory,
145
+ command,
146
+ });
147
+ if (error) {
148
+ throw new Error(`Failed to execute TUI command: ${this.formatError(error)}`);
149
+ }
150
+ return data;
151
+ }
152
+ async getProviders() {
153
+ const { data, error } = await this.client.config.providers({
154
+ directory: this.directory,
155
+ });
156
+ if (error) {
157
+ throw new Error(`Failed to get providers: ${this.formatError(error)}`);
158
+ }
159
+ return data;
160
+ }
161
+ async getAgents() {
162
+ const { data, error } = await this.client.app.agents({
163
+ directory: this.directory,
164
+ });
165
+ if (error) {
166
+ throw new Error(`Failed to get agents: ${this.formatError(error)}`);
167
+ }
168
+ return data;
169
+ }
170
+ async getCommands() {
171
+ const { data, error } = await this.client.command.list({
172
+ directory: this.directory,
173
+ });
174
+ if (error) {
175
+ throw new Error(`Failed to get commands: ${this.formatError(error)}`);
176
+ }
177
+ return data;
178
+ }
179
+ async getSessions() {
180
+ const { data, error } = await this.client.session.list({
181
+ directory: this.directory,
182
+ });
183
+ if (error) {
184
+ throw new Error(`Failed to get sessions: ${this.formatError(error)}`);
185
+ }
186
+ return data;
187
+ }
188
+ async getTools() {
189
+ const { data, error } = await this.client.tool.ids({
190
+ directory: this.directory,
191
+ });
192
+ if (error) {
193
+ throw new Error(`Failed to get tools: ${this.formatError(error)}`);
194
+ }
195
+ return data;
196
+ }
197
+ async getWorktrees() {
198
+ const { data, error } = await this.client.worktree.list({
199
+ directory: this.directory,
200
+ });
201
+ if (error) {
202
+ throw new Error(`Failed to get worktrees: ${this.formatError(error)}`);
203
+ }
204
+ return data;
205
+ }
206
+ async getFiles(path) {
207
+ const { data, error } = await this.client.file.list({
208
+ directory: this.directory,
209
+ path: path || '.',
210
+ });
211
+ if (error) {
212
+ throw new Error(`Failed to get files: ${this.formatError(error)}`);
213
+ }
214
+ return data;
215
+ }
216
+ async getStatus() {
217
+ const { data, error } = await this.client.file.status({
218
+ directory: this.directory,
219
+ });
220
+ if (error) {
221
+ throw new Error(`Failed to get status: ${this.formatError(error)}`);
222
+ }
223
+ return data;
224
+ }
225
+ async getConfig() {
226
+ const { data, error } = await this.client.config.get({
227
+ directory: this.directory,
117
228
  });
118
229
  if (error) {
119
- throw new Error(`Failed to send command: ${error}`);
230
+ throw new Error(`Failed to get config: ${this.formatError(error)}`);
120
231
  }
121
232
  return data;
122
233
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/opencode/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE,MAAM,OAAO,cAAc;IACjB,MAAM,CAA0C;IAChD,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,OAAmE;QAC7E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAEpD,MAAM,MAAM,GAAQ;YAClB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;QAEF,iBAAiB;QACjB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,CAAC,OAAO,GAAG;gBACf,eAAe,EAAE,SAAS,MAAM,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;aAC3F,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,KAAc;QAChC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACvD,KAAK,EAAE,KAAK,IAAI,aAAa;SAC9B,CAAC,CAAC;QAEH,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,6BAA6B,KAAK,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,IAAY,EAAE,KAAuE;QACvH,MAAM,KAAK,GAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9C,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE;wBACJ,IAAI,EAAE,IAAI,CAAC,QAAQ;wBACnB,IAAI,EAAE,IAAI,CAAC,QAAQ;wBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;qBACxB;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACvD,SAAS,EAAE,SAAS;YACpB,KAAK;SACN,CAAC,CAAC;QAEH,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAE7D,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,iCAAiC,KAAK,EAAE,CAAC,CAAC;QAC5D,CAAC;QAED,OAAO,IAAI,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAE3D,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,4BAA4B,KAAK,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,IAAI,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,SAAiB;QAClC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAC9B,SAAS,EAAE,SAAS;SACrB,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,SAAiB;QACnC,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;YAChF,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC;YACxB,OAAO,CAAC,CAAC,IAAI,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,SAAiB,EAAE,KAAmC;QAC1E,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC;YACzD,SAAS;YACT,KAAK;SACN,CAAC,CAAC;QAEH,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;QAED,OAAO,CAAC,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,SAAiB,EAAE,OAAmB;QACxD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YACvD,SAAS;YACT,OAAO;SACR,CAAC,CAAC;QAEH,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAC;QAC3D,CAAC;QAED,OAAO,CAAC,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAa;QACjE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;YACxD,SAAS,EAAE,SAAS;YACpB,OAAO;YACP,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC;QAEH,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,2BAA2B,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/opencode/client.ts"],"names":[],"mappings":"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"}
@@ -9,13 +9,25 @@ export declare class OpenCodeEventHandler {
9
9
  private opencodeUrl;
10
10
  private showProcess;
11
11
  private botName;
12
- constructor(sessionManager: SessionManager, feishuApi: FeishuAPI, hookManager?: HookManager, opencodeUrl?: string, showProcess?: boolean, botName?: string);
12
+ constructor(sessionManager: SessionManager, feishuApi: FeishuAPI, hookManager?: HookManager, opencodeUrl?: string, showProcess?: 'none' | 'tools' | 'thinking' | 'full', botName?: string);
13
+ /**
14
+ * Check if thinking content should be shown based on showProcess config.
15
+ */
16
+ private shouldShowThinking;
17
+ /**
18
+ * Check if tools should be shown based on showProcess config.
19
+ */
20
+ private shouldShowTools;
13
21
  start(eventStream: {
14
22
  stream: AsyncGenerator<any, void, unknown>;
15
23
  }): Promise<void>;
16
24
  stop(): void;
17
25
  private handleEvent;
18
26
  private handleTextDelta;
27
+ /**
28
+ * Determine if a field should be displayed based on showProcess config.
29
+ */
30
+ private shouldShowField;
19
31
  private handlePartUpdate;
20
32
  private handleStatusChange;
21
33
  private handleError;
@@ -25,6 +37,7 @@ export declare class OpenCodeEventHandler {
25
37
  private handleQuestionAsked;
26
38
  private handleQuestionReplied;
27
39
  private handleQuestionRejected;
40
+ private handleCommandExecuted;
28
41
  /**
29
42
  * 合成并推送主流式卡片(一个 chat 对应一张持续更新的卡片)。
30
43
  * - 首次调用:sendCard 新建一张并记录 message_id
@@ -1 +1 @@
1
- {"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../../src/opencode/event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAQ3D,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAc;IAClC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,OAAO,CAAS;gBAEtB,cAAc,EAAE,cAAc,EAC9B,SAAS,EAAE,SAAS,EACpB,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,UAAQ,EACnB,OAAO,SAAO;IAUV,KAAK,CAAC,WAAW,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBvF,IAAI,IAAI,IAAI;YAKE,WAAW;YAiDX,eAAe;YAsBf,gBAAgB;YA0ChB,kBAAkB;YAgClB,WAAW;YAkBX,iBAAiB;YA6BjB,qBAAqB;YAuBrB,uBAAuB;YAWvB,mBAAmB;YA6BnB,qBAAqB;YAYrB,sBAAsB;IASpC;;;;;OAKG;YACW,SAAS;CA4CxB"}
1
+ {"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../../src/opencode/event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAQ3D,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAc;IAClC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,OAAO,CAAS;gBAEtB,cAAc,EAAE,cAAc,EAC9B,SAAS,EAAE,SAAS,EACpB,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,GAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,MAAe,EAC5D,OAAO,SAAO;IAUhB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAI1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAIjB,KAAK,CAAC,WAAW,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBvF,IAAI,IAAI,IAAI;YAKE,WAAW;YAoDX,eAAe;IA0C7B;;OAEG;IACH,OAAO,CAAC,eAAe;YAcT,gBAAgB;YA0ChB,kBAAkB;YAsClB,WAAW;YA8BX,iBAAiB;YAkCjB,qBAAqB;YA4BrB,uBAAuB;YAyBvB,mBAAmB;YA6BnB,qBAAqB;YAsBrB,sBAAsB;YAoBtB,qBAAqB;IAmBnC;;;;;OAKG;YACW,SAAS;CAsExB"}
@@ -1,7 +1,7 @@
1
1
  import { FeishuCard } from '../feishu/card.js';
2
2
  import { createLogger } from '../core/logger.js';
3
3
  const log = createLogger('EventHandler');
4
- const UPDATE_THROTTLE_MS = 1000;
4
+ const UPDATE_THROTTLE_MS = 2000;
5
5
  export class OpenCodeEventHandler {
6
6
  sessionManager;
7
7
  feishuApi;
@@ -10,7 +10,7 @@ export class OpenCodeEventHandler {
10
10
  opencodeUrl;
11
11
  showProcess;
12
12
  botName;
13
- constructor(sessionManager, feishuApi, hookManager, opencodeUrl, showProcess = false, botName = '点点') {
13
+ constructor(sessionManager, feishuApi, hookManager, opencodeUrl, showProcess = 'none', botName = '点点') {
14
14
  this.sessionManager = sessionManager;
15
15
  this.feishuApi = feishuApi;
16
16
  this.hookManager = hookManager;
@@ -18,6 +18,18 @@ export class OpenCodeEventHandler {
18
18
  this.showProcess = showProcess;
19
19
  this.botName = botName;
20
20
  }
21
+ /**
22
+ * Check if thinking content should be shown based on showProcess config.
23
+ */
24
+ shouldShowThinking() {
25
+ return this.showProcess === 'thinking' || this.showProcess === 'full';
26
+ }
27
+ /**
28
+ * Check if tools should be shown based on showProcess config.
29
+ */
30
+ shouldShowTools() {
31
+ return this.showProcess === 'tools' || this.showProcess === 'full';
32
+ }
21
33
  async start(eventStream) {
22
34
  if (this.isRunning) {
23
35
  log.warn('Already running');
@@ -81,11 +93,14 @@ export class OpenCodeEventHandler {
81
93
  await this.handleQuestionRejected(props);
82
94
  break;
83
95
  case 'command.executed':
84
- log.info({ command: props.name, sessionID: props.sessionID, args: props.arguments }, 'Command executed');
96
+ await this.handleCommandExecuted(props);
85
97
  break;
86
98
  case 'server.heartbeat':
87
99
  // Silently ignore heartbeat events to reduce log noise
88
100
  break;
101
+ default:
102
+ log.debug({ type: payload.type }, 'Unhandled event type');
103
+ break;
89
104
  }
90
105
  }
91
106
  catch (err) {
@@ -95,20 +110,56 @@ export class OpenCodeEventHandler {
95
110
  async handleTextDelta(properties) {
96
111
  if (!properties)
97
112
  return;
98
- // In quiet mode, skip non-content fields (reasoning/thinking)
99
- log.info({ field: properties.field, partID: properties.partID?.substring(0, 20), deltaLen: properties.delta?.length, deltaPreview: properties.delta?.substring(0, 80) }, 'Text delta');
100
- if (!this.showProcess && properties.field && properties.field !== 'text') {
113
+ // Handle non-string delta (e.g. object, array) by converting to string
114
+ let deltaStr;
115
+ if (typeof properties.delta === 'string') {
116
+ deltaStr = properties.delta;
117
+ }
118
+ else if (properties.delta === null || properties.delta === undefined) {
119
+ deltaStr = '';
120
+ }
121
+ else if (typeof properties.delta === 'object') {
122
+ // If it's an object, try to extract text content or stringify
123
+ deltaStr = properties.delta.text || properties.delta.content || JSON.stringify(properties.delta);
124
+ }
125
+ else {
126
+ deltaStr = String(properties.delta);
127
+ }
128
+ // Skip empty deltas
129
+ if (!deltaStr) {
101
130
  return;
102
131
  }
132
+ log.info({ field: properties.field, partID: properties.partID?.substring(0, 20), deltaLen: deltaStr.length, deltaPreview: deltaStr.substring(0, 80) }, 'Text delta');
103
133
  const chatId = this.sessionManager.getChatIdBySession(properties.sessionID);
104
134
  if (!chatId)
105
135
  return;
106
- this.sessionManager.appendContent(chatId, properties.delta, properties.partID);
136
+ // Determine if we should show this field based on showProcess config
137
+ const shouldShowField = this.shouldShowField(properties.field);
138
+ if (!shouldShowField) {
139
+ return;
140
+ }
141
+ this.sessionManager.appendContent(chatId, deltaStr, properties.partID, properties.field);
107
142
  await this.flushCard(chatId);
108
143
  }
144
+ /**
145
+ * Determine if a field should be displayed based on showProcess config.
146
+ */
147
+ shouldShowField(field) {
148
+ switch (this.showProcess) {
149
+ case 'full':
150
+ return true;
151
+ case 'thinking':
152
+ return field === 'text' || field === 'thinking' || field === 'reasoning';
153
+ case 'tools':
154
+ return field === 'text' || !field;
155
+ case 'none':
156
+ default:
157
+ return !field || field === 'text';
158
+ }
159
+ }
109
160
  async handlePartUpdate(properties) {
110
- // Skip tool display in quiet mode
111
- if (!this.showProcess)
161
+ // Skip tool display if not showing tools
162
+ if (!this.shouldShowTools())
112
163
  return;
113
164
  const { part } = properties;
114
165
  if (part?.type !== 'tool')
@@ -163,7 +214,14 @@ export class OpenCodeEventHandler {
163
214
  break;
164
215
  case 'idle':
165
216
  // 不在这里清理 currentMessage,等 session.idle 事件做 final flush 后再清
166
- session.status = 'idle';
217
+ // 但如果用户在处理交互(权限/问题),保持 busy 状态,防止新消息绕过检查。
218
+ if (session.pendingInteraction) {
219
+ log.info({ chatId, interactionKind: session.pendingInteraction.kind }, 'OpenCode reports idle but pending interaction remains; keeping status busy');
220
+ session.status = 'busy';
221
+ }
222
+ else {
223
+ session.status = 'idle';
224
+ }
167
225
  break;
168
226
  case 'retry':
169
227
  session.retryMessage = properties.status.message || '等待重试';
@@ -177,8 +235,19 @@ export class OpenCodeEventHandler {
177
235
  const chatId = this.sessionManager.getChatIdBySession(properties.sessionID);
178
236
  if (!chatId)
179
237
  return;
180
- await this.feishuApi.sendCard(chatId, FeishuCard.createErrorCard(properties.error));
238
+ // If the session is already idle or error was already handled,
239
+ // the error was likely already handled by MessageHandler.
240
+ // Avoid sending a duplicate error card.
181
241
  const session = this.sessionManager.getSession(chatId);
242
+ if (session?.status === 'idle') {
243
+ log.info({ chatId, sessionId: properties.sessionID }, 'Session already idle, skipping duplicate error card');
244
+ return;
245
+ }
246
+ if (session?.errorHandled) {
247
+ log.info({ chatId, sessionId: properties.sessionID }, 'Error already handled, skipping duplicate error card');
248
+ return;
249
+ }
250
+ await this.feishuApi.sendCard(chatId, FeishuCard.createErrorCard(properties.error));
182
251
  if (session) {
183
252
  session.tools = undefined;
184
253
  session.retryMessage = undefined;
@@ -195,13 +264,18 @@ export class OpenCodeEventHandler {
195
264
  return;
196
265
  const hasContent = !!(session.currentContent && session.currentContent.length > 0);
197
266
  const hasTools = !!(session.tools && session.tools.length > 0);
198
- if (hasContent || hasTools) {
267
+ // Always finalize the card if one exists, even when the bot turn produced
268
+ // no text (e.g. slash commands that only trigger side effects).
269
+ if (hasContent || hasTools || session.currentMessageId) {
199
270
  await this.flushCard(chatId, { force: true, done: true });
200
271
  }
201
272
  session.tools = undefined;
202
273
  session.retryMessage = undefined;
274
+ session.interactionReplied = undefined;
203
275
  this.sessionManager.updateStatus(chatId, 'idle');
204
- this.sessionManager.clearCurrentMessage(chatId);
276
+ // Don't clear currentMessageId here — let MessageHandler clear it when a
277
+ // new user message arrives. This prevents race conditions where AI sends
278
+ // a card (via MCP) and our flushCard updates the wrong message.
205
279
  // Fire hook on session idle (context may have been compressed)
206
280
  if (this.hookManager) {
207
281
  this.hookManager.run('onSessionIdle', {
@@ -211,9 +285,12 @@ export class OpenCodeEventHandler {
211
285
  }
212
286
  }
213
287
  async handlePermissionAsked(properties) {
288
+ log.info({ sessionID: properties.sessionID }, 'handlePermissionAsked called');
214
289
  const chatId = this.sessionManager.getChatIdBySession(properties.sessionID);
215
- if (!chatId)
290
+ if (!chatId) {
291
+ log.warn({ sessionID: properties.sessionID, sessions: this.sessionManager.getAllSessions().map(s => ({ chatId: s.chatId, sessionId: s.id })) }, 'getChatIdBySession returned undefined');
216
292
  return;
293
+ }
217
294
  const perm = properties.permission || properties.type || 'unknown';
218
295
  const patterns = properties.patterns || (properties.pattern ? [properties.pattern] : []);
219
296
  const title = properties.title || `${perm}: ${patterns.join(', ')}`;
@@ -227,7 +304,8 @@ export class OpenCodeEventHandler {
227
304
  },
228
305
  };
229
306
  this.sessionManager.setPendingInteraction(chatId, interaction);
230
- log.info({ chatId, permission: perm, patterns }, 'Permission asked');
307
+ const session = this.sessionManager.getSession(chatId);
308
+ log.info({ chatId, permission: perm, patterns, currentMessageId: session?.currentMessageId }, 'Permission asked');
231
309
  await this.flushCard(chatId, { force: true });
232
310
  }
233
311
  async handlePermissionReplied(properties) {
@@ -235,9 +313,22 @@ export class OpenCodeEventHandler {
235
313
  if (!chatId)
236
314
  return;
237
315
  const reply = properties.reply || 'once';
238
- log.info({ chatId, reply }, 'Permission replied');
316
+ const hadPending = this.sessionManager.getPendingInteraction(chatId) !== undefined;
317
+ log.info({ chatId, reply, hadPending }, 'Permission replied');
318
+ // Only flush if the interaction was still pending (i.e. user replied via text).
319
+ // If the user already clicked a card button, handleCardAction already updated
320
+ // the card to a confirmation state — don't overwrite it.
239
321
  this.sessionManager.clearPendingInteraction(chatId);
240
- await this.flushCard(chatId, { force: true });
322
+ if (hadPending) {
323
+ await this.flushCard(chatId, { force: true });
324
+ }
325
+ // Clear interactionReplied flag so subsequent AI streaming output can update the card.
326
+ // The confirmation state has already been shown; now we need to allow the AI to continue.
327
+ const session = this.sessionManager.getSession(chatId);
328
+ if (session) {
329
+ session.interactionReplied = undefined;
330
+ log.info({ chatId }, 'Cleared interactionReplied to allow AI streaming');
331
+ }
241
332
  }
242
333
  async handleQuestionAsked(properties) {
243
334
  const chatId = this.sessionManager.getChatIdBySession(properties.sessionID);
@@ -272,15 +363,46 @@ export class OpenCodeEventHandler {
272
363
  const answers = properties.answers || [];
273
364
  const label = answers.map((a) => a.join(', ')).join('; ');
274
365
  log.info({ chatId, label }, 'Question replied');
366
+ const hadPending = this.sessionManager.getPendingInteraction(chatId) !== undefined;
275
367
  this.sessionManager.clearPendingInteraction(chatId);
276
- await this.flushCard(chatId, { force: true });
368
+ if (hadPending) {
369
+ await this.flushCard(chatId, { force: true });
370
+ }
371
+ // Clear interactionReplied flag so subsequent AI streaming output can update the card.
372
+ const session = this.sessionManager.getSession(chatId);
373
+ if (session) {
374
+ session.interactionReplied = undefined;
375
+ log.info({ chatId }, 'Cleared interactionReplied to allow AI streaming');
376
+ }
277
377
  }
278
378
  async handleQuestionRejected(properties) {
279
379
  const chatId = this.sessionManager.getChatIdBySession(properties.sessionID);
280
380
  if (!chatId)
281
381
  return;
282
382
  log.info({ chatId }, 'Question rejected');
383
+ const hadPending = this.sessionManager.getPendingInteraction(chatId) !== undefined;
283
384
  this.sessionManager.clearPendingInteraction(chatId);
385
+ if (hadPending) {
386
+ await this.flushCard(chatId, { force: true });
387
+ }
388
+ // Clear interactionReplied flag so subsequent AI streaming output can update the card.
389
+ const session = this.sessionManager.getSession(chatId);
390
+ if (session) {
391
+ session.interactionReplied = undefined;
392
+ log.info({ chatId }, 'Cleared interactionReplied to allow AI streaming');
393
+ }
394
+ }
395
+ async handleCommandExecuted(properties) {
396
+ const chatId = this.sessionManager.getChatIdBySession(properties.sessionID);
397
+ if (!chatId)
398
+ return;
399
+ const commandName = properties.name || 'unknown';
400
+ const args = properties.arguments || '';
401
+ log.info({ chatId, command: commandName, args }, 'Command executed');
402
+ // Append a command notice to the session content so the user sees
403
+ // feedback in the card (and the thinking animation stops).
404
+ const notice = `⚡ 命令 \`/${commandName}\`${args ? ` \`${args}\`` : ''} 已执行`;
405
+ this.sessionManager.appendContent(chatId, notice);
284
406
  await this.flushCard(chatId, { force: true });
285
407
  }
286
408
  /**
@@ -293,11 +415,24 @@ export class OpenCodeEventHandler {
293
415
  const session = this.sessionManager.getSession(chatId);
294
416
  if (!session)
295
417
  return;
418
+ // When the user replied via card button, the confirmation card should stay
419
+ // as-is. Skip flushCard updates so the AI streaming output doesn't overwrite
420
+ // the confirmation state. The flag is cleared on session.idle.
421
+ if (session.interactionReplied) {
422
+ log.info({ chatId, targetMessageId: session.currentMessageId, done: opts.done }, 'flushCard skipped: interaction was handled via card');
423
+ return;
424
+ }
425
+ // Capture target message ID at the start so concurrent flushCard calls
426
+ // don't race against each other and update different messages.
427
+ const targetMessageId = session.currentMessageId;
296
428
  const content = session.currentContent || '';
297
- const tools = this.showProcess ? (session.tools || []) : [];
429
+ const thinkingContent = this.shouldShowThinking() ? (session.thinkingContent || '') : '';
430
+ const tools = this.shouldShowTools() ? (session.tools || []) : [];
298
431
  const interaction = session.pendingInteraction;
432
+ log.info({ chatId, hasInteraction: !!interaction, interactionKind: interaction?.kind, targetMessageId, force: opts.force, done: opts.done }, 'flushCard');
299
433
  const card = FeishuCard.createStreamingCard({
300
434
  content,
435
+ thinkingContent,
301
436
  tools,
302
437
  done: !!opts.done,
303
438
  retry: session.retryMessage,
@@ -305,11 +440,18 @@ export class OpenCodeEventHandler {
305
440
  botName: this.botName,
306
441
  interaction,
307
442
  });
308
- if (!session.currentMessageId) {
443
+ if (!targetMessageId) {
309
444
  try {
310
445
  const message = await this.feishuApi.sendCard(chatId, card);
311
446
  if (message && message.message_id) {
312
- this.sessionManager.setCurrentMessage(chatId, message.message_id);
447
+ // Only set currentMessageId if it hasn't been set by another concurrent flushCard
448
+ if (!session.currentMessageId) {
449
+ this.sessionManager.setCurrentMessage(chatId, message.message_id);
450
+ log.info({ chatId, messageId: message.message_id }, 'sendCard created new card');
451
+ }
452
+ else {
453
+ log.info({ chatId, messageId: message.message_id, existingMessageId: session.currentMessageId }, 'sendCard created card but currentMessageId already set by concurrent flushCard');
454
+ }
313
455
  }
314
456
  }
315
457
  catch (err) {
@@ -319,12 +461,16 @@ export class OpenCodeEventHandler {
319
461
  }
320
462
  const now = Date.now();
321
463
  const lastUpdate = session.lastUpdateTime || 0;
322
- if (!opts.force && now - lastUpdate <= UPDATE_THROTTLE_MS)
464
+ if (!opts.force && now - lastUpdate <= UPDATE_THROTTLE_MS) {
465
+ log.info({ chatId, targetMessageId, elapsed: now - lastUpdate }, 'flushCard throttled');
323
466
  return;
467
+ }
468
+ // Update timestamp immediately to prevent concurrent flushCard calls
469
+ // from all passing the throttle check while this one awaits updateCard.
470
+ session.lastUpdateTime = now;
324
471
  try {
325
- await this.feishuApi.updateCard(session.currentMessageId, card);
326
- // 只有在更新成功后才更新时间戳,避免失败时丢失后续更新
327
- session.lastUpdateTime = now;
472
+ await this.feishuApi.updateCard(targetMessageId, card);
473
+ log.info({ chatId, targetMessageId }, 'updateCard success');
328
474
  }
329
475
  catch (err) {
330
476
  log.error({ err }, 'Failed to update card');