qqbot-opencode 1.0.0
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/LICENSE +21 -0
- package/README.md +197 -0
- package/bin/qqbot.js +16 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +154 -0
- package/dist/app.js.map +1 -0
- package/dist/bundle.cjs +850 -0
- package/dist/bundle.js +826 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +179 -0
- package/dist/config.js.map +1 -0
- package/dist/handlers/index.d.ts +3 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/message.d.ts +8 -0
- package/dist/handlers/message.d.ts.map +1 -0
- package/dist/handlers/message.js +57 -0
- package/dist/handlers/message.js.map +1 -0
- package/dist/handlers/session.d.ts +13 -0
- package/dist/handlers/session.d.ts.map +1 -0
- package/dist/handlers/session.js +104 -0
- package/dist/handlers/session.js.map +1 -0
- package/dist/opencode/client.d.ts +23 -0
- package/dist/opencode/client.d.ts.map +1 -0
- package/dist/opencode/client.js +141 -0
- package/dist/opencode/client.js.map +1 -0
- package/dist/opencode/index.d.ts +2 -0
- package/dist/opencode/index.d.ts.map +1 -0
- package/dist/opencode/index.js +2 -0
- package/dist/opencode/index.js.map +1 -0
- package/dist/qq/connection.d.ts +23 -0
- package/dist/qq/connection.d.ts.map +1 -0
- package/dist/qq/connection.js +188 -0
- package/dist/qq/connection.js.map +1 -0
- package/dist/qq/index.d.ts +5 -0
- package/dist/qq/index.d.ts.map +1 -0
- package/dist/qq/index.js +4 -0
- package/dist/qq/index.js.map +1 -0
- package/dist/qq/parser.d.ts +4 -0
- package/dist/qq/parser.d.ts.map +1 -0
- package/dist/qq/parser.js +99 -0
- package/dist/qq/parser.js.map +1 -0
- package/dist/qq/sender.d.ts +28 -0
- package/dist/qq/sender.d.ts.map +1 -0
- package/dist/qq/sender.js +123 -0
- package/dist/qq/sender.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
- package/src/app.ts +204 -0
- package/src/config.ts +200 -0
- package/src/handlers/index.ts +2 -0
- package/src/handlers/message.ts +86 -0
- package/src/handlers/session.ts +130 -0
- package/src/opencode/client.ts +204 -0
- package/src/opencode/index.ts +1 -0
- package/src/qq/connection.ts +252 -0
- package/src/qq/index.ts +9 -0
- package/src/qq/parser.ts +126 -0
- package/src/qq/sender.ts +215 -0
- package/src/types.ts +52 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig,
|
|
3
|
+
expandEnvVariables,
|
|
4
|
+
findDefaultConfig,
|
|
5
|
+
runInitWizard,
|
|
6
|
+
} from "./config.js";
|
|
7
|
+
import type { Config } from "./types.js";
|
|
8
|
+
import {
|
|
9
|
+
startQQConnection,
|
|
10
|
+
sendC2CMessage,
|
|
11
|
+
parseMessage,
|
|
12
|
+
chunkText,
|
|
13
|
+
clearTokenCache,
|
|
14
|
+
} from "./qq/index.js";
|
|
15
|
+
import {
|
|
16
|
+
initOpencodeClient,
|
|
17
|
+
closeOpencodeClient,
|
|
18
|
+
createSession,
|
|
19
|
+
} from "./opencode/client.js";
|
|
20
|
+
import { handleMessage } from "./handlers/message.js";
|
|
21
|
+
|
|
22
|
+
const TEXT_CHUNK_LIMIT = 500;
|
|
23
|
+
|
|
24
|
+
interface CLIArgs {
|
|
25
|
+
config?: string;
|
|
26
|
+
init?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(): CLIArgs {
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const result: CLIArgs = {};
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === "--config" && i + 1 < args.length) {
|
|
35
|
+
result.config = args[i + 1];
|
|
36
|
+
i++;
|
|
37
|
+
} else if (args[i] === "--init" || args[i] === "init") {
|
|
38
|
+
result.init = true;
|
|
39
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
40
|
+
printHelp();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printHelp(): void {
|
|
49
|
+
console.log(`
|
|
50
|
+
QQ Bot with OpenCode AI
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
qqbot 启动 Bot(使用默认配置)
|
|
54
|
+
qqbot --config <path> 使用指定配置文件
|
|
55
|
+
qqbot init 交互式创建配置文件
|
|
56
|
+
qqbot --help 显示帮助
|
|
57
|
+
|
|
58
|
+
配置搜索顺序:
|
|
59
|
+
1. --config 指定路径
|
|
60
|
+
2. ./config.yaml(当前工作目录)
|
|
61
|
+
3. ~/.qqbot/config.yaml(用户家目录)
|
|
62
|
+
|
|
63
|
+
示例:
|
|
64
|
+
qqbot
|
|
65
|
+
qqbot --config ./my-config.yaml
|
|
66
|
+
qqbot init
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main(): Promise<void> {
|
|
71
|
+
const args = parseArgs();
|
|
72
|
+
|
|
73
|
+
if (args.init) {
|
|
74
|
+
await runInitWizard();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let configPath: string | null = args.config ?? null;
|
|
79
|
+
|
|
80
|
+
if (configPath === null) {
|
|
81
|
+
configPath = findDefaultConfig();
|
|
82
|
+
if (configPath !== null) {
|
|
83
|
+
console.log(`[App] Using default config: ${configPath}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (configPath === null) {
|
|
88
|
+
console.error("Error: No config file found.");
|
|
89
|
+
console.error(
|
|
90
|
+
"\n请先运行 'qqbot init' 创建配置文件,或使用 --config 指定配置文件路径。",
|
|
91
|
+
);
|
|
92
|
+
console.error("\n运行 'qqbot --help' 查看更多信息。");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log("[App] Loading configuration...");
|
|
97
|
+
let config: Config;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
config = loadConfig(configPath);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(
|
|
103
|
+
`[App] Failed to load config: ${err instanceof Error ? err.message : String(err)}`,
|
|
104
|
+
);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const expandedConfig = expandEnvVariables(config) as Config;
|
|
109
|
+
|
|
110
|
+
console.log("[App] Initializing OpenCode client...");
|
|
111
|
+
try {
|
|
112
|
+
await initOpencodeClient(expandedConfig.opencode);
|
|
113
|
+
console.log("[App] OpenCode client initialized");
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(
|
|
116
|
+
`[App] Failed to initialize OpenCode: ${err instanceof Error ? err.message : String(err)}`,
|
|
117
|
+
);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await createSession();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error(
|
|
125
|
+
`[App] Failed to create initial session: ${err instanceof Error ? err.message : String(err)}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log("[App] Starting QQ connection...");
|
|
130
|
+
try {
|
|
131
|
+
await startQQConnection({
|
|
132
|
+
qq: expandedConfig.qq,
|
|
133
|
+
onMessage: async (event) => {
|
|
134
|
+
console.log(
|
|
135
|
+
`[App] Received message from ${event.author.user_openid}: ${event.content.slice(0, 50)}...`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const parsed = parseMessage(event as any);
|
|
140
|
+
const result = await handleMessage(parsed.content, parsed);
|
|
141
|
+
|
|
142
|
+
const chunks = chunkText(result.text, TEXT_CHUNK_LIMIT);
|
|
143
|
+
for (const chunk of chunks) {
|
|
144
|
+
await sendC2CMessage(expandedConfig.qq, {
|
|
145
|
+
toOpenid: event.author.user_openid,
|
|
146
|
+
content: chunk,
|
|
147
|
+
messageId: event.id,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`[App] Sent response to ${event.author.user_openid}`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error(
|
|
154
|
+
`[App] Error handling message: ${err instanceof Error ? err.message : String(err)}`,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const errorText = `处理消息时出错: ${err instanceof Error ? err.message : String(err)}`;
|
|
158
|
+
try {
|
|
159
|
+
await sendC2CMessage(expandedConfig.qq, {
|
|
160
|
+
toOpenid: event.author.user_openid,
|
|
161
|
+
content: errorText,
|
|
162
|
+
messageId: event.id,
|
|
163
|
+
});
|
|
164
|
+
} catch (sendErr) {
|
|
165
|
+
console.error(`[App] Failed to send error message: ${sendErr}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
onReady: () => {
|
|
170
|
+
console.log("[App] QQ Bot is ready!");
|
|
171
|
+
},
|
|
172
|
+
onError: (err) => {
|
|
173
|
+
console.error(`[App] QQ connection error: ${err.message}`);
|
|
174
|
+
},
|
|
175
|
+
onDisconnect: () => {
|
|
176
|
+
console.log("[App] QQ disconnected, will attempt to reconnect...");
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error(
|
|
181
|
+
`[App] Failed to start QQ connection: ${err instanceof Error ? err.message : String(err)}`,
|
|
182
|
+
);
|
|
183
|
+
await closeOpencodeClient();
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log("[App] Bot is running. Press Ctrl+C to stop.");
|
|
188
|
+
|
|
189
|
+
const shutdown = async () => {
|
|
190
|
+
console.log("\n[App] Shutting down...");
|
|
191
|
+
clearTokenCache();
|
|
192
|
+
await closeOpencodeClient();
|
|
193
|
+
console.log("[App] Goodbye!");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
process.on("SIGINT", shutdown);
|
|
198
|
+
process.on("SIGTERM", shutdown);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch((err) => {
|
|
202
|
+
console.error(`[App] Fatal error: ${err}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import type { Config } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), ".qqbot");
|
|
9
|
+
const CONFIG_FILE = "config.yaml";
|
|
10
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, CONFIG_FILE);
|
|
11
|
+
|
|
12
|
+
export function getConfigDir(): string {
|
|
13
|
+
return CONFIG_DIR;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function findDefaultConfig(): string | null {
|
|
17
|
+
const searchPaths = [path.join(process.cwd(), CONFIG_FILE), CONFIG_PATH];
|
|
18
|
+
|
|
19
|
+
for (const configPath of searchPaths) {
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
return configPath;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadConfig(configPath: string): Config {
|
|
28
|
+
const absolutePath = path.resolve(configPath);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(absolutePath)) {
|
|
31
|
+
throw new Error(`Config file not found: ${absolutePath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rawContent = fs.readFileSync(absolutePath, "utf-8");
|
|
35
|
+
const config = yaml.load(rawContent) as Config;
|
|
36
|
+
|
|
37
|
+
validateConfig(config);
|
|
38
|
+
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateConfig(config: Config): void {
|
|
43
|
+
if (!config.qq?.appId) {
|
|
44
|
+
throw new Error("Missing required field: qq.appId");
|
|
45
|
+
}
|
|
46
|
+
if (!config.qq?.clientSecret) {
|
|
47
|
+
throw new Error("Missing required field: qq.clientSecret");
|
|
48
|
+
}
|
|
49
|
+
if (!config.opencode?.port) {
|
|
50
|
+
config.opencode.port = 4097;
|
|
51
|
+
}
|
|
52
|
+
if (!config.opencode?.hostname) {
|
|
53
|
+
config.opencode.hostname = "127.0.0.1";
|
|
54
|
+
}
|
|
55
|
+
if (!config.app?.workingDir) {
|
|
56
|
+
config.app.workingDir = process.cwd();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function expandEnvVariables(obj: unknown): unknown {
|
|
61
|
+
if (typeof obj === "string") {
|
|
62
|
+
const envVarPattern = /\$\{([^}]+)\}/g;
|
|
63
|
+
return obj.replace(envVarPattern, (_, envName) => {
|
|
64
|
+
return process.env[envName] || "";
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(obj)) {
|
|
68
|
+
return obj.map(expandEnvVariables);
|
|
69
|
+
}
|
|
70
|
+
if (obj && typeof obj === "object") {
|
|
71
|
+
const result: Record<string, unknown> = {};
|
|
72
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
73
|
+
result[key] = expandEnvVariables(value);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
return obj;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function saveConfig(config: Config, destPath?: string): void {
|
|
81
|
+
const savePath = destPath || CONFIG_PATH;
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
84
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const yamlContent = yaml.dump(config, {
|
|
88
|
+
indent: 2,
|
|
89
|
+
lineWidth: -1,
|
|
90
|
+
noRefs: true,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(savePath, yamlContent, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runInitWizard(): Promise<void> {
|
|
97
|
+
console.log("\n欢迎使用 qqbot-opencode 配置向导!\n");
|
|
98
|
+
|
|
99
|
+
const answers = await prompts([
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
name: "appId",
|
|
103
|
+
message: "请输入 QQ Bot AppID:",
|
|
104
|
+
validate: (value) => value.length > 0 || "AppID 不能为空",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
name: "clientSecret",
|
|
109
|
+
message: "请输入 QQ Bot ClientSecret:",
|
|
110
|
+
validate: (value) => value.length > 0 || "ClientSecret 不能为空",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: "confirm",
|
|
114
|
+
name: "markdownSupport",
|
|
115
|
+
message: "是否启用 Markdown 渲染? (y/N):",
|
|
116
|
+
initial: false,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: "number",
|
|
120
|
+
name: "opencodePort",
|
|
121
|
+
message: "opencode 服务端口 (默认: 4097):",
|
|
122
|
+
initial: 4097,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: "select",
|
|
126
|
+
name: "providerType",
|
|
127
|
+
message: "AI Provider 类型:",
|
|
128
|
+
choices: [
|
|
129
|
+
{ title: "1. Anthropic 兼容 (MiniMax)", value: "anthropic" },
|
|
130
|
+
{ title: "2. OpenAI 兼容", value: "openai" },
|
|
131
|
+
],
|
|
132
|
+
initial: 0,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
name: "providerName",
|
|
137
|
+
message: "请输入 Provider 名称 (默认: minimax):",
|
|
138
|
+
initial: "minimax",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
name: "modelName",
|
|
143
|
+
message: "AI 模型名称 (默认: MiniMax-M2.7):",
|
|
144
|
+
initial: "MiniMax-M2.7",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
name: "apiKey",
|
|
149
|
+
message: "API Key:",
|
|
150
|
+
validate: (value) => value.length > 0 || "API Key 不能为空",
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const providerType = answers.providerType;
|
|
155
|
+
const providerName = answers.providerName || "minimax";
|
|
156
|
+
const modelName = answers.modelName || "MiniMax-M2.7";
|
|
157
|
+
|
|
158
|
+
let baseURL: string;
|
|
159
|
+
if (providerType === "anthropic") {
|
|
160
|
+
baseURL = "https://api.minimaxi.com/anthropic/v1";
|
|
161
|
+
} else {
|
|
162
|
+
baseURL = "https://api.openai.com/v1";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const config: Config = {
|
|
166
|
+
qq: {
|
|
167
|
+
appId: answers.appId,
|
|
168
|
+
clientSecret: answers.clientSecret,
|
|
169
|
+
markdownSupport: answers.markdownSupport,
|
|
170
|
+
},
|
|
171
|
+
opencode: {
|
|
172
|
+
port: answers.opencodePort || 4097,
|
|
173
|
+
hostname: "127.0.0.1",
|
|
174
|
+
config: {
|
|
175
|
+
model: `${providerName}/${modelName}`,
|
|
176
|
+
provider: {
|
|
177
|
+
[providerName]: {
|
|
178
|
+
options: {
|
|
179
|
+
baseURL,
|
|
180
|
+
apiKey: answers.apiKey,
|
|
181
|
+
},
|
|
182
|
+
models: {
|
|
183
|
+
[modelName]: {
|
|
184
|
+
name: `${providerName}/${modelName}`,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
app: {
|
|
192
|
+
workingDir: "./",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
saveConfig(config);
|
|
197
|
+
|
|
198
|
+
console.log("\n配置已保存到 ~/.qqbot/config.yaml");
|
|
199
|
+
console.log("运行 'qqbot' 启动 Bot\n");
|
|
200
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { sendPrompt } from "../opencode/client.js";
|
|
2
|
+
import {
|
|
3
|
+
parseSessionCommand,
|
|
4
|
+
handleSessionNew,
|
|
5
|
+
handleSessionSwitch,
|
|
6
|
+
handleSessionList,
|
|
7
|
+
handleSessionCurrent,
|
|
8
|
+
} from "./session.js";
|
|
9
|
+
import type { ParsedMessage } from "../qq/index.js";
|
|
10
|
+
|
|
11
|
+
export interface MessageHandlerResult {
|
|
12
|
+
text: string;
|
|
13
|
+
success: boolean;
|
|
14
|
+
isSessionCommand?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function handleMessage(
|
|
18
|
+
content: string,
|
|
19
|
+
parsedMessage: ParsedMessage,
|
|
20
|
+
): Promise<MessageHandlerResult> {
|
|
21
|
+
const sessionCmd = parseSessionCommand(content);
|
|
22
|
+
|
|
23
|
+
if (sessionCmd) {
|
|
24
|
+
return handleSessionCommand(sessionCmd.command, sessionCmd.args);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return handleAIMessage(content, parsedMessage);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handleSessionCommand(
|
|
31
|
+
command: string,
|
|
32
|
+
args: string,
|
|
33
|
+
): Promise<MessageHandlerResult> {
|
|
34
|
+
let result;
|
|
35
|
+
|
|
36
|
+
switch (command) {
|
|
37
|
+
case "new":
|
|
38
|
+
result = await handleSessionNew();
|
|
39
|
+
break;
|
|
40
|
+
case "switch":
|
|
41
|
+
result = await handleSessionSwitch(args);
|
|
42
|
+
break;
|
|
43
|
+
case "list":
|
|
44
|
+
result = await handleSessionList();
|
|
45
|
+
break;
|
|
46
|
+
case "current":
|
|
47
|
+
result = await handleSessionCurrent();
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
result = {
|
|
51
|
+
text: "未知的 session 命令",
|
|
52
|
+
success: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...result,
|
|
58
|
+
isSessionCommand: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleAIMessage(
|
|
63
|
+
content: string,
|
|
64
|
+
parsedMessage: ParsedMessage,
|
|
65
|
+
): Promise<MessageHandlerResult> {
|
|
66
|
+
try {
|
|
67
|
+
const result = await sendPrompt(content, parsedMessage.imageUrls);
|
|
68
|
+
|
|
69
|
+
if (!result.text || result.text.trim() === "") {
|
|
70
|
+
return {
|
|
71
|
+
text: "AI 返回了空响应",
|
|
72
|
+
success: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
text: result.text,
|
|
78
|
+
success: true,
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
text: `处理消息失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
83
|
+
success: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSession,
|
|
3
|
+
switchSession,
|
|
4
|
+
listSessions,
|
|
5
|
+
getCurrentSession,
|
|
6
|
+
} from "../opencode/client.js";
|
|
7
|
+
|
|
8
|
+
export interface SessionCommandResult {
|
|
9
|
+
text: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleSessionNew(): Promise<SessionCommandResult> {
|
|
14
|
+
try {
|
|
15
|
+
const session = await createSession();
|
|
16
|
+
return {
|
|
17
|
+
text: `已创建新会话: ${session.id}\n标题: ${session.title || "无"}`,
|
|
18
|
+
success: true,
|
|
19
|
+
};
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return {
|
|
22
|
+
text: `创建会话失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
23
|
+
success: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleSessionSwitch(
|
|
29
|
+
sessionId: string,
|
|
30
|
+
): Promise<SessionCommandResult> {
|
|
31
|
+
if (!sessionId || sessionId.trim() === "") {
|
|
32
|
+
return {
|
|
33
|
+
text: "请提供会话 ID,例如: /session-switch <id>",
|
|
34
|
+
success: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const session = await switchSession(sessionId.trim());
|
|
40
|
+
return {
|
|
41
|
+
text: `已切换到会话: ${session.id}\n标题: ${session.title || "无"}`,
|
|
42
|
+
success: true,
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
text: `切换会话失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
47
|
+
success: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function handleSessionList(): Promise<SessionCommandResult> {
|
|
53
|
+
try {
|
|
54
|
+
const sessions = await listSessions();
|
|
55
|
+
|
|
56
|
+
if (sessions.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
text: "暂无会话",
|
|
59
|
+
success: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const currentSession = await getCurrentSession();
|
|
64
|
+
const currentId = currentSession?.id;
|
|
65
|
+
|
|
66
|
+
const lines = sessions.map((s, i) => {
|
|
67
|
+
const marker = s.id === currentId ? " [当前]" : "";
|
|
68
|
+
const title = s.title ? ` - ${s.title}` : "";
|
|
69
|
+
return `${i + 1}. ${s.id}${title}${marker}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
text: `会话列表:\n${lines.join("\n")}`,
|
|
74
|
+
success: true,
|
|
75
|
+
};
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
text: `获取会话列表失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
|
+
success: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function handleSessionCurrent(): Promise<SessionCommandResult> {
|
|
85
|
+
try {
|
|
86
|
+
const session = await getCurrentSession();
|
|
87
|
+
|
|
88
|
+
if (!session) {
|
|
89
|
+
return {
|
|
90
|
+
text: "当前没有活跃的会话",
|
|
91
|
+
success: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
text: `当前会话:\nID: ${session.id}\n标题: ${session.title || "无"}`,
|
|
97
|
+
success: true,
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
text: `获取当前会话失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
102
|
+
success: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseSessionCommand(
|
|
108
|
+
content: string,
|
|
109
|
+
): { command: string; args: string } | null {
|
|
110
|
+
const trimmed = content.trim();
|
|
111
|
+
|
|
112
|
+
if (trimmed === "/session-new") {
|
|
113
|
+
return { command: "new", args: "" };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (trimmed.startsWith("/session-switch ")) {
|
|
117
|
+
const args = trimmed.slice("/session-switch ".length).trim();
|
|
118
|
+
return { command: "switch", args };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (trimmed === "/session-list") {
|
|
122
|
+
return { command: "list", args: "" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (trimmed === "/session-current") {
|
|
126
|
+
return { command: "current", args: "" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|