@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.
- package/dist/core/config.d.ts +3 -3
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/message-handler.d.ts +25 -11
- package/dist/core/message-handler.d.ts.map +1 -1
- package/dist/core/message-handler.js +589 -102
- package/dist/core/message-handler.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +18 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +35 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/feishu/api.d.ts +6 -0
- package/dist/feishu/api.d.ts.map +1 -1
- package/dist/feishu/api.js +71 -6
- package/dist/feishu/api.js.map +1 -1
- package/dist/feishu/card.d.ts +6 -5
- package/dist/feishu/card.d.ts.map +1 -1
- package/dist/feishu/card.js +185 -120
- package/dist/feishu/card.js.map +1 -1
- package/dist/feishu/event-source.d.ts +6 -0
- package/dist/feishu/event-source.d.ts.map +1 -1
- package/dist/feishu/event-source.js +54 -0
- package/dist/feishu/event-source.js.map +1 -1
- package/dist/opencode/client.d.ts +11 -0
- package/dist/opencode/client.d.ts.map +1 -1
- package/dist/opencode/client.js +120 -9
- package/dist/opencode/client.js.map +1 -1
- package/dist/opencode/event-handler.d.ts +14 -1
- package/dist/opencode/event-handler.d.ts.map +1 -1
- package/dist/opencode/event-handler.js +171 -25
- package/dist/opencode/event-handler.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +10 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/doc-service.d.ts +11 -45
- package/dist/services/doc-service.d.ts.map +1 -1
- package/dist/services/doc-service.js +72 -174
- package/dist/services/doc-service.js.map +1 -1
- package/dist/services/im-service.d.ts.map +1 -1
- package/dist/services/im-service.js +10 -6
- package/dist/services/im-service.js.map +1 -1
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +10 -3
- package/dist/standalone.js.map +1 -1
- package/dist/types/extended.d.ts +2 -1
- package/dist/types/extended.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/opencode/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
|
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?:
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
99
|
-
|
|
100
|
-
if (
|
|
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
|
|
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
|
|
111
|
-
if (!this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
443
|
+
if (!targetMessageId) {
|
|
309
444
|
try {
|
|
310
445
|
const message = await this.feishuApi.sendCard(chatId, card);
|
|
311
446
|
if (message && message.message_id) {
|
|
312
|
-
|
|
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(
|
|
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');
|