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,1832 @@
|
|
|
1
|
+
import { ActionRowBuilder, ChannelType, MessageFlags, ModalBuilder, PermissionFlagsBits, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle, } from 'discord.js';
|
|
2
|
+
import { promises as fsp } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { groupConfig, modelConfig } from '../config.js';
|
|
5
|
+
import { KNOWN_EFFORT_LEVELS, normalizeEffortLevel, stripPromptEffortPrefix } from '../commands/effort.js';
|
|
6
|
+
import { opencodeClient } from '../opencode/client.js';
|
|
7
|
+
import { outputBuffer } from '../opencode/output-buffer.js';
|
|
8
|
+
import { parseQuestionAnswerText } from '../opencode/question-parser.js';
|
|
9
|
+
import { questionHandler } from '../opencode/question-handler.js';
|
|
10
|
+
import { permissionHandler } from '../permissions/handler.js';
|
|
11
|
+
import { chatSessionStore } from '../store/chat-session.js';
|
|
12
|
+
import { validateFilePath } from './file-sender.js';
|
|
13
|
+
import { DirectoryPolicy } from '../utils/directory-policy.js';
|
|
14
|
+
import { buildCronHelpText, executeCronIntent, parseCronSlashIntent, resolveCronIntentForExecution, } from '../reliability/cron-control.js';
|
|
15
|
+
import { getRuntimeCronManager } from '../reliability/runtime-cron-registry.js';
|
|
16
|
+
import { formatRestartResultText, restartOpenCodeProcess } from '../reliability/opencode-restart.js';
|
|
17
|
+
import { parseCronIntentWithOpenCode } from '../reliability/cron-semantic.js';
|
|
18
|
+
const PANEL_SELECT_PREFIX = 'oc_panel';
|
|
19
|
+
const BIND_SELECT_PREFIX = 'oc_bind';
|
|
20
|
+
const RENAME_MODAL_PREFIX = 'oc_rename';
|
|
21
|
+
const QUESTION_SELECT_PREFIX = 'oc_question';
|
|
22
|
+
const MODEL_SELECT_PREFIX = 'oc_model';
|
|
23
|
+
const AGENT_SELECT_PREFIX = 'oc_agent';
|
|
24
|
+
const RENAME_INPUT_ID = 'session_name';
|
|
25
|
+
const MAX_SESSION_OPTIONS = 25;
|
|
26
|
+
const MAX_MODEL_OPTIONS = 500;
|
|
27
|
+
const MODEL_PAGE_SIZE = 24;
|
|
28
|
+
const DISCORD_FILE_MAX_SIZE = 25 * 1024 * 1024;
|
|
29
|
+
function normalizeMessageText(value) {
|
|
30
|
+
return value.trim();
|
|
31
|
+
}
|
|
32
|
+
function parsePermissionDecision(raw) {
|
|
33
|
+
const normalized = raw.normalize('NFKC').trim().toLowerCase();
|
|
34
|
+
if (!normalized) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const compact = normalized
|
|
38
|
+
.replace(/[\s\u3000]+/g, '')
|
|
39
|
+
.replace(/[。!!,.,;;::\-]/g, '');
|
|
40
|
+
const hasAlways = compact.includes('始终')
|
|
41
|
+
|| compact.includes('永久')
|
|
42
|
+
|| compact.includes('always')
|
|
43
|
+
|| compact.includes('记住')
|
|
44
|
+
|| compact.includes('总是');
|
|
45
|
+
const containsAny = (words) => {
|
|
46
|
+
return words.some(word => compact === word || compact.includes(word));
|
|
47
|
+
};
|
|
48
|
+
const isDeny = compact === 'n'
|
|
49
|
+
|| compact === 'no'
|
|
50
|
+
|| compact === '否'
|
|
51
|
+
|| compact === '拒绝'
|
|
52
|
+
|| containsAny(['拒绝', '不同意', '不允许', 'deny']);
|
|
53
|
+
if (isDeny) {
|
|
54
|
+
return { allow: false, remember: false };
|
|
55
|
+
}
|
|
56
|
+
const isAllow = compact === 'y'
|
|
57
|
+
|| compact === 'yes'
|
|
58
|
+
|| compact === 'ok'
|
|
59
|
+
|| compact === 'always'
|
|
60
|
+
|| compact === '允许'
|
|
61
|
+
|| compact === '始终允许'
|
|
62
|
+
|| containsAny(['允许', '同意', '通过', '批准', 'allow']);
|
|
63
|
+
if (isAllow) {
|
|
64
|
+
return { allow: true, remember: hasAlways };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function parseDiscordCommand(text) {
|
|
69
|
+
const normalized = text.trim();
|
|
70
|
+
if (!normalized) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const commandPrefix = normalized.startsWith('///')
|
|
74
|
+
? '///'
|
|
75
|
+
: normalized.startsWith('/')
|
|
76
|
+
? '/'
|
|
77
|
+
: null;
|
|
78
|
+
if (!commandPrefix) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const body = normalized.slice(commandPrefix.length).trim();
|
|
82
|
+
if (!body) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const [name, ...rest] = body.split(/\s+/);
|
|
86
|
+
return {
|
|
87
|
+
name: name.toLowerCase(),
|
|
88
|
+
args: rest.join(' ').trim(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function parseConversationIdFromCustomId(prefix, customId) {
|
|
92
|
+
const expectedPrefix = `${prefix}:`;
|
|
93
|
+
if (!customId.startsWith(expectedPrefix)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const value = customId.slice(expectedPrefix.length).trim();
|
|
97
|
+
return value.length > 0 ? value : null;
|
|
98
|
+
}
|
|
99
|
+
function parseNaturalFileSendText(text) {
|
|
100
|
+
const matched = text.trim().match(/^发送文件\s+(.+)$/u);
|
|
101
|
+
if (!matched) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const value = matched[1].trim();
|
|
105
|
+
return value || null;
|
|
106
|
+
}
|
|
107
|
+
class DiscordHandler {
|
|
108
|
+
sender;
|
|
109
|
+
constructor(sender) {
|
|
110
|
+
this.sender = sender;
|
|
111
|
+
}
|
|
112
|
+
shortenId(value) {
|
|
113
|
+
if (!value) {
|
|
114
|
+
return '000000';
|
|
115
|
+
}
|
|
116
|
+
const normalized = value.replace(/[^a-zA-Z0-9]/g, '');
|
|
117
|
+
if (!normalized) {
|
|
118
|
+
return '000000';
|
|
119
|
+
}
|
|
120
|
+
return normalized.slice(0, 6);
|
|
121
|
+
}
|
|
122
|
+
shortenSessionId(value) {
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
if (!trimmed) {
|
|
125
|
+
return '000000';
|
|
126
|
+
}
|
|
127
|
+
const withoutPrefix = trimmed.replace(/^[a-zA-Z_\-]+/, '');
|
|
128
|
+
const normalized = (withoutPrefix || trimmed).replace(/[^a-zA-Z0-9]/g, '');
|
|
129
|
+
if (!normalized) {
|
|
130
|
+
return this.shortenId(trimmed);
|
|
131
|
+
}
|
|
132
|
+
return normalized.slice(0, 6).toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
buildChannelNameBySessionId(sessionId) {
|
|
135
|
+
return `opencode${this.shortenSessionId(sessionId)}`;
|
|
136
|
+
}
|
|
137
|
+
buildSessionTitleBySessionId(chatType, sessionId) {
|
|
138
|
+
const mode = chatType === 'p2p' ? '私聊' : '群聊';
|
|
139
|
+
return `Discord ${mode} ${this.shortenSessionId(sessionId)}`;
|
|
140
|
+
}
|
|
141
|
+
isUnknownInteractionError(error) {
|
|
142
|
+
if (!error || typeof error !== 'object') {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const record = error;
|
|
146
|
+
return Number(record.code) === 10062;
|
|
147
|
+
}
|
|
148
|
+
async safeInteractionReply(interaction, content, options) {
|
|
149
|
+
const replyPayload = {
|
|
150
|
+
content,
|
|
151
|
+
...(options?.components ? { components: options.components } : {}),
|
|
152
|
+
flags: MessageFlags.Ephemeral,
|
|
153
|
+
};
|
|
154
|
+
const editPayload = {
|
|
155
|
+
content,
|
|
156
|
+
...(options?.components ? { components: options.components } : {}),
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
if (interaction.deferred || interaction.replied) {
|
|
160
|
+
await interaction.editReply(editPayload);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await interaction.reply(replyPayload);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (this.isUnknownInteractionError(error) && interaction.channelId) {
|
|
168
|
+
await this.sender.sendText(interaction.channelId, content);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async deferInteractionReply(interaction) {
|
|
175
|
+
if (interaction.deferred || interaction.replied) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (this.isUnknownInteractionError(error)) {
|
|
184
|
+
if (interaction.channelId) {
|
|
185
|
+
await this.sender.sendText(interaction.channelId, '⚠️ 交互已过期,请重新执行命令。');
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
getRawDiscordMessage(event) {
|
|
193
|
+
if (!event.rawEvent || typeof event.rawEvent !== 'object') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const raw = event.rawEvent;
|
|
197
|
+
if (typeof raw.channelId !== 'string') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return raw;
|
|
201
|
+
}
|
|
202
|
+
buildDefaultSessionTitle(event, conversationIdOverride) {
|
|
203
|
+
const channelShort = this.shortenId(conversationIdOverride || event.conversationId);
|
|
204
|
+
if (event.chatType === 'p2p') {
|
|
205
|
+
const privateIdShort = this.shortenId(event.senderId);
|
|
206
|
+
return `Discord 私聊 ${privateIdShort} ${channelShort}`;
|
|
207
|
+
}
|
|
208
|
+
const rawMessage = this.getRawDiscordMessage(event);
|
|
209
|
+
const guildShort = this.shortenId(rawMessage?.guildId || undefined);
|
|
210
|
+
return `Discord 群聊 ${guildShort} ${channelShort}`;
|
|
211
|
+
}
|
|
212
|
+
buildDefaultSessionTitleFromInteraction(interaction, conversationIdOverride) {
|
|
213
|
+
const channelShort = this.shortenId(conversationIdOverride || interaction.channelId);
|
|
214
|
+
if (!interaction.guildId) {
|
|
215
|
+
return `Discord 私聊 ${this.shortenId(interaction.user.id)} ${channelShort}`;
|
|
216
|
+
}
|
|
217
|
+
return `Discord 群聊 ${this.shortenId(interaction.guildId)} ${channelShort}`;
|
|
218
|
+
}
|
|
219
|
+
getPermissionQueueKey(event) {
|
|
220
|
+
return `discord:${event.conversationId}`;
|
|
221
|
+
}
|
|
222
|
+
resolvePermissionDirectoryOptions(sessionId, conversationId) {
|
|
223
|
+
const currentSession = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
224
|
+
const conversation = chatSessionStore.getConversationBySessionId(sessionId);
|
|
225
|
+
const boundSession = conversation
|
|
226
|
+
? chatSessionStore.getSessionByConversation(conversation.platform, conversation.conversationId)
|
|
227
|
+
: undefined;
|
|
228
|
+
const directory = boundSession?.resolvedDirectory
|
|
229
|
+
|| currentSession?.resolvedDirectory
|
|
230
|
+
|| boundSession?.defaultDirectory
|
|
231
|
+
|| currentSession?.defaultDirectory;
|
|
232
|
+
const fallbackDirectories = Array.from(new Set([
|
|
233
|
+
boundSession?.resolvedDirectory,
|
|
234
|
+
boundSession?.defaultDirectory,
|
|
235
|
+
currentSession?.resolvedDirectory,
|
|
236
|
+
currentSession?.defaultDirectory,
|
|
237
|
+
...chatSessionStore.getKnownDirectories(),
|
|
238
|
+
].filter((value) => typeof value === 'string' && value.trim().length > 0)));
|
|
239
|
+
return {
|
|
240
|
+
...(directory ? { directory } : {}),
|
|
241
|
+
...(fallbackDirectories.length > 0 ? { fallbackDirectories } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
shouldSkipMessage(event, text) {
|
|
245
|
+
if (event.senderType === 'bot') {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (event.chatType === 'group' && groupConfig.requireMentionInGroup) {
|
|
249
|
+
if (!event.mentions || event.mentions.length === 0) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!text && (!event.attachments || event.attachments.length === 0)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
getQuestionBufferKey(conversationId) {
|
|
259
|
+
return `chat:discord:${conversationId}`;
|
|
260
|
+
}
|
|
261
|
+
touchQuestionBuffer(conversationId) {
|
|
262
|
+
const bufferKey = this.getQuestionBufferKey(conversationId);
|
|
263
|
+
if (outputBuffer.get(bufferKey)) {
|
|
264
|
+
outputBuffer.touch(bufferKey);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
getPendingQuestionByConversation(conversationId) {
|
|
268
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', conversationId);
|
|
269
|
+
if (!sessionId) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const pending = questionHandler.getBySession(sessionId);
|
|
273
|
+
if (!pending || pending.chatId !== conversationId) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return pending;
|
|
277
|
+
}
|
|
278
|
+
updateDraftAnswerFromParsed(pending, questionIndex, parsed, rawText) {
|
|
279
|
+
if (parsed.type === 'skip') {
|
|
280
|
+
questionHandler.setDraftAnswer(pending.request.id, questionIndex, []);
|
|
281
|
+
questionHandler.setDraftCustomAnswer(pending.request.id, questionIndex, '');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (parsed.type === 'custom') {
|
|
285
|
+
questionHandler.setDraftAnswer(pending.request.id, questionIndex, []);
|
|
286
|
+
questionHandler.setDraftCustomAnswer(pending.request.id, questionIndex, parsed.custom || rawText);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
questionHandler.setDraftCustomAnswer(pending.request.id, questionIndex, '');
|
|
290
|
+
questionHandler.setDraftAnswer(pending.request.id, questionIndex, parsed.values || []);
|
|
291
|
+
}
|
|
292
|
+
async submitPendingQuestion(pending, notify) {
|
|
293
|
+
const answers = [];
|
|
294
|
+
for (let index = 0; index < pending.request.questions.length; index++) {
|
|
295
|
+
const custom = (pending.draftCustomAnswers[index] || '').trim();
|
|
296
|
+
if (custom) {
|
|
297
|
+
answers.push([custom]);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
answers.push(pending.draftAnswers[index] || []);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const success = await opencodeClient.replyQuestion(pending.request.id, answers);
|
|
304
|
+
if (!success) {
|
|
305
|
+
await notify('⚠️ 回答提交失败,请稍后重试。');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
questionHandler.remove(pending.request.id);
|
|
309
|
+
this.touchQuestionBuffer(pending.chatId);
|
|
310
|
+
await notify('✅ 已提交问题回答,任务继续执行。');
|
|
311
|
+
}
|
|
312
|
+
async applyPendingQuestionAnswer(pending, parsed, rawText, notify) {
|
|
313
|
+
const questionCount = pending.request.questions.length;
|
|
314
|
+
if (questionCount === 0) {
|
|
315
|
+
await notify('当前问题状态异常,请稍后重试。');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const currentIndex = Math.min(Math.max(pending.currentQuestionIndex, 0), questionCount - 1);
|
|
319
|
+
this.updateDraftAnswerFromParsed(pending, currentIndex, parsed, rawText);
|
|
320
|
+
const nextIndex = currentIndex + 1;
|
|
321
|
+
if (nextIndex < questionCount) {
|
|
322
|
+
questionHandler.setCurrentQuestionIndex(pending.request.id, nextIndex);
|
|
323
|
+
this.touchQuestionBuffer(pending.chatId);
|
|
324
|
+
await notify(`✅ 已记录第 ${currentIndex + 1}/${questionCount} 题,请继续回答下一题。`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
await this.submitPendingQuestion(pending, notify);
|
|
328
|
+
}
|
|
329
|
+
async getOrCreateSession(event, titleOverride, directoryOverride) {
|
|
330
|
+
const existing = chatSessionStore.getSessionIdByConversation('discord', event.conversationId);
|
|
331
|
+
if (existing) {
|
|
332
|
+
return existing;
|
|
333
|
+
}
|
|
334
|
+
const isGroup = event.chatType === 'group';
|
|
335
|
+
const title = titleOverride?.trim() || this.buildDefaultSessionTitle(event);
|
|
336
|
+
const session = await opencodeClient.createSession(title, directoryOverride);
|
|
337
|
+
if (!session?.id) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const finalTitle = this.buildSessionTitleBySessionId(isGroup ? 'group' : 'p2p', session.id);
|
|
341
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
342
|
+
chatSessionStore.setSessionByConversation('discord', event.conversationId, session.id, event.senderId, finalTitle, {
|
|
343
|
+
chatType: isGroup ? 'group' : 'p2p',
|
|
344
|
+
resolvedDirectory: session.directory,
|
|
345
|
+
});
|
|
346
|
+
// 新创建会话,发送帮助和提醒消息
|
|
347
|
+
await this.safeReply(event, this.getDiscordHelpText());
|
|
348
|
+
await this.safeReply(event, '当前会话未与opencode绑定,已新建会话并绑定如需切换请按照help提示操作');
|
|
349
|
+
// 标记提醒已发送(失败不阻断主流程)
|
|
350
|
+
try {
|
|
351
|
+
chatSessionStore.markReminderSent('discord', event.conversationId);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// 忽略元数据写入失败
|
|
355
|
+
}
|
|
356
|
+
return session.id;
|
|
357
|
+
}
|
|
358
|
+
async bindSessionToConversation(conversationId, sessionId, userId, title, chatType = 'group', resolvedDirectory, options) {
|
|
359
|
+
chatSessionStore.setSessionByConversation('discord', conversationId, sessionId, userId, title, {
|
|
360
|
+
chatType,
|
|
361
|
+
...(resolvedDirectory ? { resolvedDirectory } : {}),
|
|
362
|
+
...(options?.protectSessionDelete ? { protectSessionDelete: true } : {}),
|
|
363
|
+
});
|
|
364
|
+
if (options && 'defaultDirectory' in options) {
|
|
365
|
+
chatSessionStore.updateConfigByConversation('discord', conversationId, {
|
|
366
|
+
defaultDirectory: options.defaultDirectory,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async safeReply(event, text) {
|
|
371
|
+
await this.sender.sendText(event.conversationId, text);
|
|
372
|
+
}
|
|
373
|
+
async safeReplyCard(event, card) {
|
|
374
|
+
await this.sender.sendCard(event.conversationId, card);
|
|
375
|
+
}
|
|
376
|
+
parseProviderModel(value) {
|
|
377
|
+
if (!value) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const trimmed = value.trim();
|
|
381
|
+
if (!trimmed) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const separator = trimmed.includes(':') ? ':' : (trimmed.includes('/') ? '/' : '');
|
|
385
|
+
if (!separator) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const splitIndex = trimmed.indexOf(separator);
|
|
389
|
+
const providerId = trimmed.slice(0, splitIndex).trim();
|
|
390
|
+
const modelId = trimmed.slice(splitIndex + 1).trim();
|
|
391
|
+
if (!providerId || !modelId) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return { providerId, modelId };
|
|
395
|
+
}
|
|
396
|
+
async resolveCompactModel(conversationId) {
|
|
397
|
+
const session = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
398
|
+
const preferred = this.parseProviderModel(session?.preferredModel);
|
|
399
|
+
if (preferred) {
|
|
400
|
+
return preferred;
|
|
401
|
+
}
|
|
402
|
+
if (modelConfig.defaultProvider && modelConfig.defaultModel) {
|
|
403
|
+
return {
|
|
404
|
+
providerId: modelConfig.defaultProvider,
|
|
405
|
+
modelId: modelConfig.defaultModel,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const providerPayload = await opencodeClient.getProviders();
|
|
409
|
+
const providers = Array.isArray(providerPayload.providers) ? providerPayload.providers : [];
|
|
410
|
+
for (const provider of providers) {
|
|
411
|
+
if (!provider || typeof provider !== 'object') {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const record = provider;
|
|
415
|
+
const providerId = typeof record.id === 'string' ? record.id.trim() : '';
|
|
416
|
+
if (!providerId) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const modelsRaw = record.models;
|
|
420
|
+
const modelList = Array.isArray(modelsRaw)
|
|
421
|
+
? modelsRaw
|
|
422
|
+
: (modelsRaw && typeof modelsRaw === 'object' ? Object.values(modelsRaw) : []);
|
|
423
|
+
for (const modelItem of modelList) {
|
|
424
|
+
if (!modelItem || typeof modelItem !== 'object') {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const modelRecord = modelItem;
|
|
428
|
+
const modelId = typeof modelRecord.id === 'string' ? modelRecord.id.trim() : '';
|
|
429
|
+
if (modelId) {
|
|
430
|
+
return { providerId, modelId };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
async buildAllModelOptions(limit = MAX_MODEL_OPTIONS) {
|
|
437
|
+
const providerPayload = await opencodeClient.getProviders().catch(() => ({ providers: [] }));
|
|
438
|
+
const providers = Array.isArray(providerPayload.providers) ? providerPayload.providers : [];
|
|
439
|
+
const options = [];
|
|
440
|
+
const seen = new Set();
|
|
441
|
+
for (const provider of providers) {
|
|
442
|
+
if (!provider || typeof provider !== 'object') {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const providerRecord = provider;
|
|
446
|
+
const providerId = typeof providerRecord.id === 'string' ? providerRecord.id.trim() : '';
|
|
447
|
+
const providerName = typeof providerRecord.name === 'string' ? providerRecord.name.trim() : providerId;
|
|
448
|
+
if (!providerId) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const modelsRaw = providerRecord.models;
|
|
452
|
+
const modelList = Array.isArray(modelsRaw)
|
|
453
|
+
? modelsRaw
|
|
454
|
+
: (modelsRaw && typeof modelsRaw === 'object' ? Object.values(modelsRaw) : []);
|
|
455
|
+
for (const modelItem of modelList) {
|
|
456
|
+
if (!modelItem || typeof modelItem !== 'object') {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const modelRecord = modelItem;
|
|
460
|
+
const modelId = typeof modelRecord.id === 'string'
|
|
461
|
+
? modelRecord.id.trim()
|
|
462
|
+
: (typeof modelRecord.modelID === 'string' ? modelRecord.modelID.trim() : '');
|
|
463
|
+
if (!modelId) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const value = `${providerId}:${modelId}`;
|
|
467
|
+
if (seen.has(value)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
seen.add(value);
|
|
471
|
+
const rawName = typeof modelRecord.name === 'string' && modelRecord.name.trim()
|
|
472
|
+
? modelRecord.name.trim()
|
|
473
|
+
: modelId;
|
|
474
|
+
options.push({
|
|
475
|
+
label: rawName.slice(0, 100),
|
|
476
|
+
value,
|
|
477
|
+
description: providerName.slice(0, 100),
|
|
478
|
+
});
|
|
479
|
+
if (options.length >= limit) {
|
|
480
|
+
return options;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return options;
|
|
485
|
+
}
|
|
486
|
+
async buildModelSelectOptions(page = 1) {
|
|
487
|
+
const allOptions = await this.buildAllModelOptions();
|
|
488
|
+
if (allOptions.length === 0) {
|
|
489
|
+
return {
|
|
490
|
+
options: [],
|
|
491
|
+
currentPage: 1,
|
|
492
|
+
totalPages: 1,
|
|
493
|
+
totalCount: 0,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const totalPages = Math.max(1, Math.ceil(allOptions.length / MODEL_PAGE_SIZE));
|
|
497
|
+
const currentPage = Math.min(Math.max(1, page), totalPages);
|
|
498
|
+
const start = (currentPage - 1) * MODEL_PAGE_SIZE;
|
|
499
|
+
const end = start + MODEL_PAGE_SIZE;
|
|
500
|
+
return {
|
|
501
|
+
options: allOptions.slice(start, end),
|
|
502
|
+
currentPage,
|
|
503
|
+
totalPages,
|
|
504
|
+
totalCount: allOptions.length,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async buildAgentSelectOptions(limit = 24) {
|
|
508
|
+
const agents = await opencodeClient.getAgents().catch(() => []);
|
|
509
|
+
const visible = agents.filter(agent => agent.hidden !== true && !['compaction', 'title', 'summary'].includes(agent.name));
|
|
510
|
+
const options = [];
|
|
511
|
+
options.push({
|
|
512
|
+
label: '默认角色',
|
|
513
|
+
value: 'none',
|
|
514
|
+
description: '跟随 OpenCode 默认角色',
|
|
515
|
+
});
|
|
516
|
+
for (const agent of visible.slice(0, limit)) {
|
|
517
|
+
const modePrefix = agent.mode === 'subagent' ? '子' : '主';
|
|
518
|
+
options.push({
|
|
519
|
+
label: `${modePrefix}:${agent.name}`.slice(0, 100),
|
|
520
|
+
value: agent.name,
|
|
521
|
+
description: (agent.description || agent.name).slice(0, 100),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return options.slice(0, 25);
|
|
525
|
+
}
|
|
526
|
+
buildEffortSelectOptions() {
|
|
527
|
+
const levels = ['none', ...KNOWN_EFFORT_LEVELS.filter(level => level !== 'none')];
|
|
528
|
+
return levels.map(level => ({
|
|
529
|
+
label: level === 'none' ? '默认(自动)' : level,
|
|
530
|
+
value: level,
|
|
531
|
+
description: level === 'none' ? '清除会话强度设置' : `设置为 ${level}`,
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
parseNewSessionArgs(args) {
|
|
535
|
+
const raw = args.trim();
|
|
536
|
+
if (!raw) {
|
|
537
|
+
return {};
|
|
538
|
+
}
|
|
539
|
+
const dirInlineMatch = raw.match(/^(.*?)(?:\s+--dir=(.+))$/u);
|
|
540
|
+
if (dirInlineMatch) {
|
|
541
|
+
const title = dirInlineMatch[1].trim();
|
|
542
|
+
const directoryInput = dirInlineMatch[2].trim();
|
|
543
|
+
return {
|
|
544
|
+
...(title ? { title } : {}),
|
|
545
|
+
...(directoryInput ? { directoryInput } : {}),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const dirSplitMatch = raw.match(/^(.*?)(?:\s+--dir\s+(.+))$/u);
|
|
549
|
+
if (dirSplitMatch) {
|
|
550
|
+
const title = dirSplitMatch[1].trim();
|
|
551
|
+
const directoryInput = dirSplitMatch[2].trim();
|
|
552
|
+
return {
|
|
553
|
+
...(title ? { title } : {}),
|
|
554
|
+
...(directoryInput ? { directoryInput } : {}),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return { title: raw };
|
|
558
|
+
}
|
|
559
|
+
resolveDirectoryInput(event, directoryInput) {
|
|
560
|
+
const current = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
561
|
+
const normalizedInput = directoryInput?.trim();
|
|
562
|
+
const explicitDirectory = normalizedInput && path.isAbsolute(normalizedInput)
|
|
563
|
+
? normalizedInput
|
|
564
|
+
: undefined;
|
|
565
|
+
const aliasName = normalizedInput && !path.isAbsolute(normalizedInput)
|
|
566
|
+
? normalizedInput
|
|
567
|
+
: undefined;
|
|
568
|
+
const result = DirectoryPolicy.resolve({
|
|
569
|
+
explicitDirectory,
|
|
570
|
+
aliasName,
|
|
571
|
+
chatDefaultDirectory: current?.defaultDirectory,
|
|
572
|
+
});
|
|
573
|
+
if (!result.ok) {
|
|
574
|
+
return { ok: false, message: result.userMessage };
|
|
575
|
+
}
|
|
576
|
+
const directory = result.source === 'server_default' ? undefined : result.directory;
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
...(directory ? { directory } : {}),
|
|
580
|
+
...(result.projectName ? { projectName: result.projectName } : {}),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async sendFileToDiscord(event, rawPath) {
|
|
584
|
+
const normalized = rawPath.trim();
|
|
585
|
+
if (!normalized) {
|
|
586
|
+
await this.safeReply(event, '用法:`///send <绝对路径>` 或 `发送文件 <绝对路径>`');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const resolvedPath = path.resolve(normalized);
|
|
590
|
+
const fileName = path.basename(resolvedPath);
|
|
591
|
+
const validation = validateFilePath(resolvedPath);
|
|
592
|
+
if (!validation.safe) {
|
|
593
|
+
await this.safeReply(event, `❌ 文件发送被拒绝: ${validation.reason || '路径不安全'}`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let stat;
|
|
597
|
+
try {
|
|
598
|
+
stat = await fsp.stat(resolvedPath);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
await this.safeReply(event, `❌ 文件不存在: ${fileName}`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!stat.isFile()) {
|
|
605
|
+
await this.safeReply(event, `❌ 路径不是文件: ${fileName}`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (stat.size === 0) {
|
|
609
|
+
await this.safeReply(event, '❌ 不允许发送空文件。');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (stat.size > DISCORD_FILE_MAX_SIZE) {
|
|
613
|
+
await this.safeReply(event, `❌ 文件过大(${(stat.size / (1024 * 1024)).toFixed(1)}MB),超过 25MB 限制。`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const rawMessage = this.getRawDiscordMessage(event);
|
|
617
|
+
if (!rawMessage || !rawMessage.channel || !('send' in rawMessage.channel)) {
|
|
618
|
+
await this.safeReply(event, '❌ 当前上下文不支持发送文件。');
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const channel = rawMessage.channel;
|
|
623
|
+
await channel.send({ files: [resolvedPath] });
|
|
624
|
+
await this.safeReply(event, `✅ 已发送文件: ${fileName}`);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
628
|
+
await this.safeReply(event, `❌ 发送失败: ${message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async handleSessionCommand(event) {
|
|
632
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', event.conversationId);
|
|
633
|
+
if (!sessionId) {
|
|
634
|
+
await this.safeReply(event, '当前频道尚未绑定会话,发送任意消息会自动创建会话。');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
await this.safeReply(event, `当前会话: ${sessionId}`);
|
|
638
|
+
}
|
|
639
|
+
async handleNewSessionCommand(event, rawArgs) {
|
|
640
|
+
const parsed = this.parseNewSessionArgs(rawArgs || '');
|
|
641
|
+
const title = parsed.title?.trim() || this.buildDefaultSessionTitle(event);
|
|
642
|
+
const directoryResolved = this.resolveDirectoryInput(event, parsed.directoryInput);
|
|
643
|
+
if (!directoryResolved.ok) {
|
|
644
|
+
await this.safeReply(event, directoryResolved.message);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const session = await opencodeClient.createSession(title, directoryResolved.directory);
|
|
648
|
+
if (!session?.id) {
|
|
649
|
+
await this.safeReply(event, '❌ 创建会话失败,请稍后重试。');
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const finalTitle = this.buildSessionTitleBySessionId(event.chatType === 'group' ? 'group' : 'p2p', session.id);
|
|
653
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
654
|
+
await this.bindSessionToConversation(event.conversationId, session.id, event.senderId, finalTitle, event.chatType === 'group' ? 'group' : 'p2p', session.directory, {
|
|
655
|
+
...(directoryResolved.directory ? { defaultDirectory: directoryResolved.directory } : {}),
|
|
656
|
+
});
|
|
657
|
+
await this.safeReply(event, `✅ 已创建并绑定新会话: ${session.id}`);
|
|
658
|
+
}
|
|
659
|
+
async handleUnbindCommand(event) {
|
|
660
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', event.conversationId);
|
|
661
|
+
if (!sessionId) {
|
|
662
|
+
await this.safeReply(event, '当前频道没有可解绑会话。');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
chatSessionStore.removeSessionByConversation('discord', event.conversationId);
|
|
666
|
+
await this.safeReply(event, `✅ 已解绑当前频道会话: ${sessionId}`);
|
|
667
|
+
}
|
|
668
|
+
async handleNewChannelCommand(event, rawArgs) {
|
|
669
|
+
const rawMessage = this.getRawDiscordMessage(event);
|
|
670
|
+
if (!rawMessage || !rawMessage.guild || event.chatType === 'p2p') {
|
|
671
|
+
await this.safeReply(event, '该命令仅支持在服务器频道中执行。');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const parsed = this.parseNewSessionArgs(rawArgs || '');
|
|
675
|
+
const initialTitle = parsed.title?.trim() || this.buildDefaultSessionTitle(event);
|
|
676
|
+
const directoryResolved = this.resolveDirectoryInput(event, parsed.directoryInput);
|
|
677
|
+
if (!directoryResolved.ok) {
|
|
678
|
+
await this.safeReply(event, directoryResolved.message);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const session = await opencodeClient.createSession(initialTitle, directoryResolved.directory);
|
|
682
|
+
if (!session?.id) {
|
|
683
|
+
await this.safeReply(event, '❌ 创建会话失败,请稍后重试。');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const guild = rawMessage.guild;
|
|
687
|
+
const parentId = 'parentId' in rawMessage.channel ? rawMessage.channel.parentId : null;
|
|
688
|
+
const botId = rawMessage.client.user?.id;
|
|
689
|
+
const channelName = this.buildChannelNameBySessionId(session.id);
|
|
690
|
+
try {
|
|
691
|
+
const channel = await guild.channels.create({
|
|
692
|
+
name: channelName,
|
|
693
|
+
type: ChannelType.GuildText,
|
|
694
|
+
...(parentId ? { parent: parentId } : {}),
|
|
695
|
+
permissionOverwrites: [
|
|
696
|
+
{
|
|
697
|
+
id: guild.roles.everyone.id,
|
|
698
|
+
deny: [PermissionFlagsBits.ViewChannel],
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
id: event.senderId,
|
|
702
|
+
allow: [
|
|
703
|
+
PermissionFlagsBits.ViewChannel,
|
|
704
|
+
PermissionFlagsBits.SendMessages,
|
|
705
|
+
PermissionFlagsBits.ReadMessageHistory,
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
...(botId
|
|
709
|
+
? [{
|
|
710
|
+
id: botId,
|
|
711
|
+
allow: [
|
|
712
|
+
PermissionFlagsBits.ViewChannel,
|
|
713
|
+
PermissionFlagsBits.SendMessages,
|
|
714
|
+
PermissionFlagsBits.ReadMessageHistory,
|
|
715
|
+
PermissionFlagsBits.ManageChannels,
|
|
716
|
+
],
|
|
717
|
+
}]
|
|
718
|
+
: []),
|
|
719
|
+
],
|
|
720
|
+
topic: `oc-session:${session.id}`,
|
|
721
|
+
});
|
|
722
|
+
const finalTitle = this.buildSessionTitleBySessionId('group', session.id);
|
|
723
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
724
|
+
await this.bindSessionToConversation(channel.id, session.id, event.senderId, finalTitle, 'group', session.directory, {
|
|
725
|
+
...(directoryResolved.directory ? { defaultDirectory: directoryResolved.directory } : {}),
|
|
726
|
+
});
|
|
727
|
+
await this.sender.sendText(channel.id, `✅ 已绑定 OpenCode 会话: ${session.id}`);
|
|
728
|
+
await this.safeReply(event, `✅ 已创建会话频道 <#${channel.id}> 并绑定会话: ${session.id}`);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
732
|
+
await this.bindSessionToConversation(event.conversationId, session.id, event.senderId, initialTitle, event.chatType === 'group' ? 'group' : 'p2p', session.directory, {
|
|
733
|
+
...(directoryResolved.directory ? { defaultDirectory: directoryResolved.directory } : {}),
|
|
734
|
+
});
|
|
735
|
+
await this.safeReply(event, `⚠️ 创建频道失败(需 Discord 管理频道/权限覆盖权限),已回退为当前频道绑定会话: ${session.id}\n${message}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async handleClearCommand(event) {
|
|
739
|
+
const current = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
740
|
+
if (!current?.sessionId) {
|
|
741
|
+
await this.safeReply(event, '当前频道没有活跃会话。');
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const shouldDeleteSession = current.protectSessionDelete !== true;
|
|
745
|
+
const deleted = shouldDeleteSession
|
|
746
|
+
? await opencodeClient.deleteSession(current.sessionId, current.resolvedDirectory ? { directory: current.resolvedDirectory } : undefined)
|
|
747
|
+
: true;
|
|
748
|
+
chatSessionStore.removeSessionByConversation('discord', event.conversationId);
|
|
749
|
+
const deletedChannel = await this.tryDeleteDedicatedChannel(event);
|
|
750
|
+
if (deleted) {
|
|
751
|
+
if (deletedChannel) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
await this.safeReply(event, shouldDeleteSession
|
|
755
|
+
? '🧹 已清理当前会话并解绑频道。'
|
|
756
|
+
: '🧹 已解绑当前频道(该会话为外部绑定,未删除 OpenCode 会话)。');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
await this.safeReply(event, '⚠️ 频道绑定已清理,但 OpenCode 会话删除失败,请稍后手动检查。');
|
|
760
|
+
}
|
|
761
|
+
async handleStopCommand(event) {
|
|
762
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', event.conversationId);
|
|
763
|
+
if (!sessionId) {
|
|
764
|
+
await this.safeReply(event, '当前频道没有活跃会话。');
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const aborted = await opencodeClient.abortSession(sessionId);
|
|
768
|
+
if (aborted) {
|
|
769
|
+
await this.safeReply(event, `✅ 已停止会话: ${sessionId}`);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
await this.safeReply(event, `⚠️ 停止失败,会话可能已结束。`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async tryDeleteDedicatedChannel(event) {
|
|
776
|
+
const rawMessage = this.getRawDiscordMessage(event);
|
|
777
|
+
if (!rawMessage || !rawMessage.guild || rawMessage.channel.type !== ChannelType.GuildText) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
if (!rawMessage.channel.topic?.startsWith('oc-session:')) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
await rawMessage.channel.delete('OpenCode 会话频道已清理');
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
console.warn('[Discord] 删除会话频道失败:', error);
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async handleBindCommand(event, sessionId) {
|
|
793
|
+
const normalized = sessionId.trim();
|
|
794
|
+
if (!normalized) {
|
|
795
|
+
await this.safeReply(event, '用法:`///bind <sessionId>`');
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const target = await opencodeClient.findSessionAcrossProjects(normalized);
|
|
799
|
+
if (!target) {
|
|
800
|
+
await this.safeReply(event, `❌ 未找到会话: ${normalized}`);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
await this.bindSessionToConversation(event.conversationId, target.id, event.senderId, target.title, event.chatType === 'group' ? 'group' : 'p2p', target.directory, {
|
|
804
|
+
protectSessionDelete: true,
|
|
805
|
+
...(target.directory ? { defaultDirectory: target.directory } : {}),
|
|
806
|
+
});
|
|
807
|
+
await this.safeReply(event, `✅ 已绑定会话: ${target.id}`);
|
|
808
|
+
}
|
|
809
|
+
async handleRenameCommand(event, title) {
|
|
810
|
+
const normalizedTitle = title.trim();
|
|
811
|
+
if (!normalizedTitle) {
|
|
812
|
+
await this.safeReply(event, '用法:`///rename <新会话名称>`');
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const current = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
816
|
+
if (!current?.sessionId) {
|
|
817
|
+
await this.safeReply(event, '当前频道尚未绑定会话。');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const updated = await opencodeClient.updateSession(current.sessionId, normalizedTitle);
|
|
821
|
+
if (!updated) {
|
|
822
|
+
await this.safeReply(event, '❌ 重命名失败,请稍后重试。');
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
await this.bindSessionToConversation(event.conversationId, current.sessionId, current.creatorId || event.senderId, normalizedTitle, current.chatType === 'p2p' ? 'p2p' : 'group', current.resolvedDirectory, {
|
|
826
|
+
...(current.protectSessionDelete ? { protectSessionDelete: true } : {}),
|
|
827
|
+
...(current.defaultDirectory ? { defaultDirectory: current.defaultDirectory } : {}),
|
|
828
|
+
});
|
|
829
|
+
await this.safeReply(event, `✅ 会话已重命名为:${normalizedTitle}`);
|
|
830
|
+
}
|
|
831
|
+
async handleSessionsCommand(event) {
|
|
832
|
+
const sessions = await opencodeClient.listSessionsAcrossProjects();
|
|
833
|
+
if (!sessions.length) {
|
|
834
|
+
await this.safeReply(event, '当前没有可绑定的历史会话。');
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const lines = sessions.slice(0, 8).map((session, index) => {
|
|
838
|
+
const title = session.title || '未命名会话';
|
|
839
|
+
const directory = session.directory || '-';
|
|
840
|
+
return `${index + 1}. ${title}\n ${session.id}\n 工作区: ${directory}`;
|
|
841
|
+
});
|
|
842
|
+
await this.safeReply(event, `可绑定会话(最近 8 条):\n${lines.join('\n')}`);
|
|
843
|
+
}
|
|
844
|
+
async handleWorkdirCommand(event, rawArgs) {
|
|
845
|
+
const input = rawArgs.trim();
|
|
846
|
+
const current = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
847
|
+
if (!input) {
|
|
848
|
+
const defaultDirectory = current?.defaultDirectory || '(未设置)';
|
|
849
|
+
const resolvedDirectory = current?.resolvedDirectory || '(跟随会话目录)';
|
|
850
|
+
await this.safeReply(event, `当前工作目录配置\n- 默认目录: ${defaultDirectory}\n- 会话目录: ${resolvedDirectory}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const normalized = input.toLowerCase();
|
|
854
|
+
if (normalized === 'clear' || normalized === 'default' || normalized === 'none') {
|
|
855
|
+
if (!current) {
|
|
856
|
+
await this.safeReply(event, '当前频道尚未绑定会话。');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
chatSessionStore.updateConfigByConversation('discord', event.conversationId, { defaultDirectory: undefined });
|
|
860
|
+
await this.safeReply(event, '✅ 已清除默认工作目录。');
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const resolved = this.resolveDirectoryInput(event, input);
|
|
864
|
+
if (!resolved.ok) {
|
|
865
|
+
await this.safeReply(event, resolved.message);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (current) {
|
|
869
|
+
chatSessionStore.updateConfigByConversation('discord', event.conversationId, {
|
|
870
|
+
defaultDirectory: resolved.directory,
|
|
871
|
+
});
|
|
872
|
+
await this.safeReply(event, `✅ 已设置默认工作目录: ${resolved.directory || '(跟随服务端默认)'}`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const title = this.buildDefaultSessionTitle(event);
|
|
876
|
+
const session = await opencodeClient.createSession(title, resolved.directory);
|
|
877
|
+
if (!session?.id) {
|
|
878
|
+
await this.safeReply(event, '❌ 创建会话失败,无法设置工作目录。');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const finalTitle = this.buildSessionTitleBySessionId(event.chatType === 'group' ? 'group' : 'p2p', session.id);
|
|
882
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
883
|
+
await this.bindSessionToConversation(event.conversationId, session.id, event.senderId, finalTitle, event.chatType === 'group' ? 'group' : 'p2p', session.directory, {
|
|
884
|
+
...(resolved.directory ? { defaultDirectory: resolved.directory } : {}),
|
|
885
|
+
});
|
|
886
|
+
await this.safeReply(event, `✅ 已创建并绑定会话,同时设置默认工作目录: ${resolved.directory || '(跟随服务端默认)'}`);
|
|
887
|
+
}
|
|
888
|
+
async handleUndoCommand(event) {
|
|
889
|
+
const result = await this.performUndo(event.conversationId);
|
|
890
|
+
await this.safeReply(event, result.message);
|
|
891
|
+
}
|
|
892
|
+
async performUndo(conversationId) {
|
|
893
|
+
const current = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
894
|
+
if (!current?.sessionId) {
|
|
895
|
+
return { ok: false, message: '当前频道没有活跃会话。' };
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
const messages = await opencodeClient.getSessionMessages(current.sessionId);
|
|
899
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
900
|
+
return { ok: false, message: '⚠️ 没有可回撤的消息。' };
|
|
901
|
+
}
|
|
902
|
+
const lastMessage = messages[messages.length - 1];
|
|
903
|
+
const prevMessage = messages.length >= 2 ? messages[messages.length - 2] : undefined;
|
|
904
|
+
const targetId = (() => {
|
|
905
|
+
const prevInfo = prevMessage?.info;
|
|
906
|
+
const lastInfo = lastMessage.info;
|
|
907
|
+
if (prevInfo && typeof prevInfo.id === 'string' && prevInfo.id.trim()) {
|
|
908
|
+
return prevInfo.id.trim();
|
|
909
|
+
}
|
|
910
|
+
if (typeof lastInfo.id === 'string' && lastInfo.id.trim()) {
|
|
911
|
+
return lastInfo.id.trim();
|
|
912
|
+
}
|
|
913
|
+
return '';
|
|
914
|
+
})();
|
|
915
|
+
if (!targetId) {
|
|
916
|
+
return { ok: false, message: '❌ 回撤失败: 未找到可回撤消息 ID。' };
|
|
917
|
+
}
|
|
918
|
+
const reverted = await opencodeClient.revertMessage(current.sessionId, targetId);
|
|
919
|
+
if (!reverted) {
|
|
920
|
+
return { ok: false, message: '❌ 回撤失败: OpenCode 回撤请求未生效。' };
|
|
921
|
+
}
|
|
922
|
+
return { ok: true, message: '✅ 已执行回撤。' };
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
926
|
+
return { ok: false, message: `❌ 回撤失败: ${message}` };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async handleCompactCommand(event) {
|
|
930
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', event.conversationId);
|
|
931
|
+
if (!sessionId) {
|
|
932
|
+
await this.safeReply(event, '当前频道没有活跃会话。');
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const model = await this.resolveCompactModel(event.conversationId);
|
|
936
|
+
if (!model) {
|
|
937
|
+
await this.safeReply(event, '❌ 未找到可用模型,无法执行上下文压缩。');
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const compacted = await opencodeClient.summarizeSession(sessionId, model.providerId, model.modelId);
|
|
941
|
+
if (!compacted) {
|
|
942
|
+
await this.safeReply(event, `❌ 上下文压缩失败(模型: ${model.providerId}:${model.modelId})`);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
await this.safeReply(event, `✅ 上下文压缩完成(模型: ${model.providerId}:${model.modelId})`);
|
|
946
|
+
}
|
|
947
|
+
sortEffortLevels(values) {
|
|
948
|
+
const order = new Map();
|
|
949
|
+
KNOWN_EFFORT_LEVELS.forEach((level, index) => {
|
|
950
|
+
order.set(level, index);
|
|
951
|
+
});
|
|
952
|
+
return [...values].sort((left, right) => {
|
|
953
|
+
const leftOrder = order.get(left) ?? Number.MAX_SAFE_INTEGER;
|
|
954
|
+
const rightOrder = order.get(right) ?? Number.MAX_SAFE_INTEGER;
|
|
955
|
+
if (leftOrder !== rightOrder) {
|
|
956
|
+
return leftOrder - rightOrder;
|
|
957
|
+
}
|
|
958
|
+
return left.localeCompare(right);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
parseModelEffortVariants(modelRecord) {
|
|
962
|
+
const variants = modelRecord.variants;
|
|
963
|
+
if (!variants || typeof variants !== 'object' || Array.isArray(variants)) {
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
const result = [];
|
|
967
|
+
for (const key of Object.keys(variants)) {
|
|
968
|
+
const normalized = normalizeEffortLevel(key);
|
|
969
|
+
if (!normalized || normalized === 'none' || result.includes(normalized)) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
result.push(normalized);
|
|
973
|
+
}
|
|
974
|
+
return this.sortEffortLevels(result);
|
|
975
|
+
}
|
|
976
|
+
async getEffortSupportInfo(conversationId) {
|
|
977
|
+
const model = await this.resolveCompactModel(conversationId);
|
|
978
|
+
if (!model) {
|
|
979
|
+
return {
|
|
980
|
+
modelLabel: '未知',
|
|
981
|
+
supportedEfforts: [],
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const providersPayload = await opencodeClient.getProviders();
|
|
985
|
+
const providers = Array.isArray(providersPayload.providers) ? providersPayload.providers : [];
|
|
986
|
+
const providerLower = model.providerId.toLowerCase();
|
|
987
|
+
const modelLower = model.modelId.toLowerCase();
|
|
988
|
+
for (const provider of providers) {
|
|
989
|
+
if (!provider || typeof provider !== 'object') {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
const providerRecord = provider;
|
|
993
|
+
const providerId = typeof providerRecord.id === 'string' ? providerRecord.id.trim() : '';
|
|
994
|
+
if (!providerId || providerId.toLowerCase() !== providerLower) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
const modelsRaw = providerRecord.models;
|
|
998
|
+
const modelList = Array.isArray(modelsRaw)
|
|
999
|
+
? modelsRaw
|
|
1000
|
+
: (modelsRaw && typeof modelsRaw === 'object' ? Object.values(modelsRaw) : []);
|
|
1001
|
+
for (const modelItem of modelList) {
|
|
1002
|
+
if (!modelItem || typeof modelItem !== 'object') {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const modelRecord = modelItem;
|
|
1006
|
+
const modelId = typeof modelRecord.id === 'string'
|
|
1007
|
+
? modelRecord.id.trim()
|
|
1008
|
+
: (typeof modelRecord.modelID === 'string' ? modelRecord.modelID.trim() : '');
|
|
1009
|
+
if (!modelId || modelId.toLowerCase() !== modelLower) {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
modelLabel: `${model.providerId}:${model.modelId}`,
|
|
1014
|
+
supportedEfforts: this.parseModelEffortVariants(modelRecord),
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
modelLabel: `${model.providerId}:${model.modelId}`,
|
|
1020
|
+
supportedEfforts: [],
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
async handleEffortCommand(event, rawArgs) {
|
|
1024
|
+
const currentSessionId = await this.getOrCreateSession(event);
|
|
1025
|
+
if (!currentSessionId) {
|
|
1026
|
+
await this.safeReply(event, '❌ 无法创建会话以设置强度。');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const current = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
1030
|
+
const currentEffort = current?.preferredEffort || '默认(自动)';
|
|
1031
|
+
const args = rawArgs.trim();
|
|
1032
|
+
if (!args) {
|
|
1033
|
+
await this.safeReply(event, [
|
|
1034
|
+
`当前强度: ${currentEffort}`,
|
|
1035
|
+
'用法: ///effort <档位|default>,例如 ///effort high',
|
|
1036
|
+
'临时覆盖: #xhigh 帮我深度分析这段代码',
|
|
1037
|
+
].join('\n'));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const normalized = args.toLowerCase();
|
|
1041
|
+
if (normalized === 'default' || normalized === 'clear' || normalized === 'none') {
|
|
1042
|
+
chatSessionStore.updateConfigByConversation('discord', event.conversationId, { preferredEffort: undefined });
|
|
1043
|
+
await this.safeReply(event, '✅ 已清除会话强度,恢复模型默认。');
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const level = normalizeEffortLevel(normalized);
|
|
1047
|
+
if (!level || level === 'none') {
|
|
1048
|
+
await this.safeReply(event, `❌ 不支持的强度: ${args}`);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const support = await this.getEffortSupportInfo(event.conversationId);
|
|
1052
|
+
if (support.supportedEfforts.length > 0 && !support.supportedEfforts.includes(level)) {
|
|
1053
|
+
await this.safeReply(event, `❌ 当前模型不支持强度 ${level}\n当前模型: ${support.modelLabel}\n可用强度: ${support.supportedEfforts.join(' / ')}`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
chatSessionStore.updateConfigByConversation('discord', event.conversationId, { preferredEffort: level });
|
|
1057
|
+
await this.safeReply(event, `✅ 已设置会话强度: ${level}`);
|
|
1058
|
+
}
|
|
1059
|
+
parseCreateChatArgs(rawArgs) {
|
|
1060
|
+
const normalized = rawArgs.trim();
|
|
1061
|
+
if (!normalized) {
|
|
1062
|
+
return { category: 'all', page: 1 };
|
|
1063
|
+
}
|
|
1064
|
+
const [first, second] = normalized.split(/\s+/, 2);
|
|
1065
|
+
const key = first.toLowerCase();
|
|
1066
|
+
if (key === 'session' || key === '会话') {
|
|
1067
|
+
return { category: 'session', page: 1 };
|
|
1068
|
+
}
|
|
1069
|
+
if (key === 'model' || key === '模型') {
|
|
1070
|
+
const page = Number(second);
|
|
1071
|
+
return {
|
|
1072
|
+
category: 'model',
|
|
1073
|
+
page: Number.isFinite(page) && page > 0 ? Math.floor(page) : 1,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (key === 'agent' || key === 'role' || key === '角色') {
|
|
1077
|
+
return { category: 'agent', page: 1 };
|
|
1078
|
+
}
|
|
1079
|
+
if (key === 'effort' || key === '强度') {
|
|
1080
|
+
return { category: 'effort', page: 1 };
|
|
1081
|
+
}
|
|
1082
|
+
return { category: 'all', page: 1 };
|
|
1083
|
+
}
|
|
1084
|
+
async buildPanelCard(event, category = 'all', modelPageIndex = 1) {
|
|
1085
|
+
const currentSessionId = chatSessionStore.getSessionIdByConversation('discord', event.conversationId);
|
|
1086
|
+
const session = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
1087
|
+
const components = [];
|
|
1088
|
+
if (category === 'all' || category === 'session') {
|
|
1089
|
+
components.push({
|
|
1090
|
+
type: 'select',
|
|
1091
|
+
customId: `${PANEL_SELECT_PREFIX}:${event.conversationId}`,
|
|
1092
|
+
placeholder: '选择会话操作',
|
|
1093
|
+
options: [
|
|
1094
|
+
{ label: '查看当前会话', value: 'status', description: '显示当前频道绑定状态' },
|
|
1095
|
+
{ label: '创建并绑定新会话', value: 'new', description: '创建一个新 OpenCode 会话并绑定' },
|
|
1096
|
+
{ label: '创建会话频道', value: 'new_channel', description: '新建频道并绑定新会话' },
|
|
1097
|
+
{ label: '绑定已有会话', value: 'bind', description: '从历史会话中选择绑定' },
|
|
1098
|
+
{ label: '重命名当前会话', value: 'rename', description: '弹出输入框修改会话名' },
|
|
1099
|
+
{ label: '回撤上一轮', value: 'undo', description: '执行 undo 回撤上一轮' },
|
|
1100
|
+
{ label: '压缩上下文', value: 'compact', description: '执行 compact 压缩当前会话' },
|
|
1101
|
+
{ label: '清理并解绑会话', value: 'clear', description: '删除当前会话并解绑频道' },
|
|
1102
|
+
{ label: '命令帮助', value: 'help', description: '查看 Discord 命令速查' },
|
|
1103
|
+
],
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
const modelPage = await this.buildModelSelectOptions(modelPageIndex);
|
|
1107
|
+
if ((category === 'all' || category === 'model') && modelPage.options.length > 0) {
|
|
1108
|
+
components.push({
|
|
1109
|
+
type: 'select',
|
|
1110
|
+
customId: `${MODEL_SELECT_PREFIX}:${event.conversationId}`,
|
|
1111
|
+
placeholder: '选择模型',
|
|
1112
|
+
options: [
|
|
1113
|
+
{ label: '默认模型', value: 'none', description: '跟随 OpenCode 默认模型' },
|
|
1114
|
+
...modelPage.options,
|
|
1115
|
+
],
|
|
1116
|
+
minValues: 1,
|
|
1117
|
+
maxValues: 1,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (category === 'all' || category === 'agent') {
|
|
1121
|
+
const agentOptions = await this.buildAgentSelectOptions();
|
|
1122
|
+
if (agentOptions.length > 0) {
|
|
1123
|
+
components.push({
|
|
1124
|
+
type: 'select',
|
|
1125
|
+
customId: `${AGENT_SELECT_PREFIX}:${event.conversationId}`,
|
|
1126
|
+
placeholder: '选择角色',
|
|
1127
|
+
options: agentOptions,
|
|
1128
|
+
minValues: 1,
|
|
1129
|
+
maxValues: 1,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const scopeLabel = category === 'all' ? '综合' : category;
|
|
1134
|
+
const modelPagerLine = modelPage.totalCount > 0
|
|
1135
|
+
? `模型分页: ${modelPage.currentPage}/${modelPage.totalPages}(共 ${modelPage.totalCount} 条,使用 ///create_chat model <页码>)`
|
|
1136
|
+
: '模型分页: 暂无可用模型';
|
|
1137
|
+
return {
|
|
1138
|
+
discordText: [
|
|
1139
|
+
`🎛️ Discord 会话控制面板(${scopeLabel})`,
|
|
1140
|
+
`当前会话: ${currentSessionId || '未绑定'}`,
|
|
1141
|
+
`模型: ${session?.preferredModel || '默认'}`,
|
|
1142
|
+
`角色: ${session?.preferredAgent || '默认'}`,
|
|
1143
|
+
`强度: ${session?.preferredEffort || '默认'}`,
|
|
1144
|
+
'强度命令: ///effort <档位> / ///effort default',
|
|
1145
|
+
modelPagerLine,
|
|
1146
|
+
].join('\n'),
|
|
1147
|
+
discordComponents: components,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async handlePanelCommand(event, rawArgs) {
|
|
1151
|
+
const parsed = this.parseCreateChatArgs(rawArgs);
|
|
1152
|
+
const card = await this.buildPanelCard(event, parsed.category, parsed.page);
|
|
1153
|
+
await this.safeReplyCard(event, card);
|
|
1154
|
+
}
|
|
1155
|
+
getDiscordHelpText() {
|
|
1156
|
+
const cronHelpBlock = buildCronHelpText('discord');
|
|
1157
|
+
return [
|
|
1158
|
+
'Discord 命令速查(推荐 `///` 前缀):',
|
|
1159
|
+
'- `///session`: 查看当前频道会话',
|
|
1160
|
+
'- `///new [名称] [--dir 路径|别名]`: 新建并绑定会话',
|
|
1161
|
+
'- `///new-channel [名称] [--dir 路径|别名]`: 新建会话频道并绑定',
|
|
1162
|
+
'- `///bind <sessionId>`: 绑定已有会话',
|
|
1163
|
+
'- `///unbind`: 仅解绑当前频道会话',
|
|
1164
|
+
'- `///rename <新名称>`: 重命名当前会话',
|
|
1165
|
+
'- `///sessions`: 查看最近历史会话',
|
|
1166
|
+
'- `///effort`: 查看当前强度',
|
|
1167
|
+
'- `///effort <档位>`: 设置会话默认强度(按当前模型能力校验)',
|
|
1168
|
+
'- `///effort default`: 清除强度恢复默认',
|
|
1169
|
+
'- `#xhigh 你的问题`: 仅当前消息临时覆盖强度',
|
|
1170
|
+
'- `///workdir [路径|别名|clear]`: 设置/查看默认工作目录',
|
|
1171
|
+
'- `///undo`: 回撤上一轮',
|
|
1172
|
+
'- `///compact` 或 `///compat`: 压缩上下文',
|
|
1173
|
+
'- `///send <绝对路径>`: 发送白名单文件到当前频道',
|
|
1174
|
+
'- `///restart opencode`: 重启本地 OpenCode 进程(仅 loopback)',
|
|
1175
|
+
'- `///stop`: 停止当前频道会话',
|
|
1176
|
+
'- `///clear`: 清理并解绑当前会话',
|
|
1177
|
+
'- `///create_chat`: 打开会话控制面板',
|
|
1178
|
+
'- `///create_chat model <页码>`: 打开模型分页下拉(最多 500 条)',
|
|
1179
|
+
'- `///create_chat session|agent|effort`: 打开分类控制面板',
|
|
1180
|
+
'',
|
|
1181
|
+
cronHelpBlock,
|
|
1182
|
+
].join('\n');
|
|
1183
|
+
}
|
|
1184
|
+
async handleCronIntent(event, intent) {
|
|
1185
|
+
const manager = getRuntimeCronManager();
|
|
1186
|
+
const session = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
1187
|
+
const resolvedIntent = await resolveCronIntentForExecution({
|
|
1188
|
+
source: intent.source,
|
|
1189
|
+
action: intent.action,
|
|
1190
|
+
argsText: intent.argsText,
|
|
1191
|
+
semanticParser: async (argsText, source, actionHint) => {
|
|
1192
|
+
return await parseCronIntentWithOpenCode({
|
|
1193
|
+
argsText,
|
|
1194
|
+
source,
|
|
1195
|
+
actionHint,
|
|
1196
|
+
directory: session?.resolvedDirectory || session?.defaultDirectory,
|
|
1197
|
+
});
|
|
1198
|
+
},
|
|
1199
|
+
});
|
|
1200
|
+
const resultText = executeCronIntent({
|
|
1201
|
+
manager,
|
|
1202
|
+
intent: resolvedIntent,
|
|
1203
|
+
currentSessionId: session?.sessionId,
|
|
1204
|
+
currentDirectory: session?.resolvedDirectory || session?.defaultDirectory,
|
|
1205
|
+
currentConversationId: event.conversationId,
|
|
1206
|
+
creatorId: event.senderId,
|
|
1207
|
+
platform: 'discord',
|
|
1208
|
+
});
|
|
1209
|
+
await this.safeReply(event, resultText);
|
|
1210
|
+
}
|
|
1211
|
+
async handleCommand(event, command) {
|
|
1212
|
+
if (command.name === 'help') {
|
|
1213
|
+
await this.safeReply(event, this.getDiscordHelpText());
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
if (command.name === 'session' || command.name === 'status') {
|
|
1217
|
+
await this.handleSessionCommand(event);
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
if (command.name === 'cron') {
|
|
1221
|
+
const intent = parseCronSlashIntent(command.args);
|
|
1222
|
+
await this.handleCronIntent(event, intent);
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
if (command.name === 'new' || command.name === 'new-session') {
|
|
1226
|
+
await this.handleNewSessionCommand(event, command.args);
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
if (command.name === 'new-channel' || command.name === 'channel') {
|
|
1230
|
+
await this.handleNewChannelCommand(event, command.args);
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
if (command.name === 'bind') {
|
|
1234
|
+
await this.handleBindCommand(event, command.args);
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
if (command.name === 'rename') {
|
|
1238
|
+
await this.handleRenameCommand(event, command.args);
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
if (command.name === 'clear') {
|
|
1242
|
+
await this.handleClearCommand(event);
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
if (command.name === 'unbind') {
|
|
1246
|
+
await this.handleUnbindCommand(event);
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
if (command.name === 'create_chat' || command.name === 'panel') {
|
|
1250
|
+
await this.handlePanelCommand(event, command.args);
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
if (command.name === 'sessions') {
|
|
1254
|
+
await this.handleSessionsCommand(event);
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
if (command.name === 'effort') {
|
|
1258
|
+
await this.handleEffortCommand(event, command.args);
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
if (command.name === 'workdir') {
|
|
1262
|
+
await this.handleWorkdirCommand(event, command.args);
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
if (command.name === 'stop') {
|
|
1266
|
+
await this.handleStopCommand(event);
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
if (command.name === 'restart') {
|
|
1270
|
+
const target = command.args.trim().toLowerCase();
|
|
1271
|
+
if (target !== 'opencode') {
|
|
1272
|
+
await this.safeReply(event, '用法:`///restart opencode`');
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
await this.safeReply(event, '🔄 正在重启 OpenCode,请稍候...');
|
|
1276
|
+
const result = await restartOpenCodeProcess();
|
|
1277
|
+
await this.safeReply(event, formatRestartResultText(result));
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
if (command.name === 'undo') {
|
|
1281
|
+
await this.handleUndoCommand(event);
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
if (command.name === 'compact' || command.name === 'compat') {
|
|
1285
|
+
await this.handleCompactCommand(event);
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
if (command.name === 'send') {
|
|
1289
|
+
await this.sendFileToDiscord(event, command.args);
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
async handlePrompt(event, text) {
|
|
1295
|
+
const effortParsed = stripPromptEffortPrefix(text);
|
|
1296
|
+
const promptText = effortParsed.text.trim() || text.trim() || '请根据我发送的内容继续处理。';
|
|
1297
|
+
const sessionId = await this.getOrCreateSession(event);
|
|
1298
|
+
if (!sessionId) {
|
|
1299
|
+
await this.safeReply(event, '❌ 无法创建 OpenCode 会话,请检查服务状态。');
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const session = chatSessionStore.getSessionByConversation('discord', event.conversationId);
|
|
1303
|
+
const preferredModel = this.parseProviderModel(session?.preferredModel);
|
|
1304
|
+
const variant = effortParsed.effort || session?.preferredEffort;
|
|
1305
|
+
const pendingMessageId = await this.safePending(event);
|
|
1306
|
+
try {
|
|
1307
|
+
const response = await opencodeClient.sendMessage(sessionId, promptText, {
|
|
1308
|
+
...(preferredModel ? { providerId: preferredModel.providerId, modelId: preferredModel.modelId } : {}),
|
|
1309
|
+
...(session?.preferredAgent ? { agent: session.preferredAgent } : {}),
|
|
1310
|
+
...(variant ? { variant } : {}),
|
|
1311
|
+
...((session?.resolvedDirectory || session?.defaultDirectory)
|
|
1312
|
+
? { directory: session.resolvedDirectory || session.defaultDirectory }
|
|
1313
|
+
: {}),
|
|
1314
|
+
});
|
|
1315
|
+
if (pendingMessageId) {
|
|
1316
|
+
await this.sender.deleteMessage(pendingMessageId);
|
|
1317
|
+
}
|
|
1318
|
+
const parts = Array.isArray(response.parts) ? response.parts : [];
|
|
1319
|
+
if (parts.length === 0) {
|
|
1320
|
+
await this.safeReply(event, '-----------\n✅ 已提交请求,等待模型返回结果...');
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
if (pendingMessageId) {
|
|
1325
|
+
await this.sender.deleteMessage(pendingMessageId);
|
|
1326
|
+
}
|
|
1327
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1328
|
+
await this.safeReply(event, `❌ 请求失败: ${message}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async safePending(event) {
|
|
1332
|
+
return await this.sender.sendText(event.conversationId, '⏳ 正在处理,请稍候...');
|
|
1333
|
+
}
|
|
1334
|
+
async tryHandlePendingPermission(event, text) {
|
|
1335
|
+
const queueKey = this.getPermissionQueueKey(event);
|
|
1336
|
+
const pending = permissionHandler.peekForChat(queueKey);
|
|
1337
|
+
if (!pending) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
const decision = parsePermissionDecision(text);
|
|
1341
|
+
if (!decision) {
|
|
1342
|
+
await this.safeReply(event, `⚠️ 有待处理的权限请求:
|
|
1343
|
+
- 工具:${pending.tool}
|
|
1344
|
+
- 说明:${pending.description}
|
|
1345
|
+
${pending.risk ? `- 风险:${pending.risk}` : ''}
|
|
1346
|
+
|
|
1347
|
+
请回复"允许"或"拒绝"来确认权限。`);
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
const responded = await opencodeClient.respondToPermission(pending.sessionId, pending.permissionId, decision.allow, decision.remember, this.resolvePermissionDirectoryOptions(pending.sessionId, event.conversationId));
|
|
1351
|
+
if (!responded) {
|
|
1352
|
+
await this.safeReply(event, '权限响应失败,请稍后重试。');
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
permissionHandler.resolveForChat(queueKey, pending.permissionId);
|
|
1356
|
+
await this.safeReply(event, decision.allow
|
|
1357
|
+
? (decision.remember ? '✅ 已允许并记住该权限' : '✅ 已允许该权限')
|
|
1358
|
+
: '❌ 已拒绝该权限');
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
async tryHandlePendingQuestion(event, text) {
|
|
1362
|
+
const pending = this.getPendingQuestionByConversation(event.conversationId);
|
|
1363
|
+
if (!pending) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
const questionCount = pending.request.questions.length;
|
|
1367
|
+
if (questionCount === 0) {
|
|
1368
|
+
await this.safeReply(event, '当前问题状态异常,请稍后重试。');
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
const currentIndex = Math.min(Math.max(pending.currentQuestionIndex, 0), questionCount - 1);
|
|
1372
|
+
const question = pending.request.questions[currentIndex];
|
|
1373
|
+
const parsed = parseQuestionAnswerText(text, question);
|
|
1374
|
+
if (!parsed) {
|
|
1375
|
+
await this.safeReply(event, '当前有待回答问题,请回复选项内容/编号,或直接输入自定义答案。');
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
await this.applyPendingQuestionAnswer(pending, parsed, text, async (message) => {
|
|
1379
|
+
await this.safeReply(event, message);
|
|
1380
|
+
});
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
async buildBindOptions() {
|
|
1384
|
+
const sessions = await opencodeClient.listSessionsAcrossProjects();
|
|
1385
|
+
const options = [];
|
|
1386
|
+
for (const session of sessions.slice(0, MAX_SESSION_OPTIONS)) {
|
|
1387
|
+
const title = (session.title || '未命名会话').slice(0, 100);
|
|
1388
|
+
const workspace = (session.directory || '-').slice(0, 72);
|
|
1389
|
+
const description = `工作区: ${workspace}`.slice(0, 100);
|
|
1390
|
+
const label = `${title} · ${session.id.slice(0, 8)}`.slice(0, 100);
|
|
1391
|
+
options.push(new StringSelectMenuOptionBuilder()
|
|
1392
|
+
.setLabel(label)
|
|
1393
|
+
.setValue(session.id.slice(0, 100))
|
|
1394
|
+
.setDescription(description));
|
|
1395
|
+
}
|
|
1396
|
+
return options;
|
|
1397
|
+
}
|
|
1398
|
+
async handlePanelSelect(interaction) {
|
|
1399
|
+
const conversationId = parseConversationIdFromCustomId(PANEL_SELECT_PREFIX, interaction.customId);
|
|
1400
|
+
if (!conversationId || interaction.channelId !== conversationId) {
|
|
1401
|
+
await this.safeInteractionReply(interaction, '会话上下文不匹配,请重新打开面板。');
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const selected = interaction.values[0];
|
|
1405
|
+
if (selected !== 'rename') {
|
|
1406
|
+
const deferred = await this.deferInteractionReply(interaction);
|
|
1407
|
+
if (!deferred) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (selected === 'status') {
|
|
1412
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', conversationId);
|
|
1413
|
+
await this.safeInteractionReply(interaction, sessionId ? `当前频道会话: ${sessionId}` : '当前频道未绑定会话。');
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (selected === 'new') {
|
|
1417
|
+
const defaultTitle = this.buildDefaultSessionTitleFromInteraction(interaction, conversationId);
|
|
1418
|
+
const currentBinding = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
1419
|
+
const session = await opencodeClient.createSession(defaultTitle, currentBinding?.defaultDirectory);
|
|
1420
|
+
if (!session?.id) {
|
|
1421
|
+
await this.safeInteractionReply(interaction, '创建会话失败,请稍后重试。');
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const chatType = interaction.guildId ? 'group' : 'p2p';
|
|
1425
|
+
const finalTitle = this.buildSessionTitleBySessionId(chatType, session.id);
|
|
1426
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
1427
|
+
await this.bindSessionToConversation(conversationId, session.id, interaction.user.id, finalTitle, chatType, session.directory, {
|
|
1428
|
+
...(currentBinding?.defaultDirectory ? { defaultDirectory: currentBinding.defaultDirectory } : {}),
|
|
1429
|
+
});
|
|
1430
|
+
await this.safeInteractionReply(interaction, `已创建并绑定会话: ${session.id}`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (selected === 'bind') {
|
|
1434
|
+
const options = await this.buildBindOptions();
|
|
1435
|
+
if (options.length === 0) {
|
|
1436
|
+
await this.safeInteractionReply(interaction, '没有可绑定的历史会话。');
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const select = new StringSelectMenuBuilder()
|
|
1440
|
+
.setCustomId(`${BIND_SELECT_PREFIX}:${conversationId}`)
|
|
1441
|
+
.setPlaceholder('选择要绑定的会话')
|
|
1442
|
+
.addOptions(options)
|
|
1443
|
+
.setMinValues(1)
|
|
1444
|
+
.setMaxValues(1);
|
|
1445
|
+
const row = new ActionRowBuilder().addComponents(select);
|
|
1446
|
+
await this.safeInteractionReply(interaction, '请选择要绑定的会话:', { components: [row] });
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (selected === 'model') {
|
|
1450
|
+
const modelPage = await this.buildModelSelectOptions(1);
|
|
1451
|
+
if (modelPage.options.length === 0) {
|
|
1452
|
+
await this.safeInteractionReply(interaction, '当前无可用模型可切换。');
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const select = new StringSelectMenuBuilder()
|
|
1456
|
+
.setCustomId(`${MODEL_SELECT_PREFIX}:${conversationId}`)
|
|
1457
|
+
.setPlaceholder('选择模型')
|
|
1458
|
+
.addOptions([
|
|
1459
|
+
new StringSelectMenuOptionBuilder().setLabel('默认模型').setValue('none').setDescription('跟随 OpenCode 默认'),
|
|
1460
|
+
...modelPage.options.map(option => new StringSelectMenuOptionBuilder()
|
|
1461
|
+
.setLabel(option.label.slice(0, 100))
|
|
1462
|
+
.setValue(option.value.slice(0, 100))
|
|
1463
|
+
.setDescription((option.description || option.value).slice(0, 100))),
|
|
1464
|
+
])
|
|
1465
|
+
.setMinValues(1)
|
|
1466
|
+
.setMaxValues(1);
|
|
1467
|
+
const row = new ActionRowBuilder().addComponents(select);
|
|
1468
|
+
const text = modelPage.totalPages > 1
|
|
1469
|
+
? `请选择目标模型(第 ${modelPage.currentPage}/${modelPage.totalPages} 页,共 ${modelPage.totalCount} 条;更多请用 ///create_chat model <页码>)`
|
|
1470
|
+
: '请选择目标模型:';
|
|
1471
|
+
await this.safeInteractionReply(interaction, text, { components: [row] });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (selected === 'agent') {
|
|
1475
|
+
const agentOptions = await this.buildAgentSelectOptions();
|
|
1476
|
+
const select = new StringSelectMenuBuilder()
|
|
1477
|
+
.setCustomId(`${AGENT_SELECT_PREFIX}:${conversationId}`)
|
|
1478
|
+
.setPlaceholder('选择角色')
|
|
1479
|
+
.addOptions(agentOptions.map(option => new StringSelectMenuOptionBuilder()
|
|
1480
|
+
.setLabel(option.label.slice(0, 100))
|
|
1481
|
+
.setValue(option.value.slice(0, 100))
|
|
1482
|
+
.setDescription((option.description || option.value).slice(0, 100))))
|
|
1483
|
+
.setMinValues(1)
|
|
1484
|
+
.setMaxValues(1);
|
|
1485
|
+
const row = new ActionRowBuilder().addComponents(select);
|
|
1486
|
+
await this.safeInteractionReply(interaction, '请选择目标角色:', { components: [row] });
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (selected === 'effort') {
|
|
1490
|
+
await this.safeInteractionReply(interaction, '强度请使用命令行设置:///effort <档位>,例如 ///effort high');
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (selected === 'undo') {
|
|
1494
|
+
const result = await this.performUndo(conversationId);
|
|
1495
|
+
await this.safeInteractionReply(interaction, result.message);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (selected === 'compact') {
|
|
1499
|
+
const sessionId = chatSessionStore.getSessionIdByConversation('discord', conversationId);
|
|
1500
|
+
if (!sessionId) {
|
|
1501
|
+
await this.safeInteractionReply(interaction, '当前频道没有活跃会话。');
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const model = await this.resolveCompactModel(conversationId);
|
|
1505
|
+
if (!model) {
|
|
1506
|
+
await this.safeInteractionReply(interaction, '❌ 未找到可用模型,无法执行上下文压缩。');
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const compacted = await opencodeClient.summarizeSession(sessionId, model.providerId, model.modelId);
|
|
1510
|
+
await this.safeInteractionReply(interaction, compacted
|
|
1511
|
+
? `✅ 上下文压缩完成(模型: ${model.providerId}:${model.modelId})`
|
|
1512
|
+
: `❌ 上下文压缩失败(模型: ${model.providerId}:${model.modelId})`);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if (selected === 'new_channel') {
|
|
1516
|
+
const currentBinding = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
1517
|
+
const session = await opencodeClient.createSession(this.buildDefaultSessionTitleFromInteraction(interaction, conversationId), currentBinding?.defaultDirectory);
|
|
1518
|
+
if (!session?.id) {
|
|
1519
|
+
await this.safeInteractionReply(interaction, '创建会话失败,请稍后重试。');
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const channelName = this.buildChannelNameBySessionId(session.id);
|
|
1523
|
+
if (!interaction.guild) {
|
|
1524
|
+
const finalTitle = this.buildSessionTitleBySessionId('p2p', session.id);
|
|
1525
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
1526
|
+
await this.bindSessionToConversation(conversationId, session.id, interaction.user.id, finalTitle, 'p2p', session.directory, {
|
|
1527
|
+
...(currentBinding?.defaultDirectory ? { defaultDirectory: currentBinding.defaultDirectory } : {}),
|
|
1528
|
+
});
|
|
1529
|
+
await this.safeInteractionReply(interaction, `当前不在服务器中,已改为绑定当前会话: ${session.id}`);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const botId = interaction.client.user?.id;
|
|
1533
|
+
try {
|
|
1534
|
+
const channel = await interaction.guild.channels.create({
|
|
1535
|
+
name: channelName,
|
|
1536
|
+
type: ChannelType.GuildText,
|
|
1537
|
+
permissionOverwrites: [
|
|
1538
|
+
{
|
|
1539
|
+
id: interaction.guild.roles.everyone.id,
|
|
1540
|
+
deny: [PermissionFlagsBits.ViewChannel],
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
id: interaction.user.id,
|
|
1544
|
+
allow: [
|
|
1545
|
+
PermissionFlagsBits.ViewChannel,
|
|
1546
|
+
PermissionFlagsBits.SendMessages,
|
|
1547
|
+
PermissionFlagsBits.ReadMessageHistory,
|
|
1548
|
+
],
|
|
1549
|
+
},
|
|
1550
|
+
...(botId
|
|
1551
|
+
? [{
|
|
1552
|
+
id: botId,
|
|
1553
|
+
allow: [
|
|
1554
|
+
PermissionFlagsBits.ViewChannel,
|
|
1555
|
+
PermissionFlagsBits.SendMessages,
|
|
1556
|
+
PermissionFlagsBits.ReadMessageHistory,
|
|
1557
|
+
PermissionFlagsBits.ManageChannels,
|
|
1558
|
+
],
|
|
1559
|
+
}]
|
|
1560
|
+
: []),
|
|
1561
|
+
],
|
|
1562
|
+
topic: `oc-session:${session.id}`,
|
|
1563
|
+
});
|
|
1564
|
+
const finalTitle = this.buildSessionTitleBySessionId('group', session.id);
|
|
1565
|
+
await opencodeClient.updateSession(session.id, finalTitle).catch(() => false);
|
|
1566
|
+
await this.bindSessionToConversation(channel.id, session.id, interaction.user.id, finalTitle, 'group', session.directory, {
|
|
1567
|
+
...(currentBinding?.defaultDirectory ? { defaultDirectory: currentBinding.defaultDirectory } : {}),
|
|
1568
|
+
});
|
|
1569
|
+
await this.sender.sendText(channel.id, `✅ 已绑定 OpenCode 会话: ${session.id}`);
|
|
1570
|
+
await this.safeInteractionReply(interaction, `已创建会话频道 <#${channel.id}> 并绑定会话。`);
|
|
1571
|
+
}
|
|
1572
|
+
catch (error) {
|
|
1573
|
+
await this.bindSessionToConversation(conversationId, session.id, interaction.user.id, this.buildSessionTitleBySessionId('group', session.id), 'group', session.directory, {
|
|
1574
|
+
...(currentBinding?.defaultDirectory ? { defaultDirectory: currentBinding.defaultDirectory } : {}),
|
|
1575
|
+
});
|
|
1576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1577
|
+
await this.safeInteractionReply(interaction, `创建频道失败,已回退为当前频道绑定会话: ${session.id}\n${message}`);
|
|
1578
|
+
}
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (selected === 'rename') {
|
|
1582
|
+
const modal = new ModalBuilder()
|
|
1583
|
+
.setCustomId(`${RENAME_MODAL_PREFIX}:${conversationId}`)
|
|
1584
|
+
.setTitle('重命名当前会话');
|
|
1585
|
+
const input = new TextInputBuilder()
|
|
1586
|
+
.setCustomId(RENAME_INPUT_ID)
|
|
1587
|
+
.setLabel('新会话名称')
|
|
1588
|
+
.setStyle(TextInputStyle.Short)
|
|
1589
|
+
.setRequired(true)
|
|
1590
|
+
.setMaxLength(80);
|
|
1591
|
+
const row = new ActionRowBuilder().addComponents(input);
|
|
1592
|
+
modal.addComponents(row);
|
|
1593
|
+
await interaction.showModal(modal);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (selected === 'clear') {
|
|
1597
|
+
const current = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
1598
|
+
if (!current?.sessionId) {
|
|
1599
|
+
await this.safeInteractionReply(interaction, '当前频道没有活跃会话。');
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const shouldDeleteSession = current.protectSessionDelete !== true;
|
|
1603
|
+
const deleted = shouldDeleteSession
|
|
1604
|
+
? await opencodeClient.deleteSession(current.sessionId, current.resolvedDirectory ? { directory: current.resolvedDirectory } : undefined)
|
|
1605
|
+
: true;
|
|
1606
|
+
chatSessionStore.removeSessionByConversation('discord', conversationId);
|
|
1607
|
+
await this.safeInteractionReply(interaction, deleted
|
|
1608
|
+
? (shouldDeleteSession
|
|
1609
|
+
? '已清理并解绑当前会话。'
|
|
1610
|
+
: '已解绑当前频道(该会话为外部绑定,未删除 OpenCode 会话)。')
|
|
1611
|
+
: '频道绑定已清理,但 OpenCode 会话删除失败。');
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
await this.safeInteractionReply(interaction, this.getDiscordHelpText());
|
|
1615
|
+
}
|
|
1616
|
+
async handleQuestionSelect(interaction) {
|
|
1617
|
+
const deferred = await this.deferInteractionReply(interaction);
|
|
1618
|
+
if (!deferred) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const conversationId = parseConversationIdFromCustomId(QUESTION_SELECT_PREFIX, interaction.customId);
|
|
1622
|
+
if (!conversationId || interaction.channelId !== conversationId) {
|
|
1623
|
+
await this.safeInteractionReply(interaction, '会话上下文不匹配,请重新尝试。');
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const pending = this.getPendingQuestionByConversation(conversationId);
|
|
1627
|
+
if (!pending) {
|
|
1628
|
+
await this.safeInteractionReply(interaction, '当前没有待回答问题。');
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const questionCount = pending.request.questions.length;
|
|
1632
|
+
if (questionCount === 0) {
|
|
1633
|
+
await this.safeInteractionReply(interaction, '当前问题状态异常,请稍后重试。');
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const currentIndex = Math.min(Math.max(pending.currentQuestionIndex, 0), questionCount - 1);
|
|
1637
|
+
const question = pending.request.questions[currentIndex];
|
|
1638
|
+
const selectedValues = interaction.values;
|
|
1639
|
+
if (selectedValues.length === 0) {
|
|
1640
|
+
await this.safeInteractionReply(interaction, '未选择任何答案。');
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (selectedValues.includes('__custom__')) {
|
|
1644
|
+
await this.safeInteractionReply(interaction, '请直接在频道发送文本作为自定义答案。');
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
let parsed;
|
|
1648
|
+
if (selectedValues.includes('__skip__')) {
|
|
1649
|
+
parsed = { type: 'skip' };
|
|
1650
|
+
}
|
|
1651
|
+
else {
|
|
1652
|
+
const optionLabels = new Set(question.options.map(option => option.label));
|
|
1653
|
+
const validSelections = selectedValues.filter(value => optionLabels.has(value));
|
|
1654
|
+
if (validSelections.length === 0) {
|
|
1655
|
+
await this.safeInteractionReply(interaction, '所选答案无效,请重新选择。');
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
parsed = {
|
|
1659
|
+
type: 'selection',
|
|
1660
|
+
values: question.multiple ? validSelections : [validSelections[0]],
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
await this.applyPendingQuestionAnswer(pending, parsed, selectedValues.join(', '), async (message) => {
|
|
1664
|
+
await this.safeInteractionReply(interaction, message);
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
async handleModelSelect(interaction) {
|
|
1668
|
+
const deferred = await this.deferInteractionReply(interaction);
|
|
1669
|
+
if (!deferred) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const conversationId = parseConversationIdFromCustomId(MODEL_SELECT_PREFIX, interaction.customId);
|
|
1673
|
+
if (!conversationId || interaction.channelId !== conversationId) {
|
|
1674
|
+
await this.safeInteractionReply(interaction, '会话上下文不匹配,请重新执行操作。');
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const selected = interaction.values[0];
|
|
1678
|
+
if (!selected) {
|
|
1679
|
+
await this.safeInteractionReply(interaction, '未选择模型。');
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (selected === 'none') {
|
|
1683
|
+
chatSessionStore.updateConfigByConversation('discord', conversationId, { preferredModel: undefined });
|
|
1684
|
+
await this.safeInteractionReply(interaction, '✅ 已切换为默认模型。');
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
chatSessionStore.updateConfigByConversation('discord', conversationId, { preferredModel: selected });
|
|
1688
|
+
await this.safeInteractionReply(interaction, `✅ 已切换模型: ${selected}`);
|
|
1689
|
+
}
|
|
1690
|
+
async handleAgentSelect(interaction) {
|
|
1691
|
+
const deferred = await this.deferInteractionReply(interaction);
|
|
1692
|
+
if (!deferred) {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const conversationId = parseConversationIdFromCustomId(AGENT_SELECT_PREFIX, interaction.customId);
|
|
1696
|
+
if (!conversationId || interaction.channelId !== conversationId) {
|
|
1697
|
+
await this.safeInteractionReply(interaction, '会话上下文不匹配,请重新执行操作。');
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const selected = interaction.values[0];
|
|
1701
|
+
if (!selected) {
|
|
1702
|
+
await this.safeInteractionReply(interaction, '未选择角色。');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
chatSessionStore.updateConfigByConversation('discord', conversationId, {
|
|
1706
|
+
preferredAgent: selected === 'none' ? undefined : selected,
|
|
1707
|
+
});
|
|
1708
|
+
await this.safeInteractionReply(interaction, selected === 'none' ? '✅ 已切换为默认角色。' : `✅ 已切换角色: ${selected}`);
|
|
1709
|
+
}
|
|
1710
|
+
async handleBindSelect(interaction) {
|
|
1711
|
+
const deferred = await this.deferInteractionReply(interaction);
|
|
1712
|
+
if (!deferred) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const conversationId = parseConversationIdFromCustomId(BIND_SELECT_PREFIX, interaction.customId);
|
|
1716
|
+
if (!conversationId || interaction.channelId !== conversationId) {
|
|
1717
|
+
await this.safeInteractionReply(interaction, '会话上下文不匹配,请重新执行绑定。');
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
const selectedSessionId = interaction.values[0];
|
|
1721
|
+
if (!selectedSessionId) {
|
|
1722
|
+
await this.safeInteractionReply(interaction, '未选择会话。');
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const target = await opencodeClient.findSessionAcrossProjects(selectedSessionId);
|
|
1726
|
+
if (!target) {
|
|
1727
|
+
await this.safeInteractionReply(interaction, `未找到会话: ${selectedSessionId}`);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
await this.bindSessionToConversation(conversationId, target.id, interaction.user.id, target.title, 'group', target.directory, {
|
|
1731
|
+
protectSessionDelete: true,
|
|
1732
|
+
...(target.directory ? { defaultDirectory: target.directory } : {}),
|
|
1733
|
+
});
|
|
1734
|
+
await this.safeInteractionReply(interaction, `已绑定会话: ${target.id}`);
|
|
1735
|
+
}
|
|
1736
|
+
async handleRenameModal(interaction) {
|
|
1737
|
+
const conversationId = parseConversationIdFromCustomId(RENAME_MODAL_PREFIX, interaction.customId);
|
|
1738
|
+
if (!conversationId || interaction.channelId !== conversationId) {
|
|
1739
|
+
await this.safeInteractionReply(interaction, '会话上下文不匹配,请重新操作。');
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const nextName = interaction.fields.getTextInputValue(RENAME_INPUT_ID).trim();
|
|
1743
|
+
if (!nextName) {
|
|
1744
|
+
await this.safeInteractionReply(interaction, '会话名称不能为空。');
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
const current = chatSessionStore.getSessionByConversation('discord', conversationId);
|
|
1748
|
+
if (!current?.sessionId) {
|
|
1749
|
+
await this.safeInteractionReply(interaction, '当前频道尚未绑定会话。');
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const updated = await opencodeClient.updateSession(current.sessionId, nextName);
|
|
1753
|
+
if (!updated) {
|
|
1754
|
+
await this.safeInteractionReply(interaction, '重命名失败,请稍后重试。');
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
await this.bindSessionToConversation(conversationId, current.sessionId, current.creatorId || interaction.user.id, nextName, current.chatType === 'p2p' ? 'p2p' : 'group', current.resolvedDirectory);
|
|
1758
|
+
await this.safeInteractionReply(interaction, `会话已重命名为:${nextName}`);
|
|
1759
|
+
}
|
|
1760
|
+
async handleInteraction(interaction) {
|
|
1761
|
+
try {
|
|
1762
|
+
if (interaction.isStringSelectMenu()) {
|
|
1763
|
+
if (interaction.customId.startsWith(`${PANEL_SELECT_PREFIX}:`)) {
|
|
1764
|
+
await this.handlePanelSelect(interaction);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (interaction.customId.startsWith(`${BIND_SELECT_PREFIX}:`)) {
|
|
1768
|
+
await this.handleBindSelect(interaction);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (interaction.customId.startsWith(`${QUESTION_SELECT_PREFIX}:`)) {
|
|
1772
|
+
await this.handleQuestionSelect(interaction);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (interaction.customId.startsWith(`${MODEL_SELECT_PREFIX}:`)) {
|
|
1776
|
+
await this.handleModelSelect(interaction);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (interaction.customId.startsWith(`${AGENT_SELECT_PREFIX}:`)) {
|
|
1780
|
+
await this.handleAgentSelect(interaction);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (interaction.isModalSubmit() && interaction.customId.startsWith(`${RENAME_MODAL_PREFIX}:`)) {
|
|
1786
|
+
await this.handleRenameModal(interaction);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
catch (error) {
|
|
1790
|
+
if (this.isUnknownInteractionError(error)) {
|
|
1791
|
+
console.warn('[Discord] 交互已过期,忽略本次操作');
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
console.error('[Discord] 处理交互失败:', error);
|
|
1795
|
+
if (interaction.channelId) {
|
|
1796
|
+
await this.sender.sendText(interaction.channelId, '❌ 交互处理失败,请重试。');
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
async handleMessage(event) {
|
|
1801
|
+
const text = normalizeMessageText(event.content);
|
|
1802
|
+
const command = parseDiscordCommand(text);
|
|
1803
|
+
if (command) {
|
|
1804
|
+
const handled = await this.handleCommand(event, command);
|
|
1805
|
+
if (handled) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const naturalSendPath = parseNaturalFileSendText(text);
|
|
1810
|
+
if (naturalSendPath) {
|
|
1811
|
+
await this.sendFileToDiscord(event, naturalSendPath);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (this.shouldSkipMessage(event, text)) {
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const permissionHandled = await this.tryHandlePendingPermission(event, text);
|
|
1818
|
+
if (permissionHandled) {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const questionHandled = await this.tryHandlePendingQuestion(event, text);
|
|
1822
|
+
if (questionHandled) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const promptText = text || '请根据我发送的内容继续处理。';
|
|
1826
|
+
await this.handlePrompt(event, promptText);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
export function createDiscordHandler(sender) {
|
|
1830
|
+
return new DiscordHandler(sender);
|
|
1831
|
+
}
|
|
1832
|
+
//# sourceMappingURL=discord.js.map
|