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,1773 @@
|
|
|
1
|
+
import { getHelpText } from '../commands/parser.js';
|
|
2
|
+
import { KNOWN_EFFORT_LEVELS, normalizeEffortLevel } from '../commands/effort.js';
|
|
3
|
+
import { feishuClient } from '../feishu/client.js';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { buildSessionTimestamp } from '../utils/session-title.js';
|
|
6
|
+
import { opencodeClient, } from '../opencode/client.js';
|
|
7
|
+
import { chatSessionStore } from '../store/chat-session.js';
|
|
8
|
+
import { buildControlCard } from '../feishu/cards.js';
|
|
9
|
+
import { modelConfig, userConfig } from '../config.js';
|
|
10
|
+
import { sendFileToFeishu } from './file-sender.js';
|
|
11
|
+
import { lifecycleHandler } from './lifecycle.js';
|
|
12
|
+
import { DirectoryPolicy } from '../utils/directory-policy.js';
|
|
13
|
+
import { executeCronIntent, resolveCronIntentForExecution } from '../reliability/cron-control.js';
|
|
14
|
+
import { getRuntimeCronManager } from '../reliability/runtime-cron-registry.js';
|
|
15
|
+
import { formatRestartResultText, restartOpenCodeProcess } from '../reliability/opencode-restart.js';
|
|
16
|
+
import { parseCronIntentWithOpenCode } from '../reliability/cron-semantic.js';
|
|
17
|
+
import { cleanupRuntimeCronJobsBySessionId, scanAndCleanupOrphanRuntimeCronJobs } from '../reliability/runtime-cron-orphan.js';
|
|
18
|
+
const SUPPORTED_ROLE_TOOLS = [
|
|
19
|
+
'bash',
|
|
20
|
+
'read',
|
|
21
|
+
'write',
|
|
22
|
+
'edit',
|
|
23
|
+
'list',
|
|
24
|
+
'glob',
|
|
25
|
+
'grep',
|
|
26
|
+
'webfetch',
|
|
27
|
+
'task',
|
|
28
|
+
'todowrite',
|
|
29
|
+
'todoread',
|
|
30
|
+
];
|
|
31
|
+
const ROLE_TOOL_ALIAS = {
|
|
32
|
+
bash: 'bash',
|
|
33
|
+
shell: 'bash',
|
|
34
|
+
命令行: 'bash',
|
|
35
|
+
终端: 'bash',
|
|
36
|
+
read: 'read',
|
|
37
|
+
读取: 'read',
|
|
38
|
+
阅读: 'read',
|
|
39
|
+
write: 'write',
|
|
40
|
+
写入: 'write',
|
|
41
|
+
edit: 'edit',
|
|
42
|
+
编辑: 'edit',
|
|
43
|
+
list: 'list',
|
|
44
|
+
列表: 'list',
|
|
45
|
+
glob: 'glob',
|
|
46
|
+
文件匹配: 'glob',
|
|
47
|
+
grep: 'grep',
|
|
48
|
+
搜索: 'grep',
|
|
49
|
+
webfetch: 'webfetch',
|
|
50
|
+
网页: 'webfetch',
|
|
51
|
+
抓取网页: 'webfetch',
|
|
52
|
+
task: 'task',
|
|
53
|
+
子代理: 'task',
|
|
54
|
+
todowrite: 'todowrite',
|
|
55
|
+
待办写入: 'todowrite',
|
|
56
|
+
todoread: 'todoread',
|
|
57
|
+
待办读取: 'todoread',
|
|
58
|
+
};
|
|
59
|
+
const ROLE_CREATE_USAGE = '用法: 创建角色 名称=旅行助手; 描述=擅长制定旅行计划; 类型=主; 工具=webfetch; 提示词=先给出预算再做路线';
|
|
60
|
+
const INTERNAL_HIDDEN_AGENT_NAMES = new Set(['compaction', 'title', 'summary']);
|
|
61
|
+
const PANEL_MODEL_OPTION_LIMIT = 500;
|
|
62
|
+
const EFFORT_USAGE_TEXT = '用法: /effort(查看) 或 /effort <low|high|max|xhigh>(设置) 或 /effort default(清除)';
|
|
63
|
+
const EFFORT_DISPLAY_ORDER = KNOWN_EFFORT_LEVELS;
|
|
64
|
+
const BUILTIN_AGENT_TRANSLATION_RULES = [
|
|
65
|
+
{
|
|
66
|
+
names: ['build', 'default'],
|
|
67
|
+
descriptionStartsWith: 'the default agent. executes tools based on configured permissions.',
|
|
68
|
+
translated: '默认执行角色(按权限自动调用工具)',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
names: ['plan'],
|
|
72
|
+
descriptionStartsWith: 'plan mode. disallows all edit tools.',
|
|
73
|
+
translated: '规划模式(禁用编辑类工具)',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
names: ['general'],
|
|
77
|
+
descriptionStartsWith: 'general-purpose agent for researching complex questions and executing multi-step tasks.',
|
|
78
|
+
translated: '通用研究子角色(复杂任务/并行执行)',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
names: ['explore'],
|
|
82
|
+
descriptionStartsWith: 'fast agent specialized for exploring codebases.',
|
|
83
|
+
translated: '代码库探索子角色(快速检索与定位)',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
function normalizeAgentText(text) {
|
|
87
|
+
return text
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/\s+/g, ' ')
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
function stripWrappingQuotes(value) {
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (trimmed.length < 2)
|
|
95
|
+
return trimmed;
|
|
96
|
+
const first = trimmed[0];
|
|
97
|
+
const last = trimmed[trimmed.length - 1];
|
|
98
|
+
if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) {
|
|
99
|
+
return trimmed.slice(1, -1).trim();
|
|
100
|
+
}
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
function normalizeRoleMode(value) {
|
|
104
|
+
const normalized = value.trim().toLowerCase();
|
|
105
|
+
if (normalized === '主' || normalized === 'primary')
|
|
106
|
+
return 'primary';
|
|
107
|
+
if (normalized === '子' || normalized === 'subagent')
|
|
108
|
+
return 'subagent';
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function buildToolsConfig(value) {
|
|
112
|
+
const normalized = value.trim().toLowerCase();
|
|
113
|
+
if (!normalized || normalized === '默认' || normalized === 'default' || normalized === '继承' || normalized === 'all' || normalized === '全部') {
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
const toolsConfig = Object.fromEntries(SUPPORTED_ROLE_TOOLS.map(tool => [tool, false]));
|
|
117
|
+
if (normalized === 'none' || normalized === '无' || normalized === '关闭' || normalized === 'off') {
|
|
118
|
+
return { ok: true, tools: toolsConfig };
|
|
119
|
+
}
|
|
120
|
+
const rawItems = value.split(/[,,\s]+/).map(item => item.trim()).filter(Boolean);
|
|
121
|
+
if (rawItems.length === 0) {
|
|
122
|
+
return { ok: true };
|
|
123
|
+
}
|
|
124
|
+
const unsupported = [];
|
|
125
|
+
for (const rawItem of rawItems) {
|
|
126
|
+
const aliasKey = rawItem.toLowerCase();
|
|
127
|
+
const mapped = ROLE_TOOL_ALIAS[aliasKey] || ROLE_TOOL_ALIAS[rawItem];
|
|
128
|
+
if (!mapped) {
|
|
129
|
+
unsupported.push(rawItem);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
toolsConfig[mapped] = true;
|
|
133
|
+
}
|
|
134
|
+
if (unsupported.length > 0) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
message: `不支持的工具: ${unsupported.join(', ')}\n可用工具: ${SUPPORTED_ROLE_TOOLS.join(', ')}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, tools: toolsConfig };
|
|
141
|
+
}
|
|
142
|
+
function parseRoleCreateSpec(spec) {
|
|
143
|
+
const raw = spec.trim();
|
|
144
|
+
if (!raw) {
|
|
145
|
+
return { ok: false, message: `缺少角色参数\n${ROLE_CREATE_USAGE}` };
|
|
146
|
+
}
|
|
147
|
+
const segments = raw.split(/[;;\n]+/).map(item => item.trim()).filter(Boolean);
|
|
148
|
+
if (segments.length === 0) {
|
|
149
|
+
return { ok: false, message: `缺少角色参数\n${ROLE_CREATE_USAGE}` };
|
|
150
|
+
}
|
|
151
|
+
let name = '';
|
|
152
|
+
let description = '';
|
|
153
|
+
let modeRaw = '';
|
|
154
|
+
let toolsRaw = '';
|
|
155
|
+
let prompt = '';
|
|
156
|
+
for (const segment of segments) {
|
|
157
|
+
const sepIndex = segment.search(/[=::]/);
|
|
158
|
+
if (sepIndex < 0) {
|
|
159
|
+
if (!name) {
|
|
160
|
+
name = stripWrappingQuotes(segment);
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const key = segment.slice(0, sepIndex).trim().toLowerCase();
|
|
165
|
+
const value = stripWrappingQuotes(segment.slice(sepIndex + 1));
|
|
166
|
+
if (!value)
|
|
167
|
+
continue;
|
|
168
|
+
if (key === '名称' || key === '名字' || key === '角色' || key === 'name' || key === 'role') {
|
|
169
|
+
name = value;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (key === '描述' || key === '说明' || key === 'description' || key === 'desc') {
|
|
173
|
+
description = value;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (key === '类型' || key === '模式' || key === 'mode') {
|
|
177
|
+
modeRaw = value;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (key === '工具' || key === 'tools' || key === 'tool') {
|
|
181
|
+
toolsRaw = value;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (key === '提示词' || key === 'prompt' || key === '系统提示' || key === '指令') {
|
|
185
|
+
prompt = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
name = name.trim();
|
|
189
|
+
if (!name) {
|
|
190
|
+
return { ok: false, message: `缺少角色名称\n${ROLE_CREATE_USAGE}` };
|
|
191
|
+
}
|
|
192
|
+
if (/\s/.test(name)) {
|
|
193
|
+
return { ok: false, message: '角色名称不能包含空格,请使用连续字符(可含中文)。' };
|
|
194
|
+
}
|
|
195
|
+
if (name.length > 40) {
|
|
196
|
+
return { ok: false, message: '角色名称长度不能超过 40 个字符。' };
|
|
197
|
+
}
|
|
198
|
+
let mode = 'primary';
|
|
199
|
+
if (modeRaw) {
|
|
200
|
+
const parsedMode = normalizeRoleMode(modeRaw);
|
|
201
|
+
if (!parsedMode) {
|
|
202
|
+
return { ok: false, message: '角色类型仅支持 主 / 子(或 primary / subagent)。' };
|
|
203
|
+
}
|
|
204
|
+
mode = parsedMode;
|
|
205
|
+
}
|
|
206
|
+
const toolsResult = buildToolsConfig(toolsRaw);
|
|
207
|
+
if (!toolsResult.ok)
|
|
208
|
+
return { ok: false, message: toolsResult.message };
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
payload: {
|
|
212
|
+
name,
|
|
213
|
+
description: description || `${name}(自定义角色)`,
|
|
214
|
+
mode,
|
|
215
|
+
...(toolsResult.tools ? { tools: toolsResult.tools } : {}),
|
|
216
|
+
...(prompt ? { prompt } : {}),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export class CommandHandler {
|
|
221
|
+
parseProviderModel(raw) {
|
|
222
|
+
if (!raw) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const trimmed = raw.trim();
|
|
226
|
+
if (!trimmed) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const separator = trimmed.includes(':') ? ':' : (trimmed.includes('/') ? '/' : '');
|
|
230
|
+
if (!separator) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const splitIndex = trimmed.indexOf(separator);
|
|
234
|
+
const providerId = trimmed.slice(0, splitIndex).trim();
|
|
235
|
+
const modelId = trimmed.slice(splitIndex + 1).trim();
|
|
236
|
+
if (!providerId || !modelId) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return { providerId, modelId };
|
|
240
|
+
}
|
|
241
|
+
extractProviderId(provider) {
|
|
242
|
+
if (!provider || typeof provider !== 'object') {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
const record = provider;
|
|
246
|
+
const rawId = record.id;
|
|
247
|
+
if (typeof rawId !== 'string') {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const normalized = rawId.trim();
|
|
251
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
252
|
+
}
|
|
253
|
+
extractProviderModelIds(provider) {
|
|
254
|
+
if (!provider || typeof provider !== 'object') {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const record = provider;
|
|
258
|
+
const rawModels = record.models;
|
|
259
|
+
if (Array.isArray(rawModels)) {
|
|
260
|
+
const modelIds = [];
|
|
261
|
+
for (const model of rawModels) {
|
|
262
|
+
if (!model || typeof model !== 'object') {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const modelRecord = model;
|
|
266
|
+
const modelId = typeof modelRecord.id === 'string' ? modelRecord.id.trim() : '';
|
|
267
|
+
if (modelId) {
|
|
268
|
+
modelIds.push(modelId);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return modelIds;
|
|
272
|
+
}
|
|
273
|
+
if (!rawModels || typeof rawModels !== 'object') {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
const modelMap = rawModels;
|
|
277
|
+
const modelIds = [];
|
|
278
|
+
for (const [key, value] of Object.entries(modelMap)) {
|
|
279
|
+
if (value && typeof value === 'object') {
|
|
280
|
+
const modelRecord = value;
|
|
281
|
+
const modelId = typeof modelRecord.id === 'string' ? modelRecord.id.trim() : '';
|
|
282
|
+
if (modelId) {
|
|
283
|
+
modelIds.push(modelId);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const normalizedKey = key.trim();
|
|
288
|
+
if (normalizedKey) {
|
|
289
|
+
modelIds.push(normalizedKey);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return modelIds;
|
|
293
|
+
}
|
|
294
|
+
extractEffortVariants(modelRecord) {
|
|
295
|
+
const rawVariants = modelRecord.variants;
|
|
296
|
+
if (!rawVariants || typeof rawVariants !== 'object' || Array.isArray(rawVariants)) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
const variants = rawVariants;
|
|
300
|
+
const efforts = [];
|
|
301
|
+
for (const key of Object.keys(variants)) {
|
|
302
|
+
const normalized = normalizeEffortLevel(key);
|
|
303
|
+
if (!normalized || efforts.includes(normalized)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
efforts.push(normalized);
|
|
307
|
+
}
|
|
308
|
+
return this.sortEffortLevels(efforts);
|
|
309
|
+
}
|
|
310
|
+
sortEffortLevels(efforts) {
|
|
311
|
+
const order = new Map();
|
|
312
|
+
EFFORT_DISPLAY_ORDER.forEach((value, index) => {
|
|
313
|
+
order.set(value, index);
|
|
314
|
+
});
|
|
315
|
+
return [...efforts].sort((left, right) => {
|
|
316
|
+
const leftOrder = order.get(left) ?? Number.MAX_SAFE_INTEGER;
|
|
317
|
+
const rightOrder = order.get(right) ?? Number.MAX_SAFE_INTEGER;
|
|
318
|
+
if (leftOrder !== rightOrder) {
|
|
319
|
+
return leftOrder - rightOrder;
|
|
320
|
+
}
|
|
321
|
+
return left.localeCompare(right);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
extractProviderModels(provider) {
|
|
325
|
+
if (!provider || typeof provider !== 'object') {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
const providerId = this.extractProviderId(provider);
|
|
329
|
+
if (!providerId) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
const record = provider;
|
|
333
|
+
const rawModels = record.models;
|
|
334
|
+
const models = [];
|
|
335
|
+
const dedupe = new Set();
|
|
336
|
+
const pushModel = (rawModel, fallbackId) => {
|
|
337
|
+
const fallbackNormalized = typeof fallbackId === 'string' ? fallbackId.trim() : '';
|
|
338
|
+
if (!rawModel || typeof rawModel !== 'object') {
|
|
339
|
+
if (!fallbackNormalized) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const key = `${providerId.toLowerCase()}:${fallbackNormalized.toLowerCase()}`;
|
|
343
|
+
if (dedupe.has(key)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
dedupe.add(key);
|
|
347
|
+
models.push({
|
|
348
|
+
providerId,
|
|
349
|
+
modelId: fallbackNormalized,
|
|
350
|
+
variants: [],
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const modelRecord = rawModel;
|
|
355
|
+
const modelId = typeof modelRecord.id === 'string' && modelRecord.id.trim()
|
|
356
|
+
? modelRecord.id.trim()
|
|
357
|
+
: fallbackNormalized;
|
|
358
|
+
if (!modelId) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const modelName = typeof modelRecord.name === 'string' && modelRecord.name.trim()
|
|
362
|
+
? modelRecord.name.trim()
|
|
363
|
+
: undefined;
|
|
364
|
+
const variants = this.extractEffortVariants(modelRecord);
|
|
365
|
+
const key = `${providerId.toLowerCase()}:${modelId.toLowerCase()}`;
|
|
366
|
+
if (dedupe.has(key)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
dedupe.add(key);
|
|
370
|
+
models.push({
|
|
371
|
+
providerId,
|
|
372
|
+
modelId,
|
|
373
|
+
...(modelName ? { modelName } : {}),
|
|
374
|
+
variants,
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
if (Array.isArray(rawModels)) {
|
|
378
|
+
for (const rawModel of rawModels) {
|
|
379
|
+
pushModel(rawModel);
|
|
380
|
+
}
|
|
381
|
+
return models;
|
|
382
|
+
}
|
|
383
|
+
if (!rawModels || typeof rawModels !== 'object') {
|
|
384
|
+
return models;
|
|
385
|
+
}
|
|
386
|
+
const modelMap = rawModels;
|
|
387
|
+
for (const [modelKey, rawModel] of Object.entries(modelMap)) {
|
|
388
|
+
pushModel(rawModel, modelKey);
|
|
389
|
+
}
|
|
390
|
+
return models;
|
|
391
|
+
}
|
|
392
|
+
isSameIdentifier(left, right) {
|
|
393
|
+
return left.trim().toLowerCase() === right.trim().toLowerCase();
|
|
394
|
+
}
|
|
395
|
+
findProviderModel(providers, providerId, modelId) {
|
|
396
|
+
for (const provider of providers) {
|
|
397
|
+
const providerModels = this.extractProviderModels(provider);
|
|
398
|
+
for (const model of providerModels) {
|
|
399
|
+
if (!this.isSameIdentifier(model.providerId, providerId)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (!this.isSameIdentifier(model.modelId, modelId)) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
return model;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
resolveModelFromProviderPayload(chatId, providersResult) {
|
|
411
|
+
const session = chatSessionStore.getSession(chatId);
|
|
412
|
+
const preferredModel = this.parseProviderModel(session?.preferredModel);
|
|
413
|
+
if (preferredModel) {
|
|
414
|
+
return preferredModel;
|
|
415
|
+
}
|
|
416
|
+
if (modelConfig.defaultProvider && modelConfig.defaultModel) {
|
|
417
|
+
return {
|
|
418
|
+
providerId: modelConfig.defaultProvider,
|
|
419
|
+
modelId: modelConfig.defaultModel,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const providersRaw = Array.isArray(providersResult.providers) ? providersResult.providers : [];
|
|
423
|
+
const defaultsRaw = providersResult.default;
|
|
424
|
+
const defaults = defaultsRaw && typeof defaultsRaw === 'object'
|
|
425
|
+
? defaultsRaw
|
|
426
|
+
: {};
|
|
427
|
+
const availableProviderIds = new Set();
|
|
428
|
+
for (const provider of providersRaw) {
|
|
429
|
+
const providerId = this.extractProviderId(provider);
|
|
430
|
+
if (providerId) {
|
|
431
|
+
availableProviderIds.add(providerId);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const preferredProviders = ['openai', 'opencode'];
|
|
435
|
+
for (const providerId of preferredProviders) {
|
|
436
|
+
const defaultModel = defaults[providerId];
|
|
437
|
+
if (typeof defaultModel === 'string' && defaultModel.trim() && availableProviderIds.has(providerId)) {
|
|
438
|
+
return {
|
|
439
|
+
providerId,
|
|
440
|
+
modelId: defaultModel.trim(),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const provider of providersRaw) {
|
|
445
|
+
const providerId = this.extractProviderId(provider);
|
|
446
|
+
if (!providerId) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const defaultModel = defaults[providerId];
|
|
450
|
+
if (typeof defaultModel === 'string' && defaultModel.trim()) {
|
|
451
|
+
return {
|
|
452
|
+
providerId,
|
|
453
|
+
modelId: defaultModel.trim(),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const provider of providersRaw) {
|
|
458
|
+
const providerId = this.extractProviderId(provider);
|
|
459
|
+
if (!providerId) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const modelIds = this.extractProviderModelIds(provider);
|
|
463
|
+
if (modelIds.length > 0) {
|
|
464
|
+
return {
|
|
465
|
+
providerId,
|
|
466
|
+
modelId: modelIds[0],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
async getEffortSupportInfo(chatId) {
|
|
473
|
+
const providersResult = await opencodeClient.getProviders();
|
|
474
|
+
const model = this.resolveModelFromProviderPayload(chatId, providersResult);
|
|
475
|
+
if (!model) {
|
|
476
|
+
return {
|
|
477
|
+
model: null,
|
|
478
|
+
supportedEfforts: [],
|
|
479
|
+
modelMatched: false,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const providersRaw = Array.isArray(providersResult.providers) ? providersResult.providers : [];
|
|
483
|
+
const matchedModel = this.findProviderModel(providersRaw, model.providerId, model.modelId);
|
|
484
|
+
if (!matchedModel) {
|
|
485
|
+
return {
|
|
486
|
+
model,
|
|
487
|
+
supportedEfforts: [],
|
|
488
|
+
modelMatched: false,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
model,
|
|
493
|
+
supportedEfforts: matchedModel.variants,
|
|
494
|
+
modelMatched: true,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
formatModelLabel(model) {
|
|
498
|
+
if (!model) {
|
|
499
|
+
return '未知';
|
|
500
|
+
}
|
|
501
|
+
return `${model.providerId}:${model.modelId}`;
|
|
502
|
+
}
|
|
503
|
+
formatEffortList(efforts) {
|
|
504
|
+
if (efforts.length === 0) {
|
|
505
|
+
return '该模型未公开可选强度';
|
|
506
|
+
}
|
|
507
|
+
return efforts.join(' / ');
|
|
508
|
+
}
|
|
509
|
+
async reconcilePreferredEffort(chatId) {
|
|
510
|
+
const session = chatSessionStore.getSession(chatId);
|
|
511
|
+
const currentEffort = session?.preferredEffort;
|
|
512
|
+
const support = await this.getEffortSupportInfo(chatId);
|
|
513
|
+
if (!currentEffort || !support.modelMatched) {
|
|
514
|
+
return { support };
|
|
515
|
+
}
|
|
516
|
+
if (support.supportedEfforts.includes(currentEffort)) {
|
|
517
|
+
return { support };
|
|
518
|
+
}
|
|
519
|
+
chatSessionStore.updateConfig(chatId, { preferredEffort: undefined });
|
|
520
|
+
return {
|
|
521
|
+
clearedEffort: currentEffort,
|
|
522
|
+
support,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async resolveCompactModel(chatId) {
|
|
526
|
+
const providersResult = await opencodeClient.getProviders();
|
|
527
|
+
return this.resolveModelFromProviderPayload(chatId, providersResult);
|
|
528
|
+
}
|
|
529
|
+
async resolveShellAgent(chatId) {
|
|
530
|
+
const fallbackAgent = 'general';
|
|
531
|
+
const preferredAgentRaw = chatSessionStore.getSession(chatId)?.preferredAgent;
|
|
532
|
+
const preferredAgent = typeof preferredAgentRaw === 'string' ? preferredAgentRaw.trim() : '';
|
|
533
|
+
if (!preferredAgent) {
|
|
534
|
+
return fallbackAgent;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
const agents = await opencodeClient.getAgents();
|
|
538
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
539
|
+
return fallbackAgent;
|
|
540
|
+
}
|
|
541
|
+
const exact = agents.find(item => item.name === preferredAgent);
|
|
542
|
+
if (exact) {
|
|
543
|
+
return exact.name;
|
|
544
|
+
}
|
|
545
|
+
const preferredLower = preferredAgent.toLowerCase();
|
|
546
|
+
const caseInsensitive = agents.find(item => item.name.toLowerCase() === preferredLower);
|
|
547
|
+
if (caseInsensitive) {
|
|
548
|
+
return caseInsensitive.name;
|
|
549
|
+
}
|
|
550
|
+
const hasFallback = agents.some(item => item.name === fallbackAgent);
|
|
551
|
+
if (hasFallback) {
|
|
552
|
+
return fallbackAgent;
|
|
553
|
+
}
|
|
554
|
+
return agents[0].name;
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return fallbackAgent;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async handleCompact(chatId, messageId) {
|
|
561
|
+
const sessionId = chatSessionStore.getSessionId(chatId);
|
|
562
|
+
if (!sessionId) {
|
|
563
|
+
await feishuClient.reply(messageId, '❌ 当前没有活跃的会话,请先发送消息建立会话');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const model = await this.resolveCompactModel(chatId);
|
|
567
|
+
if (!model) {
|
|
568
|
+
await feishuClient.reply(messageId, '❌ 未找到可用模型,无法执行上下文压缩');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const compacted = await opencodeClient.summarizeSession(sessionId, model.providerId, model.modelId);
|
|
572
|
+
if (!compacted) {
|
|
573
|
+
await feishuClient.reply(messageId, `❌ 上下文压缩失败(模型: ${model.providerId}:${model.modelId})`);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
await feishuClient.reply(messageId, `✅ 上下文压缩完成(模型: ${model.providerId}:${model.modelId})`);
|
|
577
|
+
}
|
|
578
|
+
getPrivateSessionShortId(userId) {
|
|
579
|
+
const normalized = userId.startsWith('ou_') ? userId.slice(3) : userId;
|
|
580
|
+
return normalized.slice(0, 4);
|
|
581
|
+
}
|
|
582
|
+
buildSessionTitle(chatType, _userId) {
|
|
583
|
+
const timestamp = buildSessionTimestamp();
|
|
584
|
+
if (chatType === 'p2p') {
|
|
585
|
+
return `私聊-${timestamp}`;
|
|
586
|
+
}
|
|
587
|
+
return `群聊-${timestamp}`;
|
|
588
|
+
}
|
|
589
|
+
async handle(command, context) {
|
|
590
|
+
const { chatId, messageId } = context;
|
|
591
|
+
try {
|
|
592
|
+
switch (command.type) {
|
|
593
|
+
case 'help':
|
|
594
|
+
await feishuClient.reply(messageId, getHelpText());
|
|
595
|
+
break;
|
|
596
|
+
case 'status':
|
|
597
|
+
await this.handleStatus(chatId, messageId);
|
|
598
|
+
break;
|
|
599
|
+
case 'session':
|
|
600
|
+
if (command.sessionAction === 'new') {
|
|
601
|
+
await this.handleNewSession(chatId, messageId, context.senderId, context.chatType, command.sessionDirectory, command.sessionName);
|
|
602
|
+
}
|
|
603
|
+
else if (command.sessionAction === 'switch' && command.sessionId) {
|
|
604
|
+
await this.handleSwitchSession(chatId, messageId, context.senderId, command.sessionId, context.chatType);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
await feishuClient.reply(messageId, '用法: /session new 或 /session <sessionId>(列出会话请用 /sessions)');
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
case 'project':
|
|
611
|
+
if (command.projectAction === 'list') {
|
|
612
|
+
await this.handleProjectList(chatId, messageId);
|
|
613
|
+
}
|
|
614
|
+
else if (command.projectAction === 'default_set' && command.projectValue) {
|
|
615
|
+
await this.handleProjectDefault(chatId, messageId, 'set', command.projectValue);
|
|
616
|
+
}
|
|
617
|
+
else if (command.projectAction === 'default_clear') {
|
|
618
|
+
await this.handleProjectDefault(chatId, messageId, 'clear');
|
|
619
|
+
}
|
|
620
|
+
else if (command.projectAction === 'default_show') {
|
|
621
|
+
await this.handleProjectDefaultShow(chatId, messageId);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
await feishuClient.reply(messageId, '用法: /project list 或 /project default set <路径或别名>');
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
case 'clear':
|
|
628
|
+
console.log(`[Command] clear 命令, clearScope=${command.clearScope}`);
|
|
629
|
+
if (command.clearScope === 'free_session') {
|
|
630
|
+
// 清理空闲群聊(可选:指定 sessionId 删除特定会话)
|
|
631
|
+
await this.handleClearFreeSession(chatId, messageId, command.clearSessionId);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// 清空当前对话上下文(默认行为)
|
|
635
|
+
await this.handleNewSession(chatId, messageId, context.senderId, context.chatType);
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
case 'stop': {
|
|
639
|
+
const sessionId = chatSessionStore.getSessionId(chatId);
|
|
640
|
+
if (sessionId) {
|
|
641
|
+
await opencodeClient.abortSession(sessionId);
|
|
642
|
+
await feishuClient.reply(messageId, '⏹️ 已发送中断请求');
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
await feishuClient.reply(messageId, '当前没有活跃的会话');
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case 'compact':
|
|
650
|
+
await this.handleCompact(chatId, messageId);
|
|
651
|
+
break;
|
|
652
|
+
case 'command':
|
|
653
|
+
// 未知命令透传到 OpenCode
|
|
654
|
+
await this.handlePassthroughCommand(chatId, messageId, command.commandName || '', command.commandArgs || '', command.commandPrefix || '/');
|
|
655
|
+
break;
|
|
656
|
+
case 'model':
|
|
657
|
+
await this.handleModel(chatId, messageId, context.senderId, context.chatType, command.modelName);
|
|
658
|
+
break;
|
|
659
|
+
case 'agent':
|
|
660
|
+
await this.handleAgent(chatId, messageId, context.senderId, context.chatType, command.agentName);
|
|
661
|
+
break;
|
|
662
|
+
case 'effort':
|
|
663
|
+
await this.handleEffort(chatId, messageId, context.senderId, context.chatType, command);
|
|
664
|
+
break;
|
|
665
|
+
case 'role':
|
|
666
|
+
if (command.roleAction === 'create') {
|
|
667
|
+
await this.handleRoleCreate(chatId, messageId, context.senderId, context.chatType, command.roleSpec || '');
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
await feishuClient.reply(messageId, `支持的角色命令:\n- ${ROLE_CREATE_USAGE}`);
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
case 'undo':
|
|
674
|
+
await this.handleUndo(chatId, messageId);
|
|
675
|
+
break;
|
|
676
|
+
case 'panel':
|
|
677
|
+
await this.handlePanel(chatId, messageId, context.chatType);
|
|
678
|
+
break;
|
|
679
|
+
case 'sessions':
|
|
680
|
+
await this.handleListSessions(chatId, messageId, command.listAll);
|
|
681
|
+
break;
|
|
682
|
+
case 'send':
|
|
683
|
+
await this.handleSendFile(chatId, messageId, command.text || '');
|
|
684
|
+
break;
|
|
685
|
+
case 'rename':
|
|
686
|
+
await this.handleRename(chatId, messageId, command.renameTitle);
|
|
687
|
+
break;
|
|
688
|
+
case 'cron':
|
|
689
|
+
await this.handleCronCommand(chatId, messageId, context.senderId, command);
|
|
690
|
+
break;
|
|
691
|
+
case 'restart':
|
|
692
|
+
await this.handleRestartCommand(messageId, command.restartTarget);
|
|
693
|
+
break;
|
|
694
|
+
// 其他命令透传
|
|
695
|
+
default:
|
|
696
|
+
await this.handlePassthroughCommand(chatId, messageId, command.type.replace(/^\//, ''), command.commandArgs || '');
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
702
|
+
console.error('[Command] 执行失败详情:', errorMessage);
|
|
703
|
+
await feishuClient.reply(messageId, '❌ 命令执行出错,请稍后重试');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async handleRename(chatId, messageId, newTitle) {
|
|
707
|
+
const sessionId = chatSessionStore.getSessionId(chatId);
|
|
708
|
+
if (!sessionId) {
|
|
709
|
+
await feishuClient.reply(messageId, '❌ 当前没有活跃的会话,请先发送消息建立会话');
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
// 无参数时提示用法(Phase 2 卡片交互预留位)
|
|
713
|
+
if (!newTitle || !newTitle.trim()) {
|
|
714
|
+
await feishuClient.reply(messageId, '用法: /rename <新名称>\n示例: /rename Q3后端API设计讨论');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const trimmedTitle = newTitle.trim();
|
|
718
|
+
// 名称长度校验(飞书消息限制,兼容中文分词)
|
|
719
|
+
if (trimmedTitle.length > 100) {
|
|
720
|
+
await feishuClient.reply(messageId, `❌ 会话名称过长(${trimmedTitle.length} 字符),请控制在 100 字符以内`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const success = await opencodeClient.updateSession(sessionId, trimmedTitle);
|
|
724
|
+
if (success) {
|
|
725
|
+
// 同步更新本地缓存中的会话标题
|
|
726
|
+
chatSessionStore.updateTitle(chatId, trimmedTitle);
|
|
727
|
+
await feishuClient.reply(messageId, `✅ 会话已重命名为 "${trimmedTitle}"`);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
await feishuClient.reply(messageId, '❌ 重命名失败,请稍后重试');
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async handleCronCommand(chatId, messageId, senderId, command) {
|
|
734
|
+
const manager = getRuntimeCronManager();
|
|
735
|
+
const session = chatSessionStore.getSession(chatId);
|
|
736
|
+
const intent = await resolveCronIntentForExecution({
|
|
737
|
+
source: command.cronSource || 'slash',
|
|
738
|
+
action: command.cronAction,
|
|
739
|
+
argsText: command.cronArgs || '',
|
|
740
|
+
semanticParser: async (argsText, source, actionHint) => {
|
|
741
|
+
return await parseCronIntentWithOpenCode({
|
|
742
|
+
argsText,
|
|
743
|
+
source,
|
|
744
|
+
actionHint,
|
|
745
|
+
directory: session?.resolvedDirectory || session?.defaultDirectory,
|
|
746
|
+
});
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
const resultText = executeCronIntent({
|
|
750
|
+
manager,
|
|
751
|
+
intent,
|
|
752
|
+
currentSessionId: session?.sessionId,
|
|
753
|
+
currentDirectory: session?.resolvedDirectory || session?.defaultDirectory,
|
|
754
|
+
currentConversationId: chatId,
|
|
755
|
+
creatorId: senderId,
|
|
756
|
+
platform: 'feishu',
|
|
757
|
+
});
|
|
758
|
+
await feishuClient.reply(messageId, resultText);
|
|
759
|
+
}
|
|
760
|
+
async handleRestartCommand(messageId, target) {
|
|
761
|
+
const normalizedTarget = (target || '').trim().toLowerCase();
|
|
762
|
+
if (normalizedTarget !== 'opencode') {
|
|
763
|
+
await feishuClient.reply(messageId, '用法: /restart opencode');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
await feishuClient.reply(messageId, '🔄 正在重启 OpenCode,请稍候...');
|
|
767
|
+
const result = await restartOpenCodeProcess();
|
|
768
|
+
await feishuClient.reply(messageId, formatRestartResultText(result));
|
|
769
|
+
}
|
|
770
|
+
async handleStatus(chatId, messageId) {
|
|
771
|
+
const sessionId = chatSessionStore.getSessionId(chatId);
|
|
772
|
+
// 这里简单返回文本,或者用 StatusCard
|
|
773
|
+
const status = sessionId ? `当前绑定 Session: ${sessionId}` : '未绑定 Session';
|
|
774
|
+
// 如果能获取更多信息更好
|
|
775
|
+
let extra = '';
|
|
776
|
+
if (sessionId) {
|
|
777
|
+
// 尝试获取 session 详情? 暂时跳过
|
|
778
|
+
}
|
|
779
|
+
await feishuClient.reply(messageId, `🤖 **OpenCode 状态**\n\n${status}\n${extra}`);
|
|
780
|
+
}
|
|
781
|
+
async handleNewSession(chatId, messageId, userId, chatType, rawDirectory, customName) {
|
|
782
|
+
// 1. 通过 DirectoryPolicy 解析目录
|
|
783
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
784
|
+
const normalizedRaw = rawDirectory?.trim() || '';
|
|
785
|
+
const explicitDirectory = normalizedRaw && path.isAbsolute(normalizedRaw) ? normalizedRaw : undefined;
|
|
786
|
+
const aliasName = normalizedRaw && !path.isAbsolute(normalizedRaw) ? normalizedRaw : undefined;
|
|
787
|
+
const dirResult = DirectoryPolicy.resolve({
|
|
788
|
+
explicitDirectory,
|
|
789
|
+
aliasName,
|
|
790
|
+
chatDefaultDirectory: chatDefault,
|
|
791
|
+
});
|
|
792
|
+
if (!dirResult.ok) {
|
|
793
|
+
console.warn(`[Command] 目录校验失败: ${dirResult.userMessage}`);
|
|
794
|
+
await feishuClient.reply(messageId, dirResult.userMessage);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
// 2. 创建会话(优先使用 --name 参数,其次使用默认命名规则)
|
|
798
|
+
const title = customName?.trim() || this.buildSessionTitle(chatType, userId);
|
|
799
|
+
const effectiveDir = dirResult.source === 'server_default' ? undefined : dirResult.directory;
|
|
800
|
+
try {
|
|
801
|
+
const session = await opencodeClient.createSession(title, effectiveDir);
|
|
802
|
+
if (session) {
|
|
803
|
+
chatSessionStore.setSession(chatId, session.id, userId, title, {
|
|
804
|
+
chatType,
|
|
805
|
+
resolvedDirectory: session.directory,
|
|
806
|
+
projectName: dirResult.projectName,
|
|
807
|
+
});
|
|
808
|
+
// 用户显式指定路径或别名时,同步更新群默认目录
|
|
809
|
+
if (session.directory && (dirResult.source === 'explicit' || dirResult.source === 'alias')) {
|
|
810
|
+
chatSessionStore.updateConfig(chatId, { defaultDirectory: session.directory });
|
|
811
|
+
}
|
|
812
|
+
const projectLabel = dirResult.projectName ? `\n📁 项目: ${dirResult.projectName}` : '';
|
|
813
|
+
const dirInfo = session.directory ? `\n📂 工作目录: ${session.directory}` : '';
|
|
814
|
+
await feishuClient.reply(messageId, `✅ 已创建新会话窗口\nID: ${session.id}${projectLabel}${dirInfo}`);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
await feishuClient.reply(messageId, '❌ 创建会话失败');
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
console.error('[Command] 创建会话失败:', error);
|
|
822
|
+
await feishuClient.reply(messageId, '❌ 创建会话失败,请检查目录是否为有效的代码仓库\n使用 /project list 查看可用项目');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async handleProjectList(chatId, messageId) {
|
|
826
|
+
// 从 store 收集所有已知目录,聚合查询各 Instance 的 session
|
|
827
|
+
const storeKnownDirs = chatSessionStore.getKnownDirectories();
|
|
828
|
+
let knownDirs = [...storeKnownDirs];
|
|
829
|
+
try {
|
|
830
|
+
// 注意:这里使用 listSessionsAcrossProjects 从已知目录获取会话
|
|
831
|
+
const sessions = await opencodeClient.listSessionsAcrossProjects();
|
|
832
|
+
const sessionDirs = sessions
|
|
833
|
+
.map((session) => session.directory)
|
|
834
|
+
.filter((directory) => Boolean(directory));
|
|
835
|
+
const uniqueDirs = new Set([...knownDirs, ...sessionDirs]);
|
|
836
|
+
knownDirs = Array.from(uniqueDirs);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
console.warn('[Command] 获取会话列表失败:', error);
|
|
840
|
+
}
|
|
841
|
+
const projects = DirectoryPolicy.listAvailableProjects(knownDirs);
|
|
842
|
+
if (projects.length === 0) {
|
|
843
|
+
await feishuClient.reply(messageId, '暂无可用项目\n管理员可通过 PROJECT_ALIASES 环境变量配置项目别名');
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const lines = projects.map((project, index) => {
|
|
847
|
+
const tag = project.source === 'alias' ? '🏷️' : '📂';
|
|
848
|
+
return `${index + 1}. ${tag} **${project.name}** — ${project.directory}`;
|
|
849
|
+
});
|
|
850
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
851
|
+
const defaultLine = chatDefault ? `\n当前群默认: ${chatDefault}` : '\n当前群默认: 跟随全局';
|
|
852
|
+
await feishuClient.reply(messageId, `📋 **可用项目列表**\n\n${lines.join('\n')}${defaultLine}\n\n使用 \`/session new <项目名或路径>\` 创建指定项目的会话`);
|
|
853
|
+
}
|
|
854
|
+
async handleProjectDefault(chatId, messageId, action, value) {
|
|
855
|
+
if (action === 'clear') {
|
|
856
|
+
chatSessionStore.updateConfig(chatId, { defaultDirectory: undefined });
|
|
857
|
+
await feishuClient.reply(messageId, '✅ 已清除群默认项目,将跟随全局默认');
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (!value) {
|
|
861
|
+
await feishuClient.reply(messageId, '用法: /project default set <路径或别名>');
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const normalizedValue = value.trim();
|
|
865
|
+
const explicitDirectory = normalizedValue && path.isAbsolute(normalizedValue) ? normalizedValue : undefined;
|
|
866
|
+
const aliasName = normalizedValue && !path.isAbsolute(normalizedValue) ? normalizedValue : undefined;
|
|
867
|
+
const dirResult = DirectoryPolicy.resolve({ explicitDirectory, aliasName });
|
|
868
|
+
if (!dirResult.ok) {
|
|
869
|
+
await feishuClient.reply(messageId, dirResult.userMessage);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
chatSessionStore.updateConfig(chatId, { defaultDirectory: dirResult.directory });
|
|
873
|
+
const label = dirResult.projectName ? ` (${dirResult.projectName})` : '';
|
|
874
|
+
await feishuClient.reply(messageId, `✅ 已设置群默认项目: ${dirResult.directory}${label}`);
|
|
875
|
+
}
|
|
876
|
+
async handleProjectDefaultShow(chatId, messageId) {
|
|
877
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
878
|
+
if (chatDefault) {
|
|
879
|
+
await feishuClient.reply(messageId, `当前群默认项目: ${chatDefault}\n使用 \`/project default clear\` 清除`);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
await feishuClient.reply(messageId, '当前群未设置默认项目(跟随全局默认)\n使用 \`/project default set <路径或别名>\` 设置');
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async handleSwitchSession(chatId, messageId, userId, targetSessionId, chatType) {
|
|
886
|
+
if (!userConfig.enableManualSessionBind) {
|
|
887
|
+
await feishuClient.reply(messageId, '❌ 当前环境未开启“绑定已有会话”能力');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const normalizedSessionId = targetSessionId.trim();
|
|
891
|
+
if (!normalizedSessionId) {
|
|
892
|
+
await feishuClient.reply(messageId, '❌ 会话 ID 不能为空');
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const targetSession = await opencodeClient.findSessionAcrossProjects(normalizedSessionId);
|
|
896
|
+
if (!targetSession) {
|
|
897
|
+
await feishuClient.reply(messageId, `❌ 未找到会话: ${normalizedSessionId}`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const previousChatId = chatSessionStore.getChatId(normalizedSessionId);
|
|
901
|
+
const migrated = previousChatId && previousChatId !== chatId;
|
|
902
|
+
if (migrated && previousChatId) {
|
|
903
|
+
chatSessionStore.removeSession(previousChatId);
|
|
904
|
+
}
|
|
905
|
+
const title = targetSession.title && targetSession.title.trim().length > 0
|
|
906
|
+
? targetSession.title
|
|
907
|
+
: `手动绑定-${normalizedSessionId.slice(-4)}`;
|
|
908
|
+
chatSessionStore.setSession(chatId, normalizedSessionId, userId, title, { protectSessionDelete: true, chatType, resolvedDirectory: targetSession.directory });
|
|
909
|
+
const replyLines = [
|
|
910
|
+
'✅ 已切换到指定会话',
|
|
911
|
+
`ID: ${normalizedSessionId}`,
|
|
912
|
+
'🔒 自动清理不会删除该 OpenCode 会话。',
|
|
913
|
+
];
|
|
914
|
+
if (migrated) {
|
|
915
|
+
replyLines.push('🔁 该会话原绑定的旧群已自动解绑。');
|
|
916
|
+
}
|
|
917
|
+
await feishuClient.reply(messageId, replyLines.join('\n'));
|
|
918
|
+
}
|
|
919
|
+
async handleListSessions(chatId, messageId, listAll = false) {
|
|
920
|
+
let sessions = [];
|
|
921
|
+
let opencodeUnavailable = false;
|
|
922
|
+
// 获取当前群的工作目录,用于非全量模式下过滤
|
|
923
|
+
const sessionData = chatSessionStore.getSession(chatId);
|
|
924
|
+
const currentDirectory = sessionData?.resolvedDirectory || sessionData?.defaultDirectory;
|
|
925
|
+
try {
|
|
926
|
+
if (listAll) {
|
|
927
|
+
// 全量:聚合所有已知目录的会话
|
|
928
|
+
const storeKnownDirs = chatSessionStore.getKnownDirectories();
|
|
929
|
+
sessions = await opencodeClient.listAllSessions(storeKnownDirs);
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
// 默认:只查当前项目目录的会话
|
|
933
|
+
sessions = await opencodeClient.listSessions(currentDirectory ? { directory: currentDirectory } : undefined);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
opencodeUnavailable = true;
|
|
938
|
+
console.warn('[Command] 拉取 OpenCode 会话失败,回退到本地映射列表:', error);
|
|
939
|
+
}
|
|
940
|
+
const localBindings = new Map();
|
|
941
|
+
for (const boundChatId of chatSessionStore.getAllChatIds()) {
|
|
942
|
+
const binding = chatSessionStore.getSession(boundChatId);
|
|
943
|
+
if (!binding?.sessionId)
|
|
944
|
+
continue;
|
|
945
|
+
const bindingDir = binding.resolvedDirectory || binding.defaultDirectory;
|
|
946
|
+
// 非全量模式:跳过不属于当前目录的本地绑定
|
|
947
|
+
if (!listAll && bindingDir !== currentDirectory) {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const existing = localBindings.get(binding.sessionId);
|
|
951
|
+
if (existing) {
|
|
952
|
+
existing.chatIds.push(boundChatId);
|
|
953
|
+
if (!existing.title && binding.title)
|
|
954
|
+
existing.title = binding.title;
|
|
955
|
+
if (!existing.sessionDirectory && binding.sessionDirectory)
|
|
956
|
+
existing.sessionDirectory = binding.sessionDirectory;
|
|
957
|
+
if (!existing.projectName && binding.projectName)
|
|
958
|
+
existing.projectName = binding.projectName;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
localBindings.set(binding.sessionId, {
|
|
962
|
+
chatIds: [boundChatId],
|
|
963
|
+
title: binding.title,
|
|
964
|
+
...(binding.sessionDirectory ? { sessionDirectory: binding.sessionDirectory } : {}),
|
|
965
|
+
...(binding.projectName ? { projectName: binding.projectName } : {}),
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
const rows = [];
|
|
969
|
+
for (const session of sessions) {
|
|
970
|
+
const bindingInfo = localBindings.get(session.id);
|
|
971
|
+
const title = session.title && session.title.trim().length > 0 ? session.title.trim() : '未命名会话';
|
|
972
|
+
const directory = session.directory && session.directory.trim().length > 0 ? session.directory.trim() : '-';
|
|
973
|
+
const projectName = bindingInfo?.projectName;
|
|
974
|
+
const chatDetail = bindingInfo ? bindingInfo.chatIds.join(', ') : '无';
|
|
975
|
+
const status = bindingInfo ? 'OpenCode可用/已绑定' : 'OpenCode可用/未绑定';
|
|
976
|
+
rows.push({ directory, projectName, title, sessionId: session.id, chatDetail, status, statusRank: bindingInfo ? 0 : 1 });
|
|
977
|
+
localBindings.delete(session.id);
|
|
978
|
+
}
|
|
979
|
+
for (const [sessionId, bindingInfo] of Array.from(localBindings.entries())) {
|
|
980
|
+
const localTitle = bindingInfo.title && bindingInfo.title.trim().length > 0 ? bindingInfo.title.trim() : '本地绑定记录';
|
|
981
|
+
const localDirectory = bindingInfo.sessionDirectory && bindingInfo.sessionDirectory.trim().length > 0
|
|
982
|
+
? bindingInfo.sessionDirectory.trim()
|
|
983
|
+
: '-';
|
|
984
|
+
rows.push({
|
|
985
|
+
directory: localDirectory,
|
|
986
|
+
projectName: bindingInfo.projectName,
|
|
987
|
+
title: localTitle,
|
|
988
|
+
sessionId,
|
|
989
|
+
chatDetail: bindingInfo.chatIds.join(', '),
|
|
990
|
+
status: '仅本地映射(可能已失活)',
|
|
991
|
+
statusRank: 2,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
const normalizeDirectoryForSort = (directory) => {
|
|
995
|
+
const normalized = directory.trim();
|
|
996
|
+
return (!normalized || normalized === '-') ? '\uffff' : normalized;
|
|
997
|
+
};
|
|
998
|
+
rows.sort((left, right) => {
|
|
999
|
+
const directoryCompare = normalizeDirectoryForSort(left.directory).localeCompare(normalizeDirectoryForSort(right.directory), 'zh-Hans-CN');
|
|
1000
|
+
if (directoryCompare !== 0)
|
|
1001
|
+
return directoryCompare;
|
|
1002
|
+
if (left.statusRank !== right.statusRank)
|
|
1003
|
+
return left.statusRank - right.statusRank;
|
|
1004
|
+
const titleCompare = left.title.localeCompare(right.title, 'zh-Hans-CN');
|
|
1005
|
+
if (titleCompare !== 0)
|
|
1006
|
+
return titleCompare;
|
|
1007
|
+
return left.sessionId.localeCompare(right.sessionId, 'en');
|
|
1008
|
+
});
|
|
1009
|
+
const tableHeader = '工作区目录 | SessionID | OpenCode侧会话名称 | 绑定群明细 | 当前会话状态';
|
|
1010
|
+
const rowTexts = [];
|
|
1011
|
+
for (const row of rows) {
|
|
1012
|
+
const directoryDisplay = row.projectName ? `${row.directory} (${row.projectName})` : row.directory;
|
|
1013
|
+
rowTexts.push(`${directoryDisplay} | ${row.sessionId} | ${row.title} | ${row.chatDetail} | ${row.status}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (rowTexts.length === 0) {
|
|
1016
|
+
const emptyMessage = opencodeUnavailable
|
|
1017
|
+
? 'OpenCode 暂不可达,且当前无本地会话映射记录'
|
|
1018
|
+
: (listAll ? '当前无可用会话记录' : '当前工作目录无可用会话记录');
|
|
1019
|
+
await feishuClient.reply(messageId, emptyMessage);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const rowChunks = [];
|
|
1023
|
+
let currentRows = '';
|
|
1024
|
+
for (const row of rowTexts) {
|
|
1025
|
+
if ((tableHeader.length + currentRows.length + row.length + 2) > 3000 && currentRows.length > 0) {
|
|
1026
|
+
rowChunks.push(currentRows.trimEnd());
|
|
1027
|
+
currentRows = '';
|
|
1028
|
+
}
|
|
1029
|
+
currentRows += `${row}\n`;
|
|
1030
|
+
}
|
|
1031
|
+
if (currentRows.trim().length > 0)
|
|
1032
|
+
rowChunks.push(currentRows.trimEnd());
|
|
1033
|
+
const chunks = rowChunks.map(chunk => `${tableHeader}\n${chunk}`);
|
|
1034
|
+
if (chunks.length === 0) {
|
|
1035
|
+
await feishuClient.reply(messageId, `${tableHeader}\n(无数据)`);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const totalCount = rowTexts.length;
|
|
1039
|
+
const header = opencodeUnavailable
|
|
1040
|
+
? `📚 会话列表(总计 ${totalCount},OpenCode 暂不可达,仅展示本地映射)`
|
|
1041
|
+
: `📚 会话列表(总计 ${totalCount})`;
|
|
1042
|
+
const hint = listAll ? '' : '\n💡 提示:使用 `/sessions all` 查看所有项目的会话\n';
|
|
1043
|
+
await feishuClient.reply(messageId, `${header}${hint}\n${chunks[0]}`);
|
|
1044
|
+
for (let index = 1; index < chunks.length; index++) {
|
|
1045
|
+
await feishuClient.sendText(chatId, `📚 会话列表(续 ${index + 1}/${chunks.length})\n${chunks[index]}`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async handleModel(chatId, messageId, userId, chatType, modelName) {
|
|
1049
|
+
try {
|
|
1050
|
+
// 0. 确保会话存在
|
|
1051
|
+
let session = chatSessionStore.getSession(chatId);
|
|
1052
|
+
if (!session) {
|
|
1053
|
+
// 自动创建会话
|
|
1054
|
+
const title = `群聊会话-${chatId.slice(-4)}`;
|
|
1055
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
1056
|
+
const dirResult = DirectoryPolicy.resolve({ chatDefaultDirectory: chatDefault });
|
|
1057
|
+
const effectiveDir = dirResult.ok && dirResult.source !== 'server_default' ? dirResult.directory : undefined;
|
|
1058
|
+
const newSession = await opencodeClient.createSession(title, effectiveDir);
|
|
1059
|
+
if (newSession) {
|
|
1060
|
+
chatSessionStore.setSession(chatId, newSession.id, userId, title, { chatType, resolvedDirectory: newSession.directory });
|
|
1061
|
+
session = chatSessionStore.getSession(chatId);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
await feishuClient.reply(messageId, '❌ 无法创建会话以保存配置');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
// 1. 如果没有提供模型名称,显示当前状态
|
|
1069
|
+
if (!modelName) {
|
|
1070
|
+
const envDefaultModel = modelConfig.defaultProvider && modelConfig.defaultModel
|
|
1071
|
+
? `${modelConfig.defaultProvider}:${modelConfig.defaultModel}`
|
|
1072
|
+
: undefined;
|
|
1073
|
+
const currentModel = session?.preferredModel || envDefaultModel || '跟随 OpenCode 默认模型';
|
|
1074
|
+
await feishuClient.reply(messageId, `当前模型: ${currentModel}`);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const providersResult = await opencodeClient.getProviders();
|
|
1078
|
+
const providers = Array.isArray(providersResult.providers) ? providersResult.providers : [];
|
|
1079
|
+
const normalizedModelName = modelName.trim();
|
|
1080
|
+
const normalizedModelNameLower = normalizedModelName.toLowerCase();
|
|
1081
|
+
let matchedModel = null;
|
|
1082
|
+
for (const provider of providers) {
|
|
1083
|
+
const providerModels = this.extractProviderModels(provider);
|
|
1084
|
+
for (const candidate of providerModels) {
|
|
1085
|
+
const candidateValues = [
|
|
1086
|
+
`${candidate.providerId}:${candidate.modelId}`,
|
|
1087
|
+
`${candidate.providerId}/${candidate.modelId}`,
|
|
1088
|
+
candidate.modelId,
|
|
1089
|
+
candidate.modelName,
|
|
1090
|
+
].filter((item) => typeof item === 'string' && item.trim().length > 0);
|
|
1091
|
+
const isMatched = candidateValues.some(item => item.toLowerCase() === normalizedModelNameLower);
|
|
1092
|
+
if (!isMatched) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
matchedModel = candidate;
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
if (matchedModel) {
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (matchedModel) {
|
|
1103
|
+
// 3. 更新配置
|
|
1104
|
+
const newValue = `${matchedModel.providerId}:${matchedModel.modelId}`;
|
|
1105
|
+
chatSessionStore.updateConfig(chatId, { preferredModel: newValue });
|
|
1106
|
+
const lines = [`✅ 已切换模型: ${newValue}`];
|
|
1107
|
+
const reconciled = await this.reconcilePreferredEffort(chatId);
|
|
1108
|
+
if (reconciled.clearedEffort) {
|
|
1109
|
+
lines.push(`⚠️ 当前模型不支持强度 ${reconciled.clearedEffort},已回退为默认(可选: ${this.formatEffortList(reconciled.support.supportedEfforts)})`);
|
|
1110
|
+
}
|
|
1111
|
+
await feishuClient.reply(messageId, lines.join('\n'));
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
// 即使没找到匹配的,如果格式正确也允许强制设置(针对自定义或未列出的模型)
|
|
1115
|
+
if (normalizedModelName.includes(':') || normalizedModelName.includes('/')) {
|
|
1116
|
+
const separator = normalizedModelName.includes(':') ? ':' : '/';
|
|
1117
|
+
const [provider, model] = normalizedModelName.split(separator);
|
|
1118
|
+
const newValue = `${provider}:${model}`;
|
|
1119
|
+
chatSessionStore.updateConfig(chatId, { preferredModel: newValue });
|
|
1120
|
+
const currentEffort = chatSessionStore.getSession(chatId)?.preferredEffort;
|
|
1121
|
+
const warning = currentEffort
|
|
1122
|
+
? '\n⚠️ 当前模型不在列表中,无法校验已设置强度是否兼容。'
|
|
1123
|
+
: '';
|
|
1124
|
+
await feishuClient.reply(messageId, `⚠️ 未在列表中找到该模型,但已强制设置为: ${newValue}${warning}`);
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
await feishuClient.reply(messageId, `❌ 未找到模型 "${normalizedModelName}"\n请使用 /panel 查看可用列表`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
await feishuClient.reply(messageId, `❌ 设置模型失败: ${error}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async handleEffort(chatId, messageId, userId, chatType, command) {
|
|
1136
|
+
try {
|
|
1137
|
+
// 0. 确保会话存在
|
|
1138
|
+
let session = chatSessionStore.getSession(chatId);
|
|
1139
|
+
if (!session) {
|
|
1140
|
+
const title = `群聊会话-${chatId.slice(-4)}`;
|
|
1141
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
1142
|
+
const dirResult = DirectoryPolicy.resolve({ chatDefaultDirectory: chatDefault });
|
|
1143
|
+
const effectiveDir = dirResult.ok && dirResult.source !== 'server_default' ? dirResult.directory : undefined;
|
|
1144
|
+
const newSession = await opencodeClient.createSession(title, effectiveDir);
|
|
1145
|
+
if (newSession) {
|
|
1146
|
+
chatSessionStore.setSession(chatId, newSession.id, userId, title, { chatType, resolvedDirectory: newSession.directory });
|
|
1147
|
+
session = chatSessionStore.getSession(chatId);
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
await feishuClient.reply(messageId, '❌ 无法创建会话以保存强度配置');
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const support = await this.getEffortSupportInfo(chatId);
|
|
1155
|
+
const currentEffort = session?.preferredEffort;
|
|
1156
|
+
const modelLabel = this.formatModelLabel(support.model);
|
|
1157
|
+
const supportText = this.formatEffortList(support.supportedEfforts);
|
|
1158
|
+
if (command.effortReset) {
|
|
1159
|
+
chatSessionStore.updateConfig(chatId, { preferredEffort: undefined });
|
|
1160
|
+
await feishuClient.reply(messageId, [
|
|
1161
|
+
currentEffort ? `✅ 已清除会话强度(原为: ${currentEffort})` : '✅ 当前会话强度已是默认(自动)',
|
|
1162
|
+
`当前模型: ${modelLabel}`,
|
|
1163
|
+
`可选强度: ${supportText}`,
|
|
1164
|
+
].join('\n'));
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (command.effortRaw && !command.effortLevel) {
|
|
1168
|
+
await feishuClient.reply(messageId, `❌ 不支持的强度: ${command.effortRaw}\n${EFFORT_USAGE_TEXT}\n可选强度: ${supportText}`);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (!command.effortLevel) {
|
|
1172
|
+
await feishuClient.reply(messageId, [
|
|
1173
|
+
`当前强度: ${currentEffort || '默认(自动)'}`,
|
|
1174
|
+
`当前模型: ${modelLabel}`,
|
|
1175
|
+
`可选强度: ${supportText}`,
|
|
1176
|
+
'临时覆盖: 在消息开头使用 #low / #high / #max / #xhigh',
|
|
1177
|
+
].join('\n'));
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const requested = command.effortLevel;
|
|
1181
|
+
if (!support.modelMatched) {
|
|
1182
|
+
chatSessionStore.updateConfig(chatId, { preferredEffort: requested });
|
|
1183
|
+
await feishuClient.reply(messageId, `⚠️ 已设置会话强度: ${requested}\n当前模型: ${modelLabel}\n无法识别当前模型能力,暂无法校验兼容性。`);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (!support.supportedEfforts.includes(requested)) {
|
|
1187
|
+
await feishuClient.reply(messageId, `❌ 当前模型不支持强度 ${requested}\n当前模型: ${modelLabel}\n可选强度: ${supportText}`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
chatSessionStore.updateConfig(chatId, { preferredEffort: requested });
|
|
1191
|
+
await feishuClient.reply(messageId, `✅ 已设置会话强度: ${requested}\n当前模型: ${modelLabel}`);
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
await feishuClient.reply(messageId, `❌ 设置强度失败: ${error}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
getVisibleAgents(agents) {
|
|
1198
|
+
return agents.filter(agent => agent.hidden !== true && !INTERNAL_HIDDEN_AGENT_NAMES.has(agent.name));
|
|
1199
|
+
}
|
|
1200
|
+
getAgentModePrefix(agent) {
|
|
1201
|
+
return agent.mode === 'subagent' ? '(子)' : '(主)';
|
|
1202
|
+
}
|
|
1203
|
+
getBuiltinAgentTranslation(agent) {
|
|
1204
|
+
const normalizedName = normalizeAgentText(agent.name);
|
|
1205
|
+
const normalizedDescription = normalizeAgentText(typeof agent.description === 'string' ? agent.description : '');
|
|
1206
|
+
for (const rule of BUILTIN_AGENT_TRANSLATION_RULES) {
|
|
1207
|
+
const byName = rule.names.includes(normalizedName);
|
|
1208
|
+
const byDescription = normalizedDescription.length > 0
|
|
1209
|
+
&& normalizedDescription.startsWith(rule.descriptionStartsWith);
|
|
1210
|
+
if (byName || byDescription) {
|
|
1211
|
+
return rule.translated;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return undefined;
|
|
1215
|
+
}
|
|
1216
|
+
getAgentDisplayName(agent) {
|
|
1217
|
+
const translatedBuiltinName = this.getBuiltinAgentTranslation(agent);
|
|
1218
|
+
if (translatedBuiltinName) {
|
|
1219
|
+
return translatedBuiltinName;
|
|
1220
|
+
}
|
|
1221
|
+
const description = typeof agent.description === 'string' ? agent.description.trim() : '';
|
|
1222
|
+
return description || agent.name;
|
|
1223
|
+
}
|
|
1224
|
+
getAgentDisplayText(agent) {
|
|
1225
|
+
return `${this.getAgentModePrefix(agent)} ${this.getAgentDisplayName(agent)}`;
|
|
1226
|
+
}
|
|
1227
|
+
resolveAgentByInput(agents, rawInput) {
|
|
1228
|
+
const input = rawInput.trim();
|
|
1229
|
+
if (!input)
|
|
1230
|
+
return undefined;
|
|
1231
|
+
const lowered = input.toLowerCase();
|
|
1232
|
+
const byName = agents.find(agent => agent.name.toLowerCase() === lowered);
|
|
1233
|
+
if (byName)
|
|
1234
|
+
return byName;
|
|
1235
|
+
const byDescription = agents.find(agent => {
|
|
1236
|
+
const description = typeof agent.description === 'string' ? agent.description.trim().toLowerCase() : '';
|
|
1237
|
+
return description.length > 0 && description === lowered;
|
|
1238
|
+
});
|
|
1239
|
+
if (byDescription)
|
|
1240
|
+
return byDescription;
|
|
1241
|
+
const byDisplayName = agents.find(agent => this.getAgentDisplayName(agent).toLowerCase() === lowered);
|
|
1242
|
+
if (byDisplayName)
|
|
1243
|
+
return byDisplayName;
|
|
1244
|
+
return agents.find(agent => this.getAgentDisplayText(agent).toLowerCase() === lowered);
|
|
1245
|
+
}
|
|
1246
|
+
getCurrentRoleDisplay(currentAgentName, agents) {
|
|
1247
|
+
if (!currentAgentName)
|
|
1248
|
+
return '默认角色';
|
|
1249
|
+
const found = agents.find(agent => agent.name === currentAgentName);
|
|
1250
|
+
if (found)
|
|
1251
|
+
return this.getAgentDisplayText(found);
|
|
1252
|
+
return currentAgentName;
|
|
1253
|
+
}
|
|
1254
|
+
getRuntimeDefaultAgentName(config) {
|
|
1255
|
+
const record = config;
|
|
1256
|
+
const rawValue = record.default_agent ?? record.defaultAgent;
|
|
1257
|
+
if (typeof rawValue !== 'string') {
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
1260
|
+
const normalized = rawValue.trim();
|
|
1261
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
1262
|
+
}
|
|
1263
|
+
findAgentByNameInsensitive(agents, name) {
|
|
1264
|
+
const target = name.trim().toLowerCase();
|
|
1265
|
+
if (!target)
|
|
1266
|
+
return undefined;
|
|
1267
|
+
return agents.find(agent => agent.name.toLowerCase() === target);
|
|
1268
|
+
}
|
|
1269
|
+
shouldHideDefaultRoleOption(defaultAgentName, agents) {
|
|
1270
|
+
const buildAgent = this.findAgentByNameInsensitive(agents, 'build');
|
|
1271
|
+
if (!buildAgent) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
if (!defaultAgentName) {
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
return defaultAgentName.trim().toLowerCase() === 'build';
|
|
1278
|
+
}
|
|
1279
|
+
getDefaultRoleDisplay(defaultAgentName, agents) {
|
|
1280
|
+
if (defaultAgentName) {
|
|
1281
|
+
const defaultAgent = this.findAgentByNameInsensitive(agents, defaultAgentName);
|
|
1282
|
+
if (defaultAgent) {
|
|
1283
|
+
return this.getAgentDisplayText(defaultAgent);
|
|
1284
|
+
}
|
|
1285
|
+
return defaultAgentName;
|
|
1286
|
+
}
|
|
1287
|
+
const buildAgent = this.findAgentByNameInsensitive(agents, 'build');
|
|
1288
|
+
if (buildAgent) {
|
|
1289
|
+
return this.getAgentDisplayText(buildAgent);
|
|
1290
|
+
}
|
|
1291
|
+
return '默认角色';
|
|
1292
|
+
}
|
|
1293
|
+
getRoleAgentMap(config) {
|
|
1294
|
+
if (!config.agent || typeof config.agent !== 'object') {
|
|
1295
|
+
return {};
|
|
1296
|
+
}
|
|
1297
|
+
return config.agent;
|
|
1298
|
+
}
|
|
1299
|
+
async handleRoleCreate(chatId, messageId, userId, chatType, roleSpec) {
|
|
1300
|
+
const parsed = parseRoleCreateSpec(roleSpec);
|
|
1301
|
+
if (!parsed.ok) {
|
|
1302
|
+
await feishuClient.reply(messageId, `❌ 创建角色失败\n${parsed.message}`);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
let session = chatSessionStore.getSession(chatId);
|
|
1306
|
+
if (!session) {
|
|
1307
|
+
const title = `群聊会话-${chatId.slice(-4)}`;
|
|
1308
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
1309
|
+
const dirResult = DirectoryPolicy.resolve({ chatDefaultDirectory: chatDefault });
|
|
1310
|
+
const effectiveDir = dirResult.ok && dirResult.source !== 'server_default' ? dirResult.directory : undefined;
|
|
1311
|
+
const newSession = await opencodeClient.createSession(title, effectiveDir);
|
|
1312
|
+
if (!newSession) {
|
|
1313
|
+
await feishuClient.reply(messageId, '❌ 无法创建会话以保存角色设置');
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
chatSessionStore.setSession(chatId, newSession.id, userId, title, { chatType, resolvedDirectory: newSession.directory });
|
|
1317
|
+
session = chatSessionStore.getSession(chatId);
|
|
1318
|
+
}
|
|
1319
|
+
const payload = parsed.payload;
|
|
1320
|
+
const [agents, config] = await Promise.all([
|
|
1321
|
+
opencodeClient.getAgents(),
|
|
1322
|
+
opencodeClient.getConfig(),
|
|
1323
|
+
]);
|
|
1324
|
+
const roleAgentMap = this.getRoleAgentMap(config);
|
|
1325
|
+
const existingConfig = roleAgentMap[payload.name];
|
|
1326
|
+
const nameConflict = agents.find(agent => agent.name.toLowerCase() === payload.name.toLowerCase());
|
|
1327
|
+
if (nameConflict && !existingConfig) {
|
|
1328
|
+
await feishuClient.reply(messageId, `❌ 角色名称已被占用: ${payload.name}\n请更换一个名称后重试。`);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const nextAgentConfig = {
|
|
1332
|
+
description: payload.description,
|
|
1333
|
+
mode: payload.mode,
|
|
1334
|
+
...(payload.prompt ? { prompt: payload.prompt } : {}),
|
|
1335
|
+
...(payload.tools ? { tools: payload.tools } : {}),
|
|
1336
|
+
};
|
|
1337
|
+
const nextConfig = {
|
|
1338
|
+
...config,
|
|
1339
|
+
agent: {
|
|
1340
|
+
...roleAgentMap,
|
|
1341
|
+
[payload.name]: nextAgentConfig,
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
1344
|
+
const updated = await opencodeClient.updateConfig(nextConfig);
|
|
1345
|
+
if (!updated) {
|
|
1346
|
+
await feishuClient.reply(messageId, '❌ 创建角色失败:写入 OpenCode 配置失败');
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (session) {
|
|
1350
|
+
chatSessionStore.updateConfig(chatId, { preferredAgent: payload.name });
|
|
1351
|
+
}
|
|
1352
|
+
const actionText = existingConfig ? '已更新' : '已创建';
|
|
1353
|
+
const modeText = payload.mode === 'subagent' ? '子角色' : '主角色';
|
|
1354
|
+
await feishuClient.reply(messageId, `✅ ${actionText}角色: ${payload.name}\n类型: ${modeText}\n当前群已切换到该角色。\n若 /panel 未立即显示新角色,请重启 OpenCode。`);
|
|
1355
|
+
}
|
|
1356
|
+
async handleAgent(chatId, messageId, userId, chatType, agentName) {
|
|
1357
|
+
try {
|
|
1358
|
+
// 0. 确保会话存在
|
|
1359
|
+
let session = chatSessionStore.getSession(chatId);
|
|
1360
|
+
if (!session) {
|
|
1361
|
+
// 自动创建会话
|
|
1362
|
+
const title = `群聊会话-${chatId.slice(-4)}`;
|
|
1363
|
+
const chatDefault = chatSessionStore.getSession(chatId)?.defaultDirectory;
|
|
1364
|
+
const dirResult = DirectoryPolicy.resolve({ chatDefaultDirectory: chatDefault });
|
|
1365
|
+
const effectiveDir = dirResult.ok && dirResult.source !== 'server_default' ? dirResult.directory : undefined;
|
|
1366
|
+
const newSession = await opencodeClient.createSession(title, effectiveDir);
|
|
1367
|
+
if (newSession) {
|
|
1368
|
+
chatSessionStore.setSession(chatId, newSession.id, userId, title, { chatType, resolvedDirectory: newSession.directory });
|
|
1369
|
+
session = chatSessionStore.getSession(chatId);
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
await feishuClient.reply(messageId, '❌ 无法创建会话以保存配置');
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
const visibleAgents = this.getVisibleAgents(await opencodeClient.getAgents());
|
|
1377
|
+
const currentAgent = session?.preferredAgent;
|
|
1378
|
+
if (!agentName) {
|
|
1379
|
+
await feishuClient.reply(messageId, `当前角色: ${this.getCurrentRoleDisplay(currentAgent, visibleAgents)}`);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
// 特殊值处理
|
|
1383
|
+
if (agentName === 'none' || agentName === 'off' || agentName === 'default') {
|
|
1384
|
+
chatSessionStore.updateConfig(chatId, { preferredAgent: undefined });
|
|
1385
|
+
await feishuClient.reply(messageId, '✅ 已切换为默认角色');
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const matched = this.resolveAgentByInput(visibleAgents, agentName);
|
|
1389
|
+
if (!matched) {
|
|
1390
|
+
await feishuClient.reply(messageId, '❌ 未找到该角色\n请使用 /panel 查看可用角色');
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
chatSessionStore.updateConfig(chatId, { preferredAgent: matched.name });
|
|
1394
|
+
await feishuClient.reply(messageId, `✅ 已切换角色: ${this.getAgentDisplayText(matched)}`);
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
await feishuClient.reply(messageId, `❌ 设置角色失败: ${error}`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async buildPanelCard(chatId, chatType = 'group') {
|
|
1401
|
+
const session = chatSessionStore.getSession(chatId);
|
|
1402
|
+
const currentModel = session?.preferredModel || '默认';
|
|
1403
|
+
const currentEffort = session?.preferredEffort || '默认(自动)';
|
|
1404
|
+
// 获取列表供卡片使用
|
|
1405
|
+
const [{ providers }, allAgents, runtimeConfig] = await Promise.all([
|
|
1406
|
+
opencodeClient.getProviders(),
|
|
1407
|
+
opencodeClient.getAgents(),
|
|
1408
|
+
opencodeClient.getConfig(),
|
|
1409
|
+
]);
|
|
1410
|
+
const visibleAgents = this.getVisibleAgents(allAgents);
|
|
1411
|
+
const defaultAgentName = this.getRuntimeDefaultAgentName(runtimeConfig);
|
|
1412
|
+
const hideDefaultRoleOption = this.shouldHideDefaultRoleOption(defaultAgentName, visibleAgents);
|
|
1413
|
+
const currentAgent = session?.preferredAgent
|
|
1414
|
+
? this.getCurrentRoleDisplay(session.preferredAgent, visibleAgents)
|
|
1415
|
+
: this.getDefaultRoleDisplay(defaultAgentName, visibleAgents);
|
|
1416
|
+
const modelOptions = [];
|
|
1417
|
+
const modelOptionValues = new Set();
|
|
1418
|
+
const safeProviders = Array.isArray(providers) ? providers : [];
|
|
1419
|
+
for (const p of safeProviders) {
|
|
1420
|
+
// 安全获取 models,兼容数组和对象
|
|
1421
|
+
const modelsRaw = p.models;
|
|
1422
|
+
const models = Array.isArray(modelsRaw)
|
|
1423
|
+
? modelsRaw
|
|
1424
|
+
: (modelsRaw && typeof modelsRaw === 'object' ? Object.values(modelsRaw) : []);
|
|
1425
|
+
for (const m of models) {
|
|
1426
|
+
const modelId = m.id || m.modelID || m.name;
|
|
1427
|
+
const modelName = m.name || modelId;
|
|
1428
|
+
const providerId = p.id || p.providerID;
|
|
1429
|
+
if (modelId && providerId) {
|
|
1430
|
+
const label = `[${p.name || providerId}] ${modelName}`;
|
|
1431
|
+
const value = `${providerId}:${modelId}`;
|
|
1432
|
+
if (!modelOptionValues.has(value)) {
|
|
1433
|
+
modelOptionValues.add(value);
|
|
1434
|
+
modelOptions.push({ label, value });
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const selectedModel = session?.preferredModel || '';
|
|
1440
|
+
let panelModelOptions = modelOptions.slice(0, PANEL_MODEL_OPTION_LIMIT);
|
|
1441
|
+
if (selectedModel.includes(':') && panelModelOptions.every(item => item.value !== selectedModel)) {
|
|
1442
|
+
const matched = modelOptions.find(item => item.value === selectedModel);
|
|
1443
|
+
if (matched) {
|
|
1444
|
+
if (panelModelOptions.length >= PANEL_MODEL_OPTION_LIMIT) {
|
|
1445
|
+
panelModelOptions = [...panelModelOptions.slice(0, PANEL_MODEL_OPTION_LIMIT - 1), matched];
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
panelModelOptions = [...panelModelOptions, matched];
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const mappedAgentOptions = visibleAgents.map(agent => ({
|
|
1453
|
+
label: this.getAgentDisplayText(agent),
|
|
1454
|
+
value: agent.name,
|
|
1455
|
+
}));
|
|
1456
|
+
const agentOptions = hideDefaultRoleOption
|
|
1457
|
+
? mappedAgentOptions
|
|
1458
|
+
: [{ label: '(主)默认角色', value: 'none' }, ...mappedAgentOptions];
|
|
1459
|
+
return buildControlCard({
|
|
1460
|
+
conversationKey: `chat:${chatId}`,
|
|
1461
|
+
chatId,
|
|
1462
|
+
chatType,
|
|
1463
|
+
currentModel,
|
|
1464
|
+
currentAgent,
|
|
1465
|
+
currentEffort,
|
|
1466
|
+
models: panelModelOptions,
|
|
1467
|
+
agents: agentOptions,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
async pushPanelCard(chatId, chatType = 'group') {
|
|
1471
|
+
const card = await this.buildPanelCard(chatId, chatType);
|
|
1472
|
+
await feishuClient.sendCard(chatId, card);
|
|
1473
|
+
}
|
|
1474
|
+
async handlePanel(chatId, messageId, chatType) {
|
|
1475
|
+
const card = await this.buildPanelCard(chatId, chatType);
|
|
1476
|
+
if (messageId) {
|
|
1477
|
+
await feishuClient.replyCard(messageId, card);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
await feishuClient.sendCard(chatId, card);
|
|
1481
|
+
}
|
|
1482
|
+
async handlePassthroughCommand(chatId, messageId, commandName, commandArgs, commandPrefix = '/') {
|
|
1483
|
+
const sessionId = chatSessionStore.getSessionId(chatId);
|
|
1484
|
+
if (!sessionId) {
|
|
1485
|
+
await feishuClient.reply(messageId, '❌ 当前没有活跃的会话,请先发送消息建立会话');
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const shownCommand = commandPrefix === '!' ? `!${commandArgs}` : `/${commandName} ${commandArgs}`.trim();
|
|
1489
|
+
console.log(`[Command] 透传命令到 OpenCode: ${shownCommand}`);
|
|
1490
|
+
try {
|
|
1491
|
+
if (commandPrefix === '!') {
|
|
1492
|
+
const shellCommand = commandArgs.trim();
|
|
1493
|
+
if (!shellCommand) {
|
|
1494
|
+
await feishuClient.reply(messageId, '❌ 用法: !<shell命令>,例如 !ls');
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const shellAgent = await this.resolveShellAgent(chatId);
|
|
1498
|
+
const shellSessionData = chatSessionStore.getSession(chatId);
|
|
1499
|
+
const shellDirectory = shellSessionData?.sessionDirectory;
|
|
1500
|
+
const result = await opencodeClient.sendShellCommand(sessionId, shellCommand, shellAgent, shellDirectory ? { directory: shellDirectory } : undefined);
|
|
1501
|
+
const output = this.formatOutput(result.parts);
|
|
1502
|
+
if (output !== '(无输出)') {
|
|
1503
|
+
await feishuClient.reply(messageId, output);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
await feishuClient.reply(messageId, `✅ Shell 命令执行完成: !${shellCommand}`);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
// 使用专门的 sendCommand 方法(传递工作目录以切换 Instance 上下文)
|
|
1510
|
+
const cmdSessionData = chatSessionStore.getSession(chatId);
|
|
1511
|
+
const cmdDirectory = cmdSessionData?.sessionDirectory;
|
|
1512
|
+
const result = await opencodeClient.sendCommand(sessionId, commandName, commandArgs, cmdDirectory ? { directory: cmdDirectory } : undefined);
|
|
1513
|
+
// 处理返回结果
|
|
1514
|
+
if (result && result.parts) {
|
|
1515
|
+
const output = this.formatOutput(result.parts);
|
|
1516
|
+
await feishuClient.reply(messageId, output);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
await feishuClient.reply(messageId, `✅ 命令执行完成: ${shownCommand}`);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
console.error('[Command] 透传命令失败:', error);
|
|
1524
|
+
await feishuClient.reply(messageId, `❌ 命令执行失败: ${error}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
formatOutput(parts) {
|
|
1528
|
+
if (!parts || !Array.isArray(parts))
|
|
1529
|
+
return '(无输出)';
|
|
1530
|
+
const output = [];
|
|
1531
|
+
for (const part of parts) {
|
|
1532
|
+
const p = part;
|
|
1533
|
+
if (p.type === 'text' && typeof p.text === 'string') {
|
|
1534
|
+
const text = p.text.trim();
|
|
1535
|
+
if (text) {
|
|
1536
|
+
output.push(text);
|
|
1537
|
+
}
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (p.type !== 'tool') {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const state = p.state;
|
|
1544
|
+
if (!state || typeof state !== 'object') {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
const toolState = state;
|
|
1548
|
+
if (typeof toolState.output === 'string' && toolState.output.trim()) {
|
|
1549
|
+
output.push(toolState.output.trim());
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const metadata = toolState.metadata;
|
|
1553
|
+
if (metadata && typeof metadata === 'object') {
|
|
1554
|
+
const metadataRecord = metadata;
|
|
1555
|
+
if (typeof metadataRecord.output === 'string' && metadataRecord.output.trim()) {
|
|
1556
|
+
output.push(metadataRecord.output.trim());
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (typeof toolState.error === 'string' && toolState.error.trim()) {
|
|
1561
|
+
output.push(`工具执行失败: ${toolState.error.trim()}`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
const merged = output.join('\n\n').trim();
|
|
1565
|
+
if (!merged) {
|
|
1566
|
+
return '(无输出)';
|
|
1567
|
+
}
|
|
1568
|
+
const maxLength = 3500;
|
|
1569
|
+
if (merged.length <= maxLength) {
|
|
1570
|
+
return merged;
|
|
1571
|
+
}
|
|
1572
|
+
return `${merged.slice(0, maxLength)}\n\n...(输出过长,已截断)`;
|
|
1573
|
+
}
|
|
1574
|
+
async handleClearFreeSession(chatId, messageId, targetSessionId) {
|
|
1575
|
+
const normalizedTargetSessionId = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
|
|
1576
|
+
if (normalizedTargetSessionId) {
|
|
1577
|
+
// 删除指定会话
|
|
1578
|
+
await feishuClient.reply(messageId, `🧹 正在删除指定会话: ${normalizedTargetSessionId} ...`);
|
|
1579
|
+
const targetSession = await opencodeClient.findSessionAcrossProjects(normalizedTargetSessionId);
|
|
1580
|
+
if (!targetSession) {
|
|
1581
|
+
await feishuClient.reply(messageId, `❌ 未找到会话: ${normalizedTargetSessionId}`);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const boundChatIds = [];
|
|
1585
|
+
let protectedBindingCount = 0;
|
|
1586
|
+
for (const boundChatId of chatSessionStore.getAllChatIds()) {
|
|
1587
|
+
const binding = chatSessionStore.getSession(boundChatId);
|
|
1588
|
+
if (binding?.sessionId !== normalizedTargetSessionId) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
boundChatIds.push(boundChatId);
|
|
1592
|
+
if (chatSessionStore.isSessionDeleteProtected(boundChatId)) {
|
|
1593
|
+
protectedBindingCount += 1;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const deleted = await opencodeClient.deleteSession(normalizedTargetSessionId, {
|
|
1597
|
+
directory: targetSession.directory,
|
|
1598
|
+
});
|
|
1599
|
+
if (!deleted) {
|
|
1600
|
+
await feishuClient.reply(messageId, `❌ 删除会话失败: ${normalizedTargetSessionId}`);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
for (const boundChatId of boundChatIds) {
|
|
1604
|
+
chatSessionStore.removeSession(boundChatId);
|
|
1605
|
+
}
|
|
1606
|
+
const cronCleanup = cleanupRuntimeCronJobsBySessionId(getRuntimeCronManager(), normalizedTargetSessionId);
|
|
1607
|
+
const lines = [
|
|
1608
|
+
'✅ 指定会话已删除',
|
|
1609
|
+
`- 工作区目录: ${targetSession.directory || '-'}`,
|
|
1610
|
+
`- 会话ID: ${normalizedTargetSessionId}`,
|
|
1611
|
+
`- 清理本地映射: ${boundChatIds.length} 个群`,
|
|
1612
|
+
`- 清理绑定 Cron: ${cronCleanup.removedJobIds.length} 个`,
|
|
1613
|
+
];
|
|
1614
|
+
if (protectedBindingCount > 0) {
|
|
1615
|
+
lines.push(`- 删除保护映射: ${protectedBindingCount} 个(手动删除已强制执行)`);
|
|
1616
|
+
}
|
|
1617
|
+
await feishuClient.reply(messageId, lines.join('\n'));
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
// 无 targetSessionId:扫描并清理所有空闲群聊
|
|
1621
|
+
await feishuClient.reply(messageId, '🧹 正在扫描并清理无效群聊...');
|
|
1622
|
+
const stats = await lifecycleHandler.runCleanupScan();
|
|
1623
|
+
const cronCleanup = await scanAndCleanupOrphanRuntimeCronJobs(getRuntimeCronManager(), {
|
|
1624
|
+
hasConversationBinding: (platform, conversationId, sessionId) => {
|
|
1625
|
+
const binding = chatSessionStore.getSessionByConversation(platform, conversationId);
|
|
1626
|
+
if (!binding) {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
return !sessionId || binding.sessionId === sessionId;
|
|
1630
|
+
},
|
|
1631
|
+
getSessionStatus: async (sessionId, directory) => {
|
|
1632
|
+
try {
|
|
1633
|
+
const session = await opencodeClient.getSessionById(sessionId, directory ? { directory } : undefined);
|
|
1634
|
+
return session ? 'exists' : 'missing';
|
|
1635
|
+
}
|
|
1636
|
+
catch {
|
|
1637
|
+
return 'unknown';
|
|
1638
|
+
}
|
|
1639
|
+
},
|
|
1640
|
+
});
|
|
1641
|
+
await feishuClient.reply(messageId, `✅ 清理完成\n- 扫描群聊: ${stats.scannedChats} 个\n- 解散群聊: ${stats.disbandedChats} 个\n- 清理会话: ${stats.deletedSessions} 个\n- 跳过删除(受保护): ${stats.skippedProtectedSessions} 个\n- 移除孤儿映射: ${stats.removedOrphanMappings} 个\n- 自动清理 Cron: ${stats.removedCronJobs} 个\n- 手动清理僵尸 Cron: ${cronCleanup.removedJobIds.length} 个`);
|
|
1642
|
+
}
|
|
1643
|
+
async handleSendFile(chatId, messageId, filePath) {
|
|
1644
|
+
const trimmed = filePath.trim();
|
|
1645
|
+
if (!trimmed) {
|
|
1646
|
+
await feishuClient.reply(messageId, '请提供文件的绝对路径,例如:\n• /send /path/to/file.png\n• /send C:\\Users\\你\\Desktop\\图片.jpg');
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const result = await sendFileToFeishu({ filePath: trimmed, chatId });
|
|
1650
|
+
if (result.success) {
|
|
1651
|
+
await feishuClient.reply(messageId, `✅ 已发送${result.sendType === 'image' ? '图片' : '文件'}: ${result.fileName}`);
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
await feishuClient.reply(messageId, `❌ ${result.error}`);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// 公开以供外部调用(如消息撤回事件)
|
|
1658
|
+
async handleUndo(chatId, triggerMessageId) {
|
|
1659
|
+
// 0. 删除触发 undo 的命令消息(如果存在)
|
|
1660
|
+
if (triggerMessageId) {
|
|
1661
|
+
try {
|
|
1662
|
+
await feishuClient.deleteMessage(triggerMessageId);
|
|
1663
|
+
}
|
|
1664
|
+
catch (e) {
|
|
1665
|
+
// ignore (might not have permission or already deleted)
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
const session = chatSessionStore.getSession(chatId);
|
|
1669
|
+
if (!session || !session.sessionId) {
|
|
1670
|
+
// 撤回事件触发时,如果会话已失效则静默返回,避免在不可用群里再次报错。
|
|
1671
|
+
if (!triggerMessageId) {
|
|
1672
|
+
console.warn(`[Undo] 跳过撤回: chat=${chatId} 无活跃会话`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
const msg = await feishuClient.sendText(chatId, '❌ 当前没有活跃的会话');
|
|
1676
|
+
setTimeout(() => msg && feishuClient.deleteMessage(msg), 5000);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
console.log(`[Undo] 尝试撤回会话 ${session.sessionId} 的最后一次交互`);
|
|
1680
|
+
// 递归撤回函数
|
|
1681
|
+
const performUndo = async (skipOpenCodeRevert = false) => {
|
|
1682
|
+
// 1. Pop interaction
|
|
1683
|
+
const lastInteraction = chatSessionStore.popInteraction(chatId);
|
|
1684
|
+
if (!lastInteraction) {
|
|
1685
|
+
return false; // No history
|
|
1686
|
+
}
|
|
1687
|
+
// 2. Revert in OpenCode
|
|
1688
|
+
if (!skipOpenCodeRevert) {
|
|
1689
|
+
let targetRevertId = '';
|
|
1690
|
+
try {
|
|
1691
|
+
const messages = await opencodeClient.getSessionMessages(session.sessionId);
|
|
1692
|
+
// Find the AI message
|
|
1693
|
+
// For question_answer type, openCodeMsgId is empty, so this will be -1
|
|
1694
|
+
const aiMsgIndex = messages.findIndex(m => m.info.id === lastInteraction.openCodeMsgId);
|
|
1695
|
+
if (aiMsgIndex !== -1) {
|
|
1696
|
+
// We want to remove the User Message and the AI Message.
|
|
1697
|
+
// To remove a message in OpenCode (revert), we pass the ID of the message to remove.
|
|
1698
|
+
// Revert removes the target message and all subsequent messages.
|
|
1699
|
+
// So we target the User Message (aiMsgIndex - 1).
|
|
1700
|
+
if (aiMsgIndex >= 1) {
|
|
1701
|
+
targetRevertId = messages[aiMsgIndex - 1].info.id;
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
// AI message is at index 0? User message missing?
|
|
1705
|
+
// Fallback to removing AI message itself.
|
|
1706
|
+
targetRevertId = messages[aiMsgIndex].info.id;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
// Fallback: usually for question_answer or if ID not found.
|
|
1711
|
+
// Structure: [..., User/Question, Answer].
|
|
1712
|
+
// We want to remove both.
|
|
1713
|
+
// Target User/Question (index N-2).
|
|
1714
|
+
if (messages.length >= 2) {
|
|
1715
|
+
targetRevertId = messages[messages.length - 2].info.id;
|
|
1716
|
+
}
|
|
1717
|
+
else if (messages.length === 1) {
|
|
1718
|
+
targetRevertId = messages[0].info.id;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
catch (e) {
|
|
1723
|
+
console.warn('[Undo] Failed to fetch messages for revert calculation', e);
|
|
1724
|
+
}
|
|
1725
|
+
if (targetRevertId) {
|
|
1726
|
+
await opencodeClient.revertMessage(session.sessionId, targetRevertId);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
// 3. Delete Feishu messages
|
|
1730
|
+
// Delete AI replies
|
|
1731
|
+
for (const msgId of lastInteraction.botFeishuMsgIds) {
|
|
1732
|
+
try {
|
|
1733
|
+
await feishuClient.deleteMessage(msgId);
|
|
1734
|
+
}
|
|
1735
|
+
catch (e) { }
|
|
1736
|
+
}
|
|
1737
|
+
// Delete User message
|
|
1738
|
+
if (lastInteraction.userFeishuMsgId) {
|
|
1739
|
+
try {
|
|
1740
|
+
await feishuClient.deleteMessage(lastInteraction.userFeishuMsgId);
|
|
1741
|
+
}
|
|
1742
|
+
catch (e) { }
|
|
1743
|
+
}
|
|
1744
|
+
// 4. Recursive check for question answer
|
|
1745
|
+
if (lastInteraction.type === 'question_answer') {
|
|
1746
|
+
// Question 回答通常会在本地历史里对应若干 question_prompt 卡片。
|
|
1747
|
+
// 这里仅清理 question_prompt,避免误删上一轮 normal 交互。
|
|
1748
|
+
while (chatSessionStore.getLastInteraction(chatId)?.type === 'question_prompt') {
|
|
1749
|
+
await performUndo(true);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return true;
|
|
1753
|
+
};
|
|
1754
|
+
try {
|
|
1755
|
+
const success = await performUndo();
|
|
1756
|
+
if (success) {
|
|
1757
|
+
const msg = await feishuClient.sendText(chatId, '✅ 已撤回上一轮对话');
|
|
1758
|
+
setTimeout(() => msg && feishuClient.deleteMessage(msg), 3000);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
const msg = await feishuClient.sendText(chatId, '⚠️ 没有可撤回的消息');
|
|
1762
|
+
setTimeout(() => msg && feishuClient.deleteMessage(msg), 3000);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
console.error('[Undo] 执行失败:', error);
|
|
1767
|
+
const msg = await feishuClient.sendText(chatId, `❌ 撤回出错: ${error}`);
|
|
1768
|
+
setTimeout(() => msg && feishuClient.deleteMessage(msg), 5000);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
export const commandHandler = new CommandHandler();
|
|
1773
|
+
//# sourceMappingURL=command.js.map
|