opencode-bridge 2.9.0-beta
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/.env.example +131 -0
- package/LICENSE +674 -0
- package/README.md +1195 -0
- package/bin/opencode-bridge.js +31 -0
- package/dist/commands/effort.d.ts +9 -0
- package/dist/commands/effort.d.ts.map +1 -0
- package/dist/commands/effort.js +56 -0
- package/dist/commands/effort.js.map +1 -0
- package/dist/commands/parser.d.ts +37 -0
- package/dist/commands/parser.d.ts.map +1 -0
- package/dist/commands/parser.js +355 -0
- package/dist/commands/parser.js.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +340 -0
- package/dist/config.js.map +1 -0
- package/dist/feishu/cards-stream.d.ts +65 -0
- package/dist/feishu/cards-stream.d.ts.map +1 -0
- package/dist/feishu/cards-stream.js +448 -0
- package/dist/feishu/cards-stream.js.map +1 -0
- package/dist/feishu/cards.d.ts +81 -0
- package/dist/feishu/cards.d.ts.map +1 -0
- package/dist/feishu/cards.js +560 -0
- package/dist/feishu/cards.js.map +1 -0
- package/dist/feishu/client.d.ts +132 -0
- package/dist/feishu/client.d.ts.map +1 -0
- package/dist/feishu/client.js +952 -0
- package/dist/feishu/client.js.map +1 -0
- package/dist/feishu/streamer.d.ts +30 -0
- package/dist/feishu/streamer.d.ts.map +1 -0
- package/dist/feishu/streamer.js +95 -0
- package/dist/feishu/streamer.js.map +1 -0
- package/dist/handlers/card-action.d.ts +12 -0
- package/dist/handlers/card-action.d.ts.map +1 -0
- package/dist/handlers/card-action.js +154 -0
- package/dist/handlers/card-action.js.map +1 -0
- package/dist/handlers/command.d.ts +76 -0
- package/dist/handlers/command.d.ts.map +1 -0
- package/dist/handlers/command.js +1773 -0
- package/dist/handlers/command.js.map +1 -0
- package/dist/handlers/discord.d.ts +78 -0
- package/dist/handlers/discord.d.ts.map +1 -0
- package/dist/handlers/discord.js +1832 -0
- package/dist/handlers/discord.js.map +1 -0
- package/dist/handlers/file-sender.d.ts +22 -0
- package/dist/handlers/file-sender.d.ts.map +1 -0
- package/dist/handlers/file-sender.js +183 -0
- package/dist/handlers/file-sender.js.map +1 -0
- package/dist/handlers/group.d.ts +21 -0
- package/dist/handlers/group.d.ts.map +1 -0
- package/dist/handlers/group.js +414 -0
- package/dist/handlers/group.js.map +1 -0
- package/dist/handlers/lifecycle.d.ts +17 -0
- package/dist/handlers/lifecycle.d.ts.map +1 -0
- package/dist/handlers/lifecycle.js +129 -0
- package/dist/handlers/lifecycle.js.map +1 -0
- package/dist/handlers/p2p.d.ts +44 -0
- package/dist/handlers/p2p.d.ts.map +1 -0
- package/dist/handlers/p2p.js +625 -0
- package/dist/handlers/p2p.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1562 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode/client.d.ts +176 -0
- package/dist/opencode/client.d.ts.map +1 -0
- package/dist/opencode/client.js +1126 -0
- package/dist/opencode/client.js.map +1 -0
- package/dist/opencode/delayed-handler.d.ts +33 -0
- package/dist/opencode/delayed-handler.d.ts.map +1 -0
- package/dist/opencode/delayed-handler.js +74 -0
- package/dist/opencode/delayed-handler.js.map +1 -0
- package/dist/opencode/output-buffer.d.ts +56 -0
- package/dist/opencode/output-buffer.d.ts.map +1 -0
- package/dist/opencode/output-buffer.js +202 -0
- package/dist/opencode/output-buffer.js.map +1 -0
- package/dist/opencode/question-handler.d.ts +61 -0
- package/dist/opencode/question-handler.d.ts.map +1 -0
- package/dist/opencode/question-handler.js +183 -0
- package/dist/opencode/question-handler.js.map +1 -0
- package/dist/opencode/question-parser.d.ts +9 -0
- package/dist/opencode/question-parser.d.ts.map +1 -0
- package/dist/opencode/question-parser.js +69 -0
- package/dist/opencode/question-parser.js.map +1 -0
- package/dist/opencode/session-queue.d.ts +16 -0
- package/dist/opencode/session-queue.d.ts.map +1 -0
- package/dist/opencode/session-queue.js +41 -0
- package/dist/opencode/session-queue.js.map +1 -0
- package/dist/permissions/handler.d.ts +36 -0
- package/dist/permissions/handler.d.ts.map +1 -0
- package/dist/permissions/handler.js +141 -0
- package/dist/permissions/handler.js.map +1 -0
- package/dist/platform/adapters/discord-adapter.d.ts +45 -0
- package/dist/platform/adapters/discord-adapter.d.ts.map +1 -0
- package/dist/platform/adapters/discord-adapter.js +497 -0
- package/dist/platform/adapters/discord-adapter.js.map +1 -0
- package/dist/platform/adapters/feishu-adapter.d.ts +29 -0
- package/dist/platform/adapters/feishu-adapter.d.ts.map +1 -0
- package/dist/platform/adapters/feishu-adapter.js +150 -0
- package/dist/platform/adapters/feishu-adapter.js.map +1 -0
- package/dist/platform/registry.d.ts +41 -0
- package/dist/platform/registry.d.ts.map +1 -0
- package/dist/platform/registry.js +87 -0
- package/dist/platform/registry.js.map +1 -0
- package/dist/platform/types.d.ts +92 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +4 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/reliability/audit-log.d.ts +93 -0
- package/dist/reliability/audit-log.d.ts.map +1 -0
- package/dist/reliability/audit-log.js +248 -0
- package/dist/reliability/audit-log.js.map +1 -0
- package/dist/reliability/config-guard.d.ts +42 -0
- package/dist/reliability/config-guard.d.ts.map +1 -0
- package/dist/reliability/config-guard.js +264 -0
- package/dist/reliability/config-guard.js.map +1 -0
- package/dist/reliability/conversation-heartbeat.d.ts +37 -0
- package/dist/reliability/conversation-heartbeat.d.ts.map +1 -0
- package/dist/reliability/conversation-heartbeat.js +179 -0
- package/dist/reliability/conversation-heartbeat.js.map +1 -0
- package/dist/reliability/cron-api-server.d.ts +13 -0
- package/dist/reliability/cron-api-server.d.ts.map +1 -0
- package/dist/reliability/cron-api-server.js +247 -0
- package/dist/reliability/cron-api-server.js.map +1 -0
- package/dist/reliability/cron-control.d.ts +34 -0
- package/dist/reliability/cron-control.d.ts.map +1 -0
- package/dist/reliability/cron-control.js +864 -0
- package/dist/reliability/cron-control.js.map +1 -0
- package/dist/reliability/cron-semantic.d.ts +9 -0
- package/dist/reliability/cron-semantic.d.ts.map +1 -0
- package/dist/reliability/cron-semantic.js +208 -0
- package/dist/reliability/cron-semantic.js.map +1 -0
- package/dist/reliability/environment-doctor.d.ts +56 -0
- package/dist/reliability/environment-doctor.d.ts.map +1 -0
- package/dist/reliability/environment-doctor.js +213 -0
- package/dist/reliability/environment-doctor.js.map +1 -0
- package/dist/reliability/job-registry.d.ts +26 -0
- package/dist/reliability/job-registry.d.ts.map +1 -0
- package/dist/reliability/job-registry.js +77 -0
- package/dist/reliability/job-registry.js.map +1 -0
- package/dist/reliability/opencode-probe.d.ts +37 -0
- package/dist/reliability/opencode-probe.d.ts.map +1 -0
- package/dist/reliability/opencode-probe.js +195 -0
- package/dist/reliability/opencode-probe.js.map +1 -0
- package/dist/reliability/opencode-restart.d.ts +42 -0
- package/dist/reliability/opencode-restart.d.ts.map +1 -0
- package/dist/reliability/opencode-restart.js +155 -0
- package/dist/reliability/opencode-restart.js.map +1 -0
- package/dist/reliability/proactive-heartbeat.d.ts +39 -0
- package/dist/reliability/proactive-heartbeat.d.ts.map +1 -0
- package/dist/reliability/proactive-heartbeat.js +147 -0
- package/dist/reliability/proactive-heartbeat.js.map +1 -0
- package/dist/reliability/process-check-job.d.ts +73 -0
- package/dist/reliability/process-check-job.d.ts.map +1 -0
- package/dist/reliability/process-check-job.js +254 -0
- package/dist/reliability/process-check-job.js.map +1 -0
- package/dist/reliability/process-guard.d.ts +53 -0
- package/dist/reliability/process-guard.d.ts.map +1 -0
- package/dist/reliability/process-guard.js +344 -0
- package/dist/reliability/process-guard.js.map +1 -0
- package/dist/reliability/recovery-reporter.d.ts +37 -0
- package/dist/reliability/recovery-reporter.d.ts.map +1 -0
- package/dist/reliability/recovery-reporter.js +161 -0
- package/dist/reliability/recovery-reporter.js.map +1 -0
- package/dist/reliability/rescue-executor.d.ts +52 -0
- package/dist/reliability/rescue-executor.d.ts.map +1 -0
- package/dist/reliability/rescue-executor.js +244 -0
- package/dist/reliability/rescue-executor.js.map +1 -0
- package/dist/reliability/rescue-policy.d.ts +39 -0
- package/dist/reliability/rescue-policy.d.ts.map +1 -0
- package/dist/reliability/rescue-policy.js +85 -0
- package/dist/reliability/rescue-policy.js.map +1 -0
- package/dist/reliability/runtime-cron-dispatcher.d.ts +30 -0
- package/dist/reliability/runtime-cron-dispatcher.d.ts.map +1 -0
- package/dist/reliability/runtime-cron-dispatcher.js +100 -0
- package/dist/reliability/runtime-cron-dispatcher.js.map +1 -0
- package/dist/reliability/runtime-cron-orphan.d.ts +18 -0
- package/dist/reliability/runtime-cron-orphan.d.ts.map +1 -0
- package/dist/reliability/runtime-cron-orphan.js +87 -0
- package/dist/reliability/runtime-cron-orphan.js.map +1 -0
- package/dist/reliability/runtime-cron-registry.d.ts +4 -0
- package/dist/reliability/runtime-cron-registry.d.ts.map +1 -0
- package/dist/reliability/runtime-cron-registry.js +8 -0
- package/dist/reliability/runtime-cron-registry.js.map +1 -0
- package/dist/reliability/runtime-cron.d.ts +75 -0
- package/dist/reliability/runtime-cron.d.ts.map +1 -0
- package/dist/reliability/runtime-cron.js +309 -0
- package/dist/reliability/runtime-cron.js.map +1 -0
- package/dist/reliability/scheduler.d.ts +38 -0
- package/dist/reliability/scheduler.d.ts.map +1 -0
- package/dist/reliability/scheduler.js +174 -0
- package/dist/reliability/scheduler.js.map +1 -0
- package/dist/reliability/types.d.ts +151 -0
- package/dist/reliability/types.d.ts.map +1 -0
- package/dist/reliability/types.js +178 -0
- package/dist/reliability/types.js.map +1 -0
- package/dist/router/action-handlers.d.ts +27 -0
- package/dist/router/action-handlers.d.ts.map +1 -0
- package/dist/router/action-handlers.js +226 -0
- package/dist/router/action-handlers.js.map +1 -0
- package/dist/router/opencode-event-hub.d.ts +159 -0
- package/dist/router/opencode-event-hub.d.ts.map +1 -0
- package/dist/router/opencode-event-hub.js +589 -0
- package/dist/router/opencode-event-hub.js.map +1 -0
- package/dist/router/root-router.d.ts +94 -0
- package/dist/router/root-router.d.ts.map +1 -0
- package/dist/router/root-router.js +214 -0
- package/dist/router/root-router.js.map +1 -0
- package/dist/store/chat-session.d.ts +150 -0
- package/dist/store/chat-session.d.ts.map +1 -0
- package/dist/store/chat-session.js +640 -0
- package/dist/store/chat-session.js.map +1 -0
- package/dist/store/session-directory.d.ts +12 -0
- package/dist/store/session-directory.d.ts.map +1 -0
- package/dist/store/session-directory.js +47 -0
- package/dist/store/session-directory.js.map +1 -0
- package/dist/store/session-group.d.ts +19 -0
- package/dist/store/session-group.d.ts.map +1 -0
- package/dist/store/session-group.js +92 -0
- package/dist/store/session-group.js.map +1 -0
- package/dist/store/user-session.d.ts +19 -0
- package/dist/store/user-session.d.ts.map +1 -0
- package/dist/store/user-session.js +112 -0
- package/dist/store/user-session.js.map +1 -0
- package/dist/utils/async-queue.d.ts +12 -0
- package/dist/utils/async-queue.d.ts.map +1 -0
- package/dist/utils/async-queue.js +51 -0
- package/dist/utils/async-queue.js.map +1 -0
- package/dist/utils/directory-policy.d.ts +50 -0
- package/dist/utils/directory-policy.d.ts.map +1 -0
- package/dist/utils/directory-policy.js +379 -0
- package/dist/utils/directory-policy.js.map +1 -0
- package/dist/utils/session-title.d.ts +2 -0
- package/dist/utils/session-title.d.ts.map +1 -0
- package/dist/utils/session-title.js +10 -0
- package/dist/utils/session-title.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
2
|
+
import { opencodeConfig, modelConfig } from '../config.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
function getPermissionLabel(props) {
|
|
5
|
+
if (typeof props.permission === 'string' && props.permission.trim()) {
|
|
6
|
+
return props.permission;
|
|
7
|
+
}
|
|
8
|
+
if (typeof props.tool === 'string' && props.tool.trim()) {
|
|
9
|
+
return props.tool;
|
|
10
|
+
}
|
|
11
|
+
if (props.tool && typeof props.tool === 'object') {
|
|
12
|
+
const toolObj = props.tool;
|
|
13
|
+
if (typeof toolObj.name === 'string' && toolObj.name.trim()) {
|
|
14
|
+
return toolObj.name;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return 'unknown';
|
|
18
|
+
}
|
|
19
|
+
function getFirstString(...values) {
|
|
20
|
+
for (const value of values) {
|
|
21
|
+
if (typeof value === 'string' && value.trim()) {
|
|
22
|
+
return value.trim();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
function toRecord(value) {
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function getFirstStringFromRecord(record, keys) {
|
|
34
|
+
if (!record) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = record[key];
|
|
39
|
+
if (typeof value === 'string' && value.trim()) {
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
function extractPermissionCorrelation(props) {
|
|
46
|
+
const propsRecord = props;
|
|
47
|
+
const toolRecord = toRecord(props.tool);
|
|
48
|
+
const metadataRecord = toRecord(props.metadata);
|
|
49
|
+
const parentSessionId = getFirstString(getFirstStringFromRecord(propsRecord, ['parentSessionID', 'parentSessionId', 'parent_session_id']), getFirstStringFromRecord(toolRecord, ['parentSessionID', 'parentSessionId', 'parent_session_id']), getFirstStringFromRecord(metadataRecord, ['parentSessionID', 'parentSessionId', 'parent_session_id']));
|
|
50
|
+
const relatedSessionId = getFirstString(getFirstStringFromRecord(propsRecord, [
|
|
51
|
+
'originSessionID',
|
|
52
|
+
'originSessionId',
|
|
53
|
+
'origin_session_id',
|
|
54
|
+
'rootSessionID',
|
|
55
|
+
'rootSessionId',
|
|
56
|
+
'root_session_id',
|
|
57
|
+
'sourceSessionID',
|
|
58
|
+
'sourceSessionId',
|
|
59
|
+
'source_session_id',
|
|
60
|
+
]), getFirstStringFromRecord(toolRecord, [
|
|
61
|
+
'originSessionID',
|
|
62
|
+
'originSessionId',
|
|
63
|
+
'origin_session_id',
|
|
64
|
+
'rootSessionID',
|
|
65
|
+
'rootSessionId',
|
|
66
|
+
'root_session_id',
|
|
67
|
+
'sourceSessionID',
|
|
68
|
+
'sourceSessionId',
|
|
69
|
+
'source_session_id',
|
|
70
|
+
]), getFirstStringFromRecord(metadataRecord, [
|
|
71
|
+
'originSessionID',
|
|
72
|
+
'originSessionId',
|
|
73
|
+
'origin_session_id',
|
|
74
|
+
'rootSessionID',
|
|
75
|
+
'rootSessionId',
|
|
76
|
+
'root_session_id',
|
|
77
|
+
'sourceSessionID',
|
|
78
|
+
'sourceSessionId',
|
|
79
|
+
'source_session_id',
|
|
80
|
+
]));
|
|
81
|
+
const messageId = getFirstString(getFirstStringFromRecord(propsRecord, ['messageID', 'messageId', 'message_id']), getFirstStringFromRecord(toolRecord, ['messageID', 'messageId', 'message_id']), getFirstStringFromRecord(metadataRecord, ['messageID', 'messageId', 'message_id']));
|
|
82
|
+
const callId = getFirstString(getFirstStringFromRecord(propsRecord, ['callID', 'callId', 'call_id', 'toolCallID', 'toolCallId', 'tool_call_id']), getFirstStringFromRecord(toolRecord, ['callID', 'callId', 'call_id', 'toolCallID', 'toolCallId', 'tool_call_id']), getFirstStringFromRecord(metadataRecord, ['callID', 'callId', 'call_id', 'toolCallID', 'toolCallId', 'tool_call_id']));
|
|
83
|
+
return {
|
|
84
|
+
...(parentSessionId ? { parentSessionId } : {}),
|
|
85
|
+
...(relatedSessionId ? { relatedSessionId } : {}),
|
|
86
|
+
...(messageId ? { messageId } : {}),
|
|
87
|
+
...(callId ? { callId } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function isPermissionRequestEventType(eventType) {
|
|
91
|
+
const normalized = eventType.toLowerCase();
|
|
92
|
+
if (!normalized.includes('permission')) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (normalized.includes('replied') ||
|
|
96
|
+
normalized.includes('reply') ||
|
|
97
|
+
normalized.includes('granted') ||
|
|
98
|
+
normalized.includes('denied') ||
|
|
99
|
+
normalized.includes('resolved')) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return (normalized.includes('request') ||
|
|
103
|
+
normalized.includes('asked') ||
|
|
104
|
+
normalized.includes('require') ||
|
|
105
|
+
normalized.includes('pending'));
|
|
106
|
+
}
|
|
107
|
+
function formatSdkError(error) {
|
|
108
|
+
if (!error)
|
|
109
|
+
return '未知错误';
|
|
110
|
+
if (typeof error === 'string') {
|
|
111
|
+
return error;
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
return error.message;
|
|
115
|
+
}
|
|
116
|
+
if (typeof error === 'object') {
|
|
117
|
+
const record = error;
|
|
118
|
+
if (typeof record.message === 'string' && record.message.trim()) {
|
|
119
|
+
return record.message;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return JSON.stringify(record);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return String(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return String(error);
|
|
129
|
+
}
|
|
130
|
+
function parseBoolean(value) {
|
|
131
|
+
if (typeof value === 'boolean')
|
|
132
|
+
return value;
|
|
133
|
+
if (typeof value === 'number')
|
|
134
|
+
return value !== 0;
|
|
135
|
+
if (typeof value === 'string') {
|
|
136
|
+
const normalized = value.trim().toLowerCase();
|
|
137
|
+
if (normalized === 'true' || normalized === '1')
|
|
138
|
+
return true;
|
|
139
|
+
if (normalized === 'false' || normalized === '0')
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
function parseAgentMode(value) {
|
|
145
|
+
if (typeof value !== 'string')
|
|
146
|
+
return undefined;
|
|
147
|
+
const normalized = value.trim().toLowerCase();
|
|
148
|
+
if (normalized === 'primary' || normalized === 'subagent' || normalized === 'all') {
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
function buildOpencodeAuthorizationHeaderValue() {
|
|
154
|
+
const password = opencodeConfig.serverPassword;
|
|
155
|
+
if (!password) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const username = opencodeConfig.serverUsername || 'opencode';
|
|
159
|
+
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
|
160
|
+
return `Basic ${encoded}`;
|
|
161
|
+
}
|
|
162
|
+
function withOpencodeAuthorizationHeaders(headers) {
|
|
163
|
+
const merged = {
|
|
164
|
+
...(headers || {}),
|
|
165
|
+
};
|
|
166
|
+
const authorization = buildOpencodeAuthorizationHeaderValue();
|
|
167
|
+
if (authorization) {
|
|
168
|
+
merged.Authorization = authorization;
|
|
169
|
+
}
|
|
170
|
+
return merged;
|
|
171
|
+
}
|
|
172
|
+
function isUnauthorizedStatusCode(statusCode) {
|
|
173
|
+
return statusCode === 401 || statusCode === 403;
|
|
174
|
+
}
|
|
175
|
+
function buildAuthEnvHint() {
|
|
176
|
+
return '请检查 OPENCODE_SERVER_USERNAME / OPENCODE_SERVER_PASSWORD 是否与 OpenCode 服务端一致';
|
|
177
|
+
}
|
|
178
|
+
function appendAuthHint(message, statusCode) {
|
|
179
|
+
if (!isUnauthorizedStatusCode(statusCode)) {
|
|
180
|
+
return message;
|
|
181
|
+
}
|
|
182
|
+
return `${message};${buildAuthEnvHint()}`;
|
|
183
|
+
}
|
|
184
|
+
class OpencodeClientWrapper extends EventEmitter {
|
|
185
|
+
client = null;
|
|
186
|
+
eventAbortController = null;
|
|
187
|
+
eventReconnectTimer = null;
|
|
188
|
+
eventReconnectAttempt = 0;
|
|
189
|
+
eventListeningEnabled = false;
|
|
190
|
+
eventStreamActive = false;
|
|
191
|
+
directoryEventStreams = new Map();
|
|
192
|
+
// 防止并发调用 ensureDirectoryEventStream 对同一目录建立多条 SSE 连接
|
|
193
|
+
pendingDirectoryStreams = new Map();
|
|
194
|
+
constructor() {
|
|
195
|
+
super();
|
|
196
|
+
}
|
|
197
|
+
// 连接到OpenCode服务器
|
|
198
|
+
async connect() {
|
|
199
|
+
try {
|
|
200
|
+
console.log(`[OpenCode] 正在连接到 ${opencodeConfig.baseUrl}...`);
|
|
201
|
+
this.client = createOpencodeClient({
|
|
202
|
+
baseUrl: opencodeConfig.baseUrl,
|
|
203
|
+
headers: withOpencodeAuthorizationHeaders(),
|
|
204
|
+
});
|
|
205
|
+
// 通过获取会话列表来检查服务器状态
|
|
206
|
+
try {
|
|
207
|
+
const result = await this.client.session.list();
|
|
208
|
+
if (result.error) {
|
|
209
|
+
const statusCode = result.response?.status;
|
|
210
|
+
const reason = appendAuthHint(statusCode
|
|
211
|
+
? `OpenCode 连接失败(HTTP ${statusCode})`
|
|
212
|
+
: `OpenCode 连接失败: ${formatSdkError(result.error)}`, statusCode);
|
|
213
|
+
console.error(`[OpenCode] ${reason}`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
console.log('[OpenCode] 已连接');
|
|
217
|
+
this.eventListeningEnabled = true;
|
|
218
|
+
// 启动事件监听
|
|
219
|
+
void this.startEventListener();
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
224
|
+
const withHint = /\b(401|403)\b/.test(errorMessage)
|
|
225
|
+
? `${errorMessage};${buildAuthEnvHint()}`
|
|
226
|
+
: errorMessage;
|
|
227
|
+
console.error(`[OpenCode] 服务器状态异常: ${withHint}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
console.error('[OpenCode] 连接失败:', error);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
clearEventReconnectTimer() {
|
|
237
|
+
if (this.eventReconnectTimer) {
|
|
238
|
+
clearTimeout(this.eventReconnectTimer);
|
|
239
|
+
this.eventReconnectTimer = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
scheduleEventReconnect(reason) {
|
|
243
|
+
if (!this.eventListeningEnabled || !this.client) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (this.eventReconnectTimer) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const maxBackoffMs = 15000;
|
|
250
|
+
const baseBackoffMs = 2000;
|
|
251
|
+
const step = Math.min(this.eventReconnectAttempt, 4);
|
|
252
|
+
const delay = Math.min(baseBackoffMs * Math.pow(2, step), maxBackoffMs);
|
|
253
|
+
this.eventReconnectAttempt += 1;
|
|
254
|
+
console.warn(`[OpenCode] ${reason},将在 ${Math.round(delay / 1000)} 秒后重连事件流(第 ${this.eventReconnectAttempt} 次)`);
|
|
255
|
+
this.eventReconnectTimer = setTimeout(() => {
|
|
256
|
+
this.eventReconnectTimer = null;
|
|
257
|
+
void this.startEventListener();
|
|
258
|
+
}, delay);
|
|
259
|
+
}
|
|
260
|
+
clearDirectoryEventReconnectTimer(directory) {
|
|
261
|
+
const entry = this.directoryEventStreams.get(directory);
|
|
262
|
+
if (!entry || !entry.reconnectTimer) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
clearTimeout(entry.reconnectTimer);
|
|
266
|
+
entry.reconnectTimer = null;
|
|
267
|
+
}
|
|
268
|
+
scheduleDirectoryEventReconnect(directory, reason) {
|
|
269
|
+
if (!this.eventListeningEnabled || !this.client) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const entry = this.directoryEventStreams.get(directory);
|
|
273
|
+
if (!entry || entry.reconnectTimer) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const delay = 3000;
|
|
277
|
+
console.warn(`[OpenCode] ${reason},将在 ${Math.round(delay / 1000)} 秒后重连目录事件流: ${directory}`);
|
|
278
|
+
entry.reconnectTimer = setTimeout(() => {
|
|
279
|
+
entry.reconnectTimer = null;
|
|
280
|
+
void this.ensureDirectoryEventStream(directory);
|
|
281
|
+
}, delay);
|
|
282
|
+
}
|
|
283
|
+
async ensureDirectoryEventStream(directory) {
|
|
284
|
+
if (!this.client || !this.eventListeningEnabled) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const normalizedDirectory = directory.trim();
|
|
288
|
+
if (!normalizedDirectory) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// 防竞态:同一目录正在建立连接时,等待而非重复创建
|
|
292
|
+
const pending = this.pendingDirectoryStreams.get(normalizedDirectory);
|
|
293
|
+
if (pending) {
|
|
294
|
+
await pending;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const promise = this._createDirectoryEventStream(normalizedDirectory);
|
|
298
|
+
this.pendingDirectoryStreams.set(normalizedDirectory, promise);
|
|
299
|
+
try {
|
|
300
|
+
await promise;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
this.pendingDirectoryStreams.delete(normalizedDirectory);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async _createDirectoryEventStream(normalizedDirectory) {
|
|
307
|
+
const existing = this.directoryEventStreams.get(normalizedDirectory);
|
|
308
|
+
if (existing?.active) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (existing) {
|
|
312
|
+
this.clearDirectoryEventReconnectTimer(normalizedDirectory);
|
|
313
|
+
existing.controller.abort();
|
|
314
|
+
}
|
|
315
|
+
const controller = new AbortController();
|
|
316
|
+
this.directoryEventStreams.set(normalizedDirectory, {
|
|
317
|
+
controller,
|
|
318
|
+
active: true,
|
|
319
|
+
reconnectTimer: null,
|
|
320
|
+
});
|
|
321
|
+
try {
|
|
322
|
+
const events = await this.client.event.subscribe({
|
|
323
|
+
query: { directory: normalizedDirectory },
|
|
324
|
+
});
|
|
325
|
+
console.log(`[OpenCode] 目录事件流订阅成功: ${normalizedDirectory}`);
|
|
326
|
+
(async () => {
|
|
327
|
+
try {
|
|
328
|
+
for await (const event of events.stream) {
|
|
329
|
+
if (controller.signal.aborted || !this.eventListeningEnabled) {
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (event.type.toLowerCase().includes('permission')) {
|
|
333
|
+
console.log(`[OpenCode] 目录事件: ${event.type}`, JSON.stringify(event.properties || {}).slice(0, 1200));
|
|
334
|
+
}
|
|
335
|
+
this.handleEvent(event);
|
|
336
|
+
}
|
|
337
|
+
if (!controller.signal.aborted && this.eventListeningEnabled) {
|
|
338
|
+
const entry = this.directoryEventStreams.get(normalizedDirectory);
|
|
339
|
+
if (entry) {
|
|
340
|
+
entry.active = false;
|
|
341
|
+
}
|
|
342
|
+
this.scheduleDirectoryEventReconnect(normalizedDirectory, '目录事件流已结束');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (!controller.signal.aborted && this.eventListeningEnabled) {
|
|
347
|
+
console.error(`[OpenCode] 目录事件流中断: ${normalizedDirectory}`, error);
|
|
348
|
+
const entry = this.directoryEventStreams.get(normalizedDirectory);
|
|
349
|
+
if (entry) {
|
|
350
|
+
entry.active = false;
|
|
351
|
+
}
|
|
352
|
+
this.scheduleDirectoryEventReconnect(normalizedDirectory, '目录事件流中断');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
console.error(`[OpenCode] 目录事件流订阅失败: ${normalizedDirectory}`, error);
|
|
359
|
+
// 先清理可能存在的重连定时器,再删除条目,防止僵尸 timer 继续触发
|
|
360
|
+
this.clearDirectoryEventReconnectTimer(normalizedDirectory);
|
|
361
|
+
this.directoryEventStreams.delete(normalizedDirectory);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// 启动SSE事件监听
|
|
365
|
+
async startEventListener() {
|
|
366
|
+
if (!this.client || !this.eventListeningEnabled)
|
|
367
|
+
return;
|
|
368
|
+
if (this.eventStreamActive)
|
|
369
|
+
return;
|
|
370
|
+
this.eventStreamActive = true;
|
|
371
|
+
this.clearEventReconnectTimer();
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
if (this.eventAbortController) {
|
|
374
|
+
this.eventAbortController.abort();
|
|
375
|
+
}
|
|
376
|
+
this.eventAbortController = controller;
|
|
377
|
+
try {
|
|
378
|
+
const events = await this.client.event.subscribe();
|
|
379
|
+
console.log('[OpenCode] 事件流订阅成功');
|
|
380
|
+
this.eventReconnectAttempt = 0;
|
|
381
|
+
// 异步处理事件流
|
|
382
|
+
(async () => {
|
|
383
|
+
try {
|
|
384
|
+
for await (const event of events.stream) {
|
|
385
|
+
if (controller.signal.aborted || !this.eventListeningEnabled) {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
// Debug log for permission requests to catch missing ones
|
|
389
|
+
if (event.type.toLowerCase().includes('permission')) {
|
|
390
|
+
console.log(`[OpenCode] 收到底层事件: ${event.type}`, JSON.stringify(event.properties || {}).slice(0, 1200));
|
|
391
|
+
}
|
|
392
|
+
this.handleEvent(event);
|
|
393
|
+
}
|
|
394
|
+
if (!controller.signal.aborted && this.eventListeningEnabled) {
|
|
395
|
+
this.scheduleEventReconnect('事件流已结束');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (!controller.signal.aborted && this.eventListeningEnabled) {
|
|
400
|
+
console.error('[OpenCode] 事件流中断:', error);
|
|
401
|
+
this.scheduleEventReconnect('事件流中断');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
if (this.eventAbortController === controller) {
|
|
406
|
+
this.eventAbortController = null;
|
|
407
|
+
}
|
|
408
|
+
this.eventStreamActive = false;
|
|
409
|
+
}
|
|
410
|
+
})();
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
console.error('[OpenCode] 无法订阅事件:', error);
|
|
414
|
+
this.eventStreamActive = false;
|
|
415
|
+
if (!controller.signal.aborted && this.eventListeningEnabled) {
|
|
416
|
+
this.scheduleEventReconnect('订阅失败');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// 处理SSE事件
|
|
421
|
+
handleEvent(event) {
|
|
422
|
+
const eventType = event.type.toLowerCase();
|
|
423
|
+
// 权限请求事件(兼容不同事件命名)
|
|
424
|
+
if (isPermissionRequestEventType(eventType) && event.properties) {
|
|
425
|
+
const props = event.properties;
|
|
426
|
+
const correlation = extractPermissionCorrelation(props);
|
|
427
|
+
const directSessionId = getFirstString(props.sessionID, props.sessionId, props.session_id);
|
|
428
|
+
const sessionId = getFirstString(directSessionId, correlation.relatedSessionId, correlation.parentSessionId);
|
|
429
|
+
const permissionEvent = {
|
|
430
|
+
sessionId,
|
|
431
|
+
permissionId: getFirstString(props.id, props.requestId, props.requestID, props.request_id, props.permissionId, props.permissionID, props.permission_id),
|
|
432
|
+
// permission.asked 的 tool 常为对象(messageID/callID),显示/判断应优先用 permission
|
|
433
|
+
tool: getPermissionLabel(props),
|
|
434
|
+
// If description is missing, try to construct one from metadata
|
|
435
|
+
description: props.description || (props.metadata ? JSON.stringify(props.metadata) : ''),
|
|
436
|
+
risk: props.risk,
|
|
437
|
+
...(correlation.parentSessionId ? { parentSessionId: correlation.parentSessionId } : {}),
|
|
438
|
+
...(correlation.relatedSessionId ? { relatedSessionId: correlation.relatedSessionId } : {}),
|
|
439
|
+
...(correlation.messageId ? { messageId: correlation.messageId } : {}),
|
|
440
|
+
...(correlation.callId ? { callId: correlation.callId } : {}),
|
|
441
|
+
};
|
|
442
|
+
if (!permissionEvent.sessionId || !permissionEvent.permissionId) {
|
|
443
|
+
console.warn('[OpenCode] 权限事件缺少关键字段:', event.type, JSON.stringify(event.properties || {}).slice(0, 1200));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.emit('permissionRequest', permissionEvent);
|
|
447
|
+
}
|
|
448
|
+
// 消息更新事件
|
|
449
|
+
if (event.type === 'message.updated' && event.properties) {
|
|
450
|
+
this.emit('messageUpdated', event.properties);
|
|
451
|
+
}
|
|
452
|
+
// 会话状态变化事件
|
|
453
|
+
if (event.type === 'session.status' && event.properties) {
|
|
454
|
+
this.emit('sessionStatus', event.properties);
|
|
455
|
+
}
|
|
456
|
+
// 会话空闲事件(处理完成)
|
|
457
|
+
if (event.type === 'session.idle' && event.properties) {
|
|
458
|
+
this.emit('sessionIdle', event.properties);
|
|
459
|
+
}
|
|
460
|
+
// 会话错误事件
|
|
461
|
+
if (event.type === 'session.error' && event.properties) {
|
|
462
|
+
this.emit('sessionError', event.properties);
|
|
463
|
+
}
|
|
464
|
+
// 消息部分更新事件(流式输出)
|
|
465
|
+
if (event.type === 'message.part.updated' && event.properties) {
|
|
466
|
+
this.emit('messagePartUpdated', event.properties);
|
|
467
|
+
}
|
|
468
|
+
// AI 提问事件
|
|
469
|
+
if (event.type === 'question.asked' && event.properties) {
|
|
470
|
+
this.emit('questionAsked', event.properties);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// 获取客户端实例
|
|
474
|
+
getClient() {
|
|
475
|
+
if (!this.client) {
|
|
476
|
+
throw new Error('OpenCode客户端未连接');
|
|
477
|
+
}
|
|
478
|
+
return this.client;
|
|
479
|
+
}
|
|
480
|
+
// 获取或创建会话
|
|
481
|
+
async getOrCreateSession(title) {
|
|
482
|
+
const client = this.getClient();
|
|
483
|
+
// 尝试获取现有会话列表
|
|
484
|
+
const sessions = await client.session.list();
|
|
485
|
+
// 如果有会话,返回最近的一个
|
|
486
|
+
if (sessions.data && sessions.data.length > 0) {
|
|
487
|
+
const latestSession = sessions.data[0];
|
|
488
|
+
return latestSession;
|
|
489
|
+
}
|
|
490
|
+
// 创建新会话
|
|
491
|
+
const newSession = await client.session.create({
|
|
492
|
+
body: { title: title || '飞书对话' },
|
|
493
|
+
});
|
|
494
|
+
return newSession.data;
|
|
495
|
+
}
|
|
496
|
+
resolveModelOption(options) {
|
|
497
|
+
const providerId = options?.providerId?.trim();
|
|
498
|
+
const modelId = options?.modelId?.trim();
|
|
499
|
+
if (providerId && modelId) {
|
|
500
|
+
return {
|
|
501
|
+
providerID: providerId,
|
|
502
|
+
modelID: modelId,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const defaultProvider = modelConfig.defaultProvider;
|
|
506
|
+
const defaultModel = modelConfig.defaultModel;
|
|
507
|
+
if (defaultProvider && defaultModel) {
|
|
508
|
+
return {
|
|
509
|
+
providerID: defaultProvider,
|
|
510
|
+
modelID: defaultModel,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
normalizeDirectory(directory) {
|
|
516
|
+
if (typeof directory !== 'string') {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
const normalized = directory.trim();
|
|
520
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
521
|
+
}
|
|
522
|
+
buildPermissionDirectoryCandidates(options) {
|
|
523
|
+
const candidates = [];
|
|
524
|
+
const seen = new Set();
|
|
525
|
+
const pushDirectory = (directory) => {
|
|
526
|
+
const normalized = this.normalizeDirectory(directory);
|
|
527
|
+
if (!normalized) {
|
|
528
|
+
if (!seen.has('__default__')) {
|
|
529
|
+
seen.add('__default__');
|
|
530
|
+
candidates.push(undefined);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const key = process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
535
|
+
if (seen.has(key)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
seen.add(key);
|
|
539
|
+
candidates.push(normalized);
|
|
540
|
+
};
|
|
541
|
+
pushDirectory(options?.directory);
|
|
542
|
+
if (Array.isArray(options?.fallbackDirectories)) {
|
|
543
|
+
for (const directory of options.fallbackDirectories) {
|
|
544
|
+
pushDirectory(directory);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
pushDirectory(undefined);
|
|
548
|
+
return candidates;
|
|
549
|
+
}
|
|
550
|
+
// 发送消息并等待响应
|
|
551
|
+
async sendMessage(sessionId, text, options) {
|
|
552
|
+
const client = this.getClient();
|
|
553
|
+
const model = this.resolveModelOption(options);
|
|
554
|
+
if (options?.directory) {
|
|
555
|
+
void this.ensureDirectoryEventStream(options.directory);
|
|
556
|
+
}
|
|
557
|
+
const response = await client.session.prompt({
|
|
558
|
+
path: { id: sessionId },
|
|
559
|
+
body: {
|
|
560
|
+
parts: [{ type: 'text', text }],
|
|
561
|
+
...(options?.agent ? { agent: options.agent } : {}),
|
|
562
|
+
...(model ? { model } : {}),
|
|
563
|
+
...(options?.variant ? { variant: options.variant } : {}),
|
|
564
|
+
},
|
|
565
|
+
...(options?.directory ? { query: { directory: options.directory } } : {}),
|
|
566
|
+
});
|
|
567
|
+
return response.data;
|
|
568
|
+
}
|
|
569
|
+
// 发送带多类型 parts 的消息
|
|
570
|
+
async sendMessageParts(sessionId, parts, options, messageId) {
|
|
571
|
+
const client = this.getClient();
|
|
572
|
+
const model = this.resolveModelOption(options);
|
|
573
|
+
if (options?.directory) {
|
|
574
|
+
void this.ensureDirectoryEventStream(options.directory);
|
|
575
|
+
}
|
|
576
|
+
const response = await client.session.prompt({
|
|
577
|
+
path: { id: sessionId },
|
|
578
|
+
body: {
|
|
579
|
+
parts,
|
|
580
|
+
// ...(messageId ? { messageID: messageId } : {}), // 已注释:避免传递飞书 MessageID 导致 Opencode 无法处理
|
|
581
|
+
...(options?.agent ? { agent: options.agent } : {}),
|
|
582
|
+
...(model ? { model } : {}),
|
|
583
|
+
...(options?.variant ? { variant: options.variant } : {}),
|
|
584
|
+
},
|
|
585
|
+
...(options?.directory ? { query: { directory: options.directory } } : {}),
|
|
586
|
+
});
|
|
587
|
+
return response.data;
|
|
588
|
+
}
|
|
589
|
+
// 异步发送消息(不等待响应)
|
|
590
|
+
async sendMessageAsync(sessionId, text, options) {
|
|
591
|
+
this.getClient();
|
|
592
|
+
const model = this.resolveModelOption(options);
|
|
593
|
+
if (options?.directory) {
|
|
594
|
+
void this.ensureDirectoryEventStream(options.directory);
|
|
595
|
+
}
|
|
596
|
+
const dirQuery = options?.directory ? `?directory=${encodeURIComponent(options.directory)}` : '';
|
|
597
|
+
const response = await fetch(`${opencodeConfig.baseUrl}/session/${sessionId}/prompt_async${dirQuery}`, {
|
|
598
|
+
method: 'POST',
|
|
599
|
+
headers: withOpencodeAuthorizationHeaders({ 'Content-Type': 'application/json' }),
|
|
600
|
+
body: JSON.stringify({
|
|
601
|
+
parts: [{ type: 'text', text }],
|
|
602
|
+
...(options?.agent ? { agent: options.agent } : {}),
|
|
603
|
+
...(model ? { model } : {}),
|
|
604
|
+
...(options?.variant ? { variant: options.variant } : {}),
|
|
605
|
+
}),
|
|
606
|
+
});
|
|
607
|
+
if (!response.ok) {
|
|
608
|
+
const detail = await response.text().catch(() => '');
|
|
609
|
+
const suffix = detail ? `: ${detail.slice(0, 300)}` : '';
|
|
610
|
+
const message = `prompt_async 请求失败 (${response.status} ${response.statusText})${suffix}`;
|
|
611
|
+
throw new Error(appendAuthHint(message, response.status));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// 异步发送多 parts 消息(立即返回,结果通过事件流推送)
|
|
615
|
+
async sendMessagePartsAsync(sessionId, parts, options) {
|
|
616
|
+
this.getClient();
|
|
617
|
+
const model = this.resolveModelOption(options);
|
|
618
|
+
if (options?.directory) {
|
|
619
|
+
void this.ensureDirectoryEventStream(options.directory);
|
|
620
|
+
}
|
|
621
|
+
const dirQuery = options?.directory ? `?directory=${encodeURIComponent(options.directory)}` : '';
|
|
622
|
+
const response = await fetch(`${opencodeConfig.baseUrl}/session/${sessionId}/prompt_async${dirQuery}`, {
|
|
623
|
+
method: 'POST',
|
|
624
|
+
headers: withOpencodeAuthorizationHeaders({ 'Content-Type': 'application/json' }),
|
|
625
|
+
body: JSON.stringify({
|
|
626
|
+
parts,
|
|
627
|
+
...(options?.agent ? { agent: options.agent } : {}),
|
|
628
|
+
...(model ? { model } : {}),
|
|
629
|
+
...(options?.variant ? { variant: options.variant } : {}),
|
|
630
|
+
}),
|
|
631
|
+
});
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
const detail = await response.text().catch(() => '');
|
|
634
|
+
const suffix = detail ? `: ${detail.slice(0, 300)}` : '';
|
|
635
|
+
const message = `prompt_async 请求失败 (${response.status} ${response.statusText})${suffix}`;
|
|
636
|
+
throw new Error(appendAuthHint(message, response.status));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// 发送命令
|
|
640
|
+
async sendCommand(sessionId, command, args, options) {
|
|
641
|
+
const client = this.getClient();
|
|
642
|
+
if (options?.directory) {
|
|
643
|
+
void this.ensureDirectoryEventStream(options.directory);
|
|
644
|
+
}
|
|
645
|
+
const result = await client.session.command({
|
|
646
|
+
path: { id: sessionId },
|
|
647
|
+
body: {
|
|
648
|
+
command,
|
|
649
|
+
arguments: args,
|
|
650
|
+
},
|
|
651
|
+
...(options?.directory ? { query: { directory: options.directory } } : {}),
|
|
652
|
+
});
|
|
653
|
+
if (result.error) {
|
|
654
|
+
const statusCode = result.response?.status;
|
|
655
|
+
const detail = formatSdkError(result.error);
|
|
656
|
+
const message = statusCode
|
|
657
|
+
? `OpenCode 命令调用失败(HTTP ${statusCode}): ${detail}`
|
|
658
|
+
: `OpenCode 命令调用失败: ${detail}`;
|
|
659
|
+
throw new Error(appendAuthHint(message, statusCode));
|
|
660
|
+
}
|
|
661
|
+
return result.data;
|
|
662
|
+
}
|
|
663
|
+
async sendShellCommand(sessionId, command, agent, options) {
|
|
664
|
+
this.getClient();
|
|
665
|
+
if (options?.directory) {
|
|
666
|
+
void this.ensureDirectoryEventStream(options.directory);
|
|
667
|
+
}
|
|
668
|
+
const model = options?.providerId && options?.modelId
|
|
669
|
+
? {
|
|
670
|
+
providerID: options.providerId,
|
|
671
|
+
modelID: options.modelId,
|
|
672
|
+
}
|
|
673
|
+
: undefined;
|
|
674
|
+
const dirQuery = options?.directory ? `?directory=${encodeURIComponent(options.directory)}` : '';
|
|
675
|
+
const response = await fetch(`${opencodeConfig.baseUrl}/session/${sessionId}/shell${dirQuery}`, {
|
|
676
|
+
method: 'POST',
|
|
677
|
+
headers: withOpencodeAuthorizationHeaders({ 'Content-Type': 'application/json' }),
|
|
678
|
+
body: JSON.stringify({
|
|
679
|
+
agent,
|
|
680
|
+
command,
|
|
681
|
+
...(model ? { model } : {}),
|
|
682
|
+
}),
|
|
683
|
+
});
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
const detail = await response.text().catch(() => '');
|
|
686
|
+
const suffix = detail ? `: ${detail.slice(0, 500)}` : '';
|
|
687
|
+
const message = `OpenCode Shell 调用失败(HTTP ${response.status} ${response.statusText})${suffix}`;
|
|
688
|
+
throw new Error(appendAuthHint(message, response.status));
|
|
689
|
+
}
|
|
690
|
+
const payload = await response.json().catch(() => null);
|
|
691
|
+
if (!payload || typeof payload !== 'object') {
|
|
692
|
+
return { parts: [] };
|
|
693
|
+
}
|
|
694
|
+
const record = payload;
|
|
695
|
+
const parts = Array.isArray(record.parts) ? record.parts : [];
|
|
696
|
+
if (record.info && typeof record.info === 'object') {
|
|
697
|
+
return {
|
|
698
|
+
info: record.info,
|
|
699
|
+
parts,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
if (typeof record.id === 'string' && typeof record.sessionID === 'string') {
|
|
703
|
+
return {
|
|
704
|
+
info: record,
|
|
705
|
+
parts,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return { parts };
|
|
709
|
+
}
|
|
710
|
+
async summarizeSession(sessionId, providerId, modelId) {
|
|
711
|
+
const client = this.getClient();
|
|
712
|
+
const result = await client.session.summarize({
|
|
713
|
+
path: { id: sessionId },
|
|
714
|
+
body: {
|
|
715
|
+
providerID: providerId,
|
|
716
|
+
modelID: modelId,
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
if (result.error) {
|
|
720
|
+
const statusCode = result.response?.status;
|
|
721
|
+
const detail = formatSdkError(result.error);
|
|
722
|
+
const message = statusCode
|
|
723
|
+
? `会话压缩失败(HTTP ${statusCode}): ${detail}`
|
|
724
|
+
: `会话压缩失败: ${detail}`;
|
|
725
|
+
throw new Error(appendAuthHint(message, statusCode));
|
|
726
|
+
}
|
|
727
|
+
return result.data === true;
|
|
728
|
+
}
|
|
729
|
+
// 撤回消息
|
|
730
|
+
async revertMessage(sessionId, messageId) {
|
|
731
|
+
const client = this.getClient();
|
|
732
|
+
try {
|
|
733
|
+
const result = await client.session.revert({
|
|
734
|
+
path: { id: sessionId },
|
|
735
|
+
body: { messageID: messageId },
|
|
736
|
+
});
|
|
737
|
+
return Boolean(result.data);
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
console.error('[OpenCode] 撤回消息失败:', error);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// 中断会话执行
|
|
745
|
+
async abortSession(sessionId) {
|
|
746
|
+
const client = this.getClient();
|
|
747
|
+
try {
|
|
748
|
+
const result = await client.session.abort({
|
|
749
|
+
path: { id: sessionId },
|
|
750
|
+
});
|
|
751
|
+
return result.data === true;
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
console.error('[OpenCode] 中断会话失败:', error);
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// 响应权限请求
|
|
759
|
+
async respondToPermission(sessionId, permissionId, allow, remember = false, options) {
|
|
760
|
+
const responseType = allow ? (remember ? 'always' : 'once') : 'reject';
|
|
761
|
+
const directoryCandidates = this.buildPermissionDirectoryCandidates(options);
|
|
762
|
+
for (const directory of directoryCandidates) {
|
|
763
|
+
try {
|
|
764
|
+
const query = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
|
765
|
+
const response = await fetch(`${opencodeConfig.baseUrl}/session/${sessionId}/permissions/${permissionId}${query}`, {
|
|
766
|
+
method: 'POST',
|
|
767
|
+
headers: withOpencodeAuthorizationHeaders({ 'Content-Type': 'application/json' }),
|
|
768
|
+
body: JSON.stringify({
|
|
769
|
+
response: responseType,
|
|
770
|
+
}),
|
|
771
|
+
});
|
|
772
|
+
if (response.ok) {
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
const detail = await response.text().catch(() => '');
|
|
776
|
+
const suffix = detail ? `: ${detail.slice(0, 300)}` : '';
|
|
777
|
+
const message = appendAuthHint(`权限响应失败(HTTP ${response.status} ${response.statusText})${suffix}`, response.status);
|
|
778
|
+
const directoryLabel = directory ? `directory=${directory}` : 'directory=<default>';
|
|
779
|
+
console.error(`[OpenCode] ${message} (${directoryLabel})`);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
const directoryLabel = directory ? `directory=${directory}` : 'directory=<default>';
|
|
783
|
+
console.error(`[OpenCode] 响应权限失败 (${directoryLabel}):`, error);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
// 获取工作区列表
|
|
789
|
+
async listProjects(options) {
|
|
790
|
+
const client = this.getClient();
|
|
791
|
+
const directory = this.normalizeDirectory(options?.directory);
|
|
792
|
+
const result = await client.project.list(directory ? { query: { directory } } : undefined);
|
|
793
|
+
return Array.isArray(result.data) ? result.data : [];
|
|
794
|
+
}
|
|
795
|
+
// 获取会话列表(可按目录过滤)
|
|
796
|
+
async listSessions(options) {
|
|
797
|
+
const client = this.getClient();
|
|
798
|
+
const directory = this.normalizeDirectory(options?.directory);
|
|
799
|
+
const result = await client.session.list(directory ? { query: { directory } } : undefined);
|
|
800
|
+
return Array.isArray(result.data) ? result.data : [];
|
|
801
|
+
}
|
|
802
|
+
// 跨工作区聚合会话列表
|
|
803
|
+
async listSessionsAcrossProjects() {
|
|
804
|
+
const merged = new Map();
|
|
805
|
+
const upsertSessions = (sessions) => {
|
|
806
|
+
for (const session of sessions) {
|
|
807
|
+
const existing = merged.get(session.id);
|
|
808
|
+
if (!existing) {
|
|
809
|
+
merged.set(session.id, session);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const existingUpdated = existing.time?.updated ?? existing.time?.created ?? 0;
|
|
813
|
+
const nextUpdated = session.time?.updated ?? session.time?.created ?? 0;
|
|
814
|
+
if (nextUpdated >= existingUpdated) {
|
|
815
|
+
merged.set(session.id, session);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
try {
|
|
820
|
+
upsertSessions(await this.listSessions());
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
console.warn('[OpenCode] 获取默认作用域会话列表失败:', error);
|
|
824
|
+
}
|
|
825
|
+
const projects = await this.listProjects();
|
|
826
|
+
const directories = [];
|
|
827
|
+
const seenDirectories = new Set();
|
|
828
|
+
for (const project of projects) {
|
|
829
|
+
const normalized = this.normalizeDirectory(project.worktree);
|
|
830
|
+
if (!normalized || seenDirectories.has(normalized)) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
seenDirectories.add(normalized);
|
|
834
|
+
directories.push(normalized);
|
|
835
|
+
}
|
|
836
|
+
const sessionGroups = await Promise.all(directories.map(async (directory) => {
|
|
837
|
+
try {
|
|
838
|
+
return await this.listSessions({ directory });
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
console.warn(`[OpenCode] 获取目录会话列表失败: directory=${directory}`, error);
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
}));
|
|
845
|
+
for (const sessions of sessionGroups) {
|
|
846
|
+
upsertSessions(sessions);
|
|
847
|
+
}
|
|
848
|
+
return Array.from(merged.values());
|
|
849
|
+
}
|
|
850
|
+
// 聚合查询所有已知目录的 session(默认 Instance + 各自定义 directory Instance)
|
|
851
|
+
async listAllSessions(knownDirectories) {
|
|
852
|
+
const allSessions = [];
|
|
853
|
+
const seen = new Set();
|
|
854
|
+
// 1. 默认 Instance
|
|
855
|
+
try {
|
|
856
|
+
const defaultSessions = await this.listSessions();
|
|
857
|
+
for (const s of defaultSessions) {
|
|
858
|
+
if (!seen.has(s.id)) {
|
|
859
|
+
seen.add(s.id);
|
|
860
|
+
allSessions.push(s);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
// 默认 Instance 查询失败不阻塞
|
|
866
|
+
}
|
|
867
|
+
// 2. 各自定义目录的 Instance
|
|
868
|
+
for (const dir of knownDirectories) {
|
|
869
|
+
try {
|
|
870
|
+
const sessions = await this.listSessions({ directory: dir });
|
|
871
|
+
for (const s of sessions) {
|
|
872
|
+
if (!seen.has(s.id)) {
|
|
873
|
+
seen.add(s.id);
|
|
874
|
+
allSessions.push(s);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// 单个目录查询失败不阻塞其他
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return allSessions;
|
|
883
|
+
}
|
|
884
|
+
// 通过 ID 获取会话(可按目录限定)
|
|
885
|
+
async getSessionById(sessionId, options) {
|
|
886
|
+
const client = this.getClient();
|
|
887
|
+
const normalizedSessionId = sessionId.trim();
|
|
888
|
+
if (!normalizedSessionId) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
const directory = this.normalizeDirectory(options?.directory);
|
|
892
|
+
const result = await client.session.get({
|
|
893
|
+
path: { id: normalizedSessionId },
|
|
894
|
+
...(directory ? { query: { directory } } : {}),
|
|
895
|
+
});
|
|
896
|
+
if (result.error) {
|
|
897
|
+
const statusCode = result.response?.status;
|
|
898
|
+
if (statusCode === 404) {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
const detail = formatSdkError(result.error);
|
|
902
|
+
const message = statusCode
|
|
903
|
+
? `获取会话失败(HTTP ${statusCode}): ${detail}`
|
|
904
|
+
: `获取会话失败: ${detail}`;
|
|
905
|
+
throw new Error(appendAuthHint(message, statusCode));
|
|
906
|
+
}
|
|
907
|
+
return result.data || null;
|
|
908
|
+
}
|
|
909
|
+
// 跨工作区按 ID 查找会话
|
|
910
|
+
async findSessionAcrossProjects(sessionId) {
|
|
911
|
+
const normalizedSessionId = sessionId.trim();
|
|
912
|
+
if (!normalizedSessionId) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
const direct = await this.getSessionById(normalizedSessionId);
|
|
916
|
+
if (direct) {
|
|
917
|
+
return direct;
|
|
918
|
+
}
|
|
919
|
+
const projects = await this.listProjects();
|
|
920
|
+
const directories = [];
|
|
921
|
+
const seenDirectories = new Set();
|
|
922
|
+
for (const project of projects) {
|
|
923
|
+
const normalized = this.normalizeDirectory(project.worktree);
|
|
924
|
+
if (!normalized || seenDirectories.has(normalized)) {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
seenDirectories.add(normalized);
|
|
928
|
+
directories.push(normalized);
|
|
929
|
+
}
|
|
930
|
+
for (const directory of directories) {
|
|
931
|
+
const found = await this.getSessionById(normalizedSessionId, { directory });
|
|
932
|
+
if (found) {
|
|
933
|
+
return found;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
// 创建新会话
|
|
939
|
+
async createSession(title, directory) {
|
|
940
|
+
const client = this.getClient();
|
|
941
|
+
const normalizedDir = this.normalizeDirectory(directory);
|
|
942
|
+
if (normalizedDir) {
|
|
943
|
+
void this.ensureDirectoryEventStream(normalizedDir);
|
|
944
|
+
}
|
|
945
|
+
const result = await client.session.create({
|
|
946
|
+
body: { title: title || '新对话' },
|
|
947
|
+
...(normalizedDir ? { query: { directory: normalizedDir } } : {}),
|
|
948
|
+
});
|
|
949
|
+
return result.data;
|
|
950
|
+
}
|
|
951
|
+
// 更新会话标题
|
|
952
|
+
async updateSession(sessionId, title) {
|
|
953
|
+
const client = this.getClient();
|
|
954
|
+
const trimmedTitle = title.trim();
|
|
955
|
+
if (!trimmedTitle) {
|
|
956
|
+
console.warn('[OpenCode] updateSession: 标题不能为空');
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
const result = await client.session.update({
|
|
961
|
+
path: { id: sessionId },
|
|
962
|
+
body: { title: trimmedTitle },
|
|
963
|
+
});
|
|
964
|
+
if (result.error) {
|
|
965
|
+
const statusCode = result.response?.status;
|
|
966
|
+
const detail = formatSdkError(result.error);
|
|
967
|
+
const message = statusCode
|
|
968
|
+
? `会话重命名失败(HTTP ${statusCode}): ${detail}`
|
|
969
|
+
: `会话重命名失败: ${detail}`;
|
|
970
|
+
console.error(`[OpenCode] ${appendAuthHint(message, statusCode)}`);
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
console.error(`[OpenCode] 更新会话标题失败: ${sessionId}`, error);
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// 删除会话
|
|
981
|
+
async deleteSession(sessionId, options) {
|
|
982
|
+
const client = this.getClient();
|
|
983
|
+
try {
|
|
984
|
+
const directory = this.normalizeDirectory(options?.directory);
|
|
985
|
+
await client.session.delete({
|
|
986
|
+
path: { id: sessionId },
|
|
987
|
+
...(directory ? { query: { directory } } : {}),
|
|
988
|
+
});
|
|
989
|
+
console.log(`[OpenCode] 已删除会话: ${sessionId}`);
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
console.error(`[OpenCode] 删除会话失败: ${sessionId}`, error);
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// 获取会话消息
|
|
998
|
+
async getSessionMessages(sessionId) {
|
|
999
|
+
const client = this.getClient();
|
|
1000
|
+
const result = await client.session.messages({
|
|
1001
|
+
path: { id: sessionId },
|
|
1002
|
+
});
|
|
1003
|
+
return result.data || [];
|
|
1004
|
+
}
|
|
1005
|
+
// 获取配置(含模型列表)
|
|
1006
|
+
async getProviders() {
|
|
1007
|
+
const client = this.getClient();
|
|
1008
|
+
const result = await client.config.providers();
|
|
1009
|
+
return result.data;
|
|
1010
|
+
}
|
|
1011
|
+
// 获取完整配置
|
|
1012
|
+
async getConfig() {
|
|
1013
|
+
const client = this.getClient();
|
|
1014
|
+
const result = await client.config.get();
|
|
1015
|
+
return (result.data || {});
|
|
1016
|
+
}
|
|
1017
|
+
// 更新完整配置
|
|
1018
|
+
async updateConfig(config) {
|
|
1019
|
+
const client = this.getClient();
|
|
1020
|
+
try {
|
|
1021
|
+
const result = await client.config.update({
|
|
1022
|
+
body: config,
|
|
1023
|
+
});
|
|
1024
|
+
return (result.data || null);
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
console.error('[OpenCode] 更新配置失败:', error);
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// 获取可用 Agent 列表
|
|
1032
|
+
async getAgents() {
|
|
1033
|
+
const client = this.getClient();
|
|
1034
|
+
const result = await client.app.agents();
|
|
1035
|
+
const rawAgents = Array.isArray(result.data) ? result.data : [];
|
|
1036
|
+
const agents = [];
|
|
1037
|
+
for (const item of rawAgents) {
|
|
1038
|
+
if (!item || typeof item !== 'object')
|
|
1039
|
+
continue;
|
|
1040
|
+
const record = item;
|
|
1041
|
+
const name = typeof record.name === 'string' ? record.name.trim() : '';
|
|
1042
|
+
if (!name)
|
|
1043
|
+
continue;
|
|
1044
|
+
const description = typeof record.description === 'string' && record.description.trim().length > 0
|
|
1045
|
+
? record.description.trim()
|
|
1046
|
+
: undefined;
|
|
1047
|
+
const mode = parseAgentMode(record.mode);
|
|
1048
|
+
const hidden = parseBoolean(record.hidden);
|
|
1049
|
+
const builtIn = parseBoolean(record.builtIn);
|
|
1050
|
+
const native = parseBoolean(record.native);
|
|
1051
|
+
agents.push({
|
|
1052
|
+
name,
|
|
1053
|
+
description,
|
|
1054
|
+
mode,
|
|
1055
|
+
...(hidden !== undefined ? { hidden } : {}),
|
|
1056
|
+
...(builtIn !== undefined ? { builtIn } : {}),
|
|
1057
|
+
...(native !== undefined ? { native } : {}),
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return agents;
|
|
1061
|
+
}
|
|
1062
|
+
// 回复问题 (question 工具)
|
|
1063
|
+
// answers 是一个二维数组: [[第一个问题的答案们], [第二个问题的答案们], ...]
|
|
1064
|
+
// 每个答案是选项的 label
|
|
1065
|
+
async replyQuestion(requestId, answers) {
|
|
1066
|
+
try {
|
|
1067
|
+
const response = await fetch(`${opencodeConfig.baseUrl}/question/${requestId}/reply`, {
|
|
1068
|
+
method: 'POST',
|
|
1069
|
+
headers: withOpencodeAuthorizationHeaders({ 'Content-Type': 'application/json' }),
|
|
1070
|
+
body: JSON.stringify({ answers }),
|
|
1071
|
+
});
|
|
1072
|
+
if (!response.ok) {
|
|
1073
|
+
const detail = await response.text().catch(() => '');
|
|
1074
|
+
const suffix = detail ? `: ${detail.slice(0, 300)}` : '';
|
|
1075
|
+
const message = appendAuthHint(`回复问题失败(HTTP ${response.status} ${response.statusText})${suffix}`, response.status);
|
|
1076
|
+
console.error(`[OpenCode] ${message}`);
|
|
1077
|
+
}
|
|
1078
|
+
return response.ok;
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
console.error('[OpenCode] 回复问题失败:', error);
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// 拒绝/跳过问题
|
|
1086
|
+
async rejectQuestion(requestId) {
|
|
1087
|
+
try {
|
|
1088
|
+
const response = await fetch(`${opencodeConfig.baseUrl}/question/${requestId}/reject`, {
|
|
1089
|
+
method: 'POST',
|
|
1090
|
+
headers: withOpencodeAuthorizationHeaders({ 'Content-Type': 'application/json' }),
|
|
1091
|
+
});
|
|
1092
|
+
if (!response.ok) {
|
|
1093
|
+
const detail = await response.text().catch(() => '');
|
|
1094
|
+
const suffix = detail ? `: ${detail.slice(0, 300)}` : '';
|
|
1095
|
+
const message = appendAuthHint(`拒绝问题失败(HTTP ${response.status} ${response.statusText})${suffix}`, response.status);
|
|
1096
|
+
console.error(`[OpenCode] ${message}`);
|
|
1097
|
+
}
|
|
1098
|
+
return response.ok;
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
console.error('[OpenCode] 拒绝问题失败:', error);
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// 断开连接
|
|
1106
|
+
disconnect() {
|
|
1107
|
+
this.eventListeningEnabled = false;
|
|
1108
|
+
this.eventStreamActive = false;
|
|
1109
|
+
this.clearEventReconnectTimer();
|
|
1110
|
+
this.eventReconnectAttempt = 0;
|
|
1111
|
+
if (this.eventAbortController) {
|
|
1112
|
+
this.eventAbortController.abort();
|
|
1113
|
+
this.eventAbortController = null;
|
|
1114
|
+
}
|
|
1115
|
+
for (const [directory, entry] of this.directoryEventStreams) {
|
|
1116
|
+
this.clearDirectoryEventReconnectTimer(directory);
|
|
1117
|
+
entry.controller.abort();
|
|
1118
|
+
}
|
|
1119
|
+
this.directoryEventStreams.clear();
|
|
1120
|
+
this.client = null;
|
|
1121
|
+
console.log('[OpenCode] 已断开连接');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// 单例导出
|
|
1125
|
+
export const opencodeClient = new OpencodeClientWrapper();
|
|
1126
|
+
//# sourceMappingURL=client.js.map
|