@debbl/relay 0.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/README.md +101 -0
- package/README.zh.md +101 -0
- package/bin/relay.mjs +3 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1360 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +79 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1360 @@
|
|
|
1
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import { i18n } from "@lingui/core";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
//#region src/locales/en/messages.po
|
|
11
|
+
const messages$1 = JSON.parse("{\"1xKjU/\":[\"Failed to read open projects. Please try again later.\"],\"36QmnO\":[\"thread: \",[\"0\"]],\"4IDydv\":[\"Invalid relay config at \",[\"configPath\"],\": root must be a JSON object.\"],\"4Vrm3r\":[\"cwd: \",[\"0\"]],\"6tcMXX\":[\"Invalid relay config: \",[\"field\"],\" is required and must be a non-empty string.\"],\"AWIU5i\":[\"Relay config is missing. Template created at \",[\"configPath\"],\". Please edit this file and restart.\"],\"Bb/dZi\":[\"Available commands:\"],\"CfFOzJ\":[\"Invalid relay config at \",[\"configPath\"],\": env must be a JSON object.\"],\"Ck6rmi\":[\"Current session has been cleared.\"],\"FZcpfm\":[\"Failed to process message. Please try again later.\"],\"G1WYAl\":[\"Unknown command \\\"\",[\"0\"],\"\\\".\\n\\n\",[\"helpText\"]],\"H7VlDR\":[\"Currently busy. Please try again later.\"],\"HEx9te\":[\"New Session\"],\"Hrkm8q\":[\"Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer.\"],\"JvAWQ5\":[\"No working directories are currently open.\"],\"K8IUjY\":[\"mode: \",[\"0\"]],\"Ks6r4a\":[\"Invalid relay config: \",[\"field\"],\" must be a string.\"],\"MZ28Ys\":[\"Failed to read open projects: \",[\"0\"]],\"NX40Ku\":[\"/new [default|plan] - Create a new session\"],\"OZZfXh\":[\"Current session status:\"],\"Oeb7jH\":[\"model: \",[\"0\"]],\"Qq8U1p\":[\"/status does not accept arguments.\\n\\n\",[\"helpText\"]],\"RLcmLa\":[\"/status - Show current session status\"],\"Rn/EAE\":[\"You are a session title generator.\\nGenerate a short English title based on the user message.\\nStrict requirements:\\n1. Output title text only, with no explanation.\\n2. Output a single line with no line breaks.\\n3. Do not use quotes.\\n4. Keep the title within 24 characters.\"],\"T/qXZl\":[\"/new accepts at most one optional argument: default or plan.\\n\\n\",[\"helpText\"]],\"Xg0HiS\":[\"/mode <default|plan> - Switch current session mode\"],\"YwjZm9\":[\"No active session. Send a normal message or use /new to create one.\"],\"aQiwam\":[\"Failed to start relay: \",[\"message\"]],\"dV7HiV\":[\"Invalid relay config: LOCALE \\\"\",[\"0\"],\"\\\" is not supported. Falling back to en.\"],\"dXeZCW\":[\"Switched to \",[\"0\"],\" mode.\"],\"dZhhUX\":[\"Created a new session.\"],\"doQwEN\":[\"Codex execution failed: \",[\"0\"]],\"eie2Mj\":[\"Command cannot be empty.\\n\\n\",[\"helpText\"]],\"gHcqpZ\":[\"Invalid mode \\\"\",[\"modeToken\"],\"\\\", only default or plan are supported.\\n\\n\",[\"helpText\"]],\"gf810l\":[\"/mode requires one argument: default or plan.\\n\\n\",[\"helpText\"]],\"j3bsc1\":[[\"0\"],\". \",[\"root\"]],\"ksj3fj\":[\"Invalid relay config: LOCALE \\\"\",[\"normalized\"],\"\\\" is not supported. Falling back to en.\"],\"lksJXf\":[\"/projects - Show current working directories\"],\"mehbut\":[\"No active session. Send a normal message or use /new to create one first.\"],\"miupLy\":[\"Current working directories:\"],\"o4F8ck\":[\"/reset does not accept arguments.\\n\\n\",[\"helpText\"]],\"oEMyJf\":[\"Please send a text message.\"],\"oKQkYZ\":[\"/projects does not accept arguments.\\n\\n\",[\"helpText\"]],\"orhkAj\":[\"Codex execution failed. Please try again later.\"],\"qN3BkN\":[\"/reset - Clear current session\"],\"rHDhWM\":[\"Failed to parse message. Please send a text message.\"],\"rdodSw\":[\"You are a session title generator.\\nGenerate a short Chinese title based on the user message.\\nStrict requirements:\\n1. Output title text only, with no explanation.\\n2. Output a single line with no line breaks.\\n3. Do not use quotes or title marks.\\n4. Keep the title within 24 characters.\"],\"smfLBQ\":[\"Cannot identify sender. Please try again later.\"],\"tHEFWw\":[\"title: \",[\"title\"]],\"tZQUtS\":[\"Failed to read relay config at \",[\"configPath\"],\": \",[\"0\"]],\"uAkDSp\":[\"User message: \",[\"0\"]],\"wBrugH\":[\"/help does not accept arguments.\\n\\n\",[\"helpText\"]],\"wSNjFy\":[\"/help - Show help\"],\"z+7ZYe\":[\"Invalid JSON in relay config at \",[\"configPath\"],\": \",[\"0\"]]}");
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/locales/zh/messages.po
|
|
15
|
+
const messages = JSON.parse("{\"1xKjU/\":[\"读取已打开项目失败,请稍后重试。\"],\"36QmnO\":[\"thread: \",[\"0\"]],\"4IDydv\":[\"中继配置 \",[\"configPath\"],\" 无效:root 必须是 JSON 对象。\"],\"4Vrm3r\":[\"cwd: \",[\"0\"]],\"6tcMXX\":[\"中继配置无效:\",[\"field\"],\" 为必填项且必须为非空字符串。\"],\"AWIU5i\":[\"中继配置缺失,已在 \",[\"configPath\"],\" 创建模板。请编辑该文件后重启。\"],\"Bb/dZi\":[\"可用命令:\"],\"CfFOzJ\":[\"中继配置 \",[\"configPath\"],\" 无效:env 必须是 JSON 对象。\"],\"Ck6rmi\":[\"当前会话已清空。\"],\"FZcpfm\":[\"处理消息失败,请稍后重试。\"],\"G1WYAl\":[\"未知命令 \\\"\",[\"0\"],\"\\\"。\\n\\n\",[\"helpText\"]],\"H7VlDR\":[\"当前忙碌,请稍后重试。\"],\"HEx9te\":[\"新会话\"],\"Hrkm8q\":[\"中继配置无效:CODEX_TIMEOUT_MS 必须是正整数。\"],\"JvAWQ5\":[\"当前没有打开的工作目录。\"],\"K8IUjY\":[\"mode: \",[\"0\"]],\"Ks6r4a\":[\"中继配置无效:\",[\"field\"],\" 必须是字符串。\"],\"MZ28Ys\":[\"读取已打开项目失败:\",[\"0\"]],\"NX40Ku\":[\"/new [default|plan] - 创建新会话\"],\"OZZfXh\":[\"当前会话状态:\"],\"Oeb7jH\":[\"model: \",[\"0\"]],\"Qq8U1p\":[\"/status 不接受参数。\\n\\n\",[\"helpText\"]],\"RLcmLa\":[\"/status - 显示当前会话状态\"],\"Rn/EAE\":[\"你是会话标题生成器。\\n根据用户消息生成简短英文标题。\\n严格要求:\\n1. 仅输出标题文字,不要解释。\\n2. 单行输出,不要换行。\\n3. 不要使用引号。\\n4. 标题不超过 24 个字符。\"],\"T/qXZl\":[\"/new 最多接受一个可选参数:default 或 plan。\\n\\n\",[\"helpText\"]],\"Xg0HiS\":[\"/mode <default|plan> - 切换当前会话模式\"],\"YwjZm9\":[\"没有活跃会话。请发送普通消息或使用 /new 创建会话。\"],\"aQiwam\":[\"启动中继失败:\",[\"message\"]],\"dV7HiV\":[\"中继配置无效:不支持的 LOCALE \\\"\",[\"0\"],\"\\\",已回退为 en。\"],\"dXeZCW\":[\"已切换到 \",[\"0\"],\" 模式。\"],\"dZhhUX\":[\"已创建新会话。\"],\"doQwEN\":[\"Codex 执行失败:\",[\"0\"]],\"eie2Mj\":[\"命令不能为空。\\n\\n\",[\"helpText\"]],\"gHcqpZ\":[\"无效的模式 \\\"\",[\"modeToken\"],\"\\\",仅支持 default 或 plan。\\n\\n\",[\"helpText\"]],\"gf810l\":[\"/mode 需要一个参数:default 或 plan。\\n\\n\",[\"helpText\"]],\"j3bsc1\":[[\"0\"],\". \",[\"root\"]],\"ksj3fj\":[\"中继配置无效:不支持的 LOCALE \\\"\",[\"normalized\"],\"\\\",已回退为 en。\"],\"lksJXf\":[\"/projects - 显示当前工作目录\"],\"mehbut\":[\"没有活跃会话。请先发送普通消息或使用 /new 创建会话。\"],\"miupLy\":[\"当前工作目录:\"],\"o4F8ck\":[\"/reset 不接受参数。\\n\\n\",[\"helpText\"]],\"oEMyJf\":[\"请发送文本消息。\"],\"oKQkYZ\":[\"/projects 不接受参数。\\n\\n\",[\"helpText\"]],\"orhkAj\":[\"Codex 执行失败,请稍后重试。\"],\"qN3BkN\":[\"/reset - 清空当前会话\"],\"rHDhWM\":[\"解析消息失败,请发送文本消息。\"],\"rdodSw\":[\"你是会话标题生成器。\\n根据用户消息生成简短中文标题。\\n严格要求:\\n1. 仅输出标题文字,不要解释。\\n2. 单行输出,不要换行。\\n3. 不要使用引号或书名号。\\n4. 标题不超过 24 个字符。\"],\"smfLBQ\":[\"无法识别发送者,请稍后重试。\"],\"tHEFWw\":[\"title: \",[\"title\"]],\"tZQUtS\":[\"读取中继配置失败 \",[\"configPath\"],\":\",[\"0\"]],\"uAkDSp\":[\"用户消息:\",[\"0\"]],\"wBrugH\":[\"/help 不接受参数。\\n\\n\",[\"helpText\"]],\"wSNjFy\":[\"/help - 显示帮助\"],\"z+7ZYe\":[\"中继配置 \",[\"configPath\"],\" 中的 JSON 无效:\",[\"0\"]]}");
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/i18n/runtime.ts
|
|
19
|
+
const DEFAULT_LOCALE = "en";
|
|
20
|
+
const CATALOGS = {
|
|
21
|
+
en: messages$1,
|
|
22
|
+
zh: messages
|
|
23
|
+
};
|
|
24
|
+
let activeLocale = null;
|
|
25
|
+
function initializeI18n(locale) {
|
|
26
|
+
const resolved = resolveLocale(locale);
|
|
27
|
+
i18n.loadAndActivate({
|
|
28
|
+
locale: resolved,
|
|
29
|
+
messages: CATALOGS[resolved]
|
|
30
|
+
});
|
|
31
|
+
activeLocale = resolved;
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
function getCurrentLocale() {
|
|
35
|
+
ensureI18nInitialized();
|
|
36
|
+
return activeLocale ?? DEFAULT_LOCALE;
|
|
37
|
+
}
|
|
38
|
+
function isSupportedLocale(locale) {
|
|
39
|
+
return locale === "en" || locale === "zh";
|
|
40
|
+
}
|
|
41
|
+
function getDefaultLocale() {
|
|
42
|
+
return DEFAULT_LOCALE;
|
|
43
|
+
}
|
|
44
|
+
function resolveLocale(locale) {
|
|
45
|
+
if (!locale) return DEFAULT_LOCALE;
|
|
46
|
+
if (isSupportedLocale(locale)) return locale;
|
|
47
|
+
return DEFAULT_LOCALE;
|
|
48
|
+
}
|
|
49
|
+
function ensureI18nInitialized() {
|
|
50
|
+
if (!activeLocale) initializeI18n(DEFAULT_LOCALE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/session/store.ts
|
|
55
|
+
const sessionStore = /* @__PURE__ */ new Map();
|
|
56
|
+
const sessionQueue = /* @__PURE__ */ new Map();
|
|
57
|
+
function getSessionKey(input) {
|
|
58
|
+
if (input.chatType === "p2p") return `p2p:${input.chatId}`;
|
|
59
|
+
return `group:${input.chatId}:${input.userId}`;
|
|
60
|
+
}
|
|
61
|
+
function getSession(sessionKey) {
|
|
62
|
+
return sessionStore.get(sessionKey);
|
|
63
|
+
}
|
|
64
|
+
function setSession(sessionKey, session) {
|
|
65
|
+
sessionStore.set(sessionKey, session);
|
|
66
|
+
}
|
|
67
|
+
function clearSession(sessionKey) {
|
|
68
|
+
sessionStore.delete(sessionKey);
|
|
69
|
+
}
|
|
70
|
+
async function withSessionLock(sessionKey, run) {
|
|
71
|
+
const running = (sessionQueue.get(sessionKey) ?? Promise.resolve()).then(() => run(), () => run());
|
|
72
|
+
const queueItem = running.then(() => void 0, () => void 0);
|
|
73
|
+
sessionQueue.set(sessionKey, queueItem);
|
|
74
|
+
try {
|
|
75
|
+
return await running;
|
|
76
|
+
} finally {
|
|
77
|
+
if (sessionQueue.get(sessionKey) === queueItem) sessionQueue.delete(sessionKey);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/bot/commands.ts
|
|
83
|
+
const COMMAND_HELP = "/help";
|
|
84
|
+
const COMMAND_NEW = "/new";
|
|
85
|
+
const COMMAND_MODE = "/mode";
|
|
86
|
+
const COMMAND_STATUS = "/status";
|
|
87
|
+
const COMMAND_PROJECTS = "/projects";
|
|
88
|
+
const COMMAND_RESET = "/reset";
|
|
89
|
+
function getHelpText() {
|
|
90
|
+
return [
|
|
91
|
+
i18n._({
|
|
92
|
+
id: "Bb/dZi",
|
|
93
|
+
message: "Available commands:"
|
|
94
|
+
}),
|
|
95
|
+
i18n._({
|
|
96
|
+
id: "wSNjFy",
|
|
97
|
+
message: "/help - Show help"
|
|
98
|
+
}),
|
|
99
|
+
i18n._({
|
|
100
|
+
id: "NX40Ku",
|
|
101
|
+
message: "/new [default|plan] - Create a new session"
|
|
102
|
+
}),
|
|
103
|
+
i18n._({
|
|
104
|
+
id: "Xg0HiS",
|
|
105
|
+
message: "/mode <default|plan> - Switch current session mode"
|
|
106
|
+
}),
|
|
107
|
+
i18n._({
|
|
108
|
+
id: "RLcmLa",
|
|
109
|
+
message: "/status - Show current session status"
|
|
110
|
+
}),
|
|
111
|
+
i18n._({
|
|
112
|
+
id: "lksJXf",
|
|
113
|
+
message: "/projects - Show current working directories"
|
|
114
|
+
}),
|
|
115
|
+
i18n._({
|
|
116
|
+
id: "qN3BkN",
|
|
117
|
+
message: "/reset - Clear current session"
|
|
118
|
+
})
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
function parseCommand(input) {
|
|
122
|
+
const normalized = input.trim();
|
|
123
|
+
const helpText = getHelpText();
|
|
124
|
+
if (normalized.length === 0) return {
|
|
125
|
+
type: "invalid",
|
|
126
|
+
message: i18n._({
|
|
127
|
+
id: "eie2Mj",
|
|
128
|
+
message: "Command cannot be empty.\n\n{helpText}",
|
|
129
|
+
values: { helpText }
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
if (!normalized.startsWith("/")) return {
|
|
133
|
+
type: "prompt",
|
|
134
|
+
prompt: normalized
|
|
135
|
+
};
|
|
136
|
+
const parts = normalized.split(/\s+/);
|
|
137
|
+
const command = parts[0]?.toLowerCase();
|
|
138
|
+
if (command === COMMAND_HELP) {
|
|
139
|
+
if (parts.length > 1) return {
|
|
140
|
+
type: "invalid",
|
|
141
|
+
message: i18n._({
|
|
142
|
+
id: "wBrugH",
|
|
143
|
+
message: "/help does not accept arguments.\n\n{helpText}",
|
|
144
|
+
values: { helpText }
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
return { type: "help" };
|
|
148
|
+
}
|
|
149
|
+
if (command === COMMAND_NEW) {
|
|
150
|
+
if (parts.length > 2) return {
|
|
151
|
+
type: "invalid",
|
|
152
|
+
message: i18n._({
|
|
153
|
+
id: "T/qXZl",
|
|
154
|
+
message: "/new accepts at most one optional argument: default or plan.\n\n{helpText}",
|
|
155
|
+
values: { helpText }
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
const modeToken = parts[1];
|
|
159
|
+
if (!modeToken) return {
|
|
160
|
+
type: "new",
|
|
161
|
+
mode: "default"
|
|
162
|
+
};
|
|
163
|
+
const mode = parseMode(modeToken);
|
|
164
|
+
if (!mode) return {
|
|
165
|
+
type: "invalid",
|
|
166
|
+
message: i18n._({
|
|
167
|
+
id: "gHcqpZ",
|
|
168
|
+
message: "Invalid mode \"{modeToken}\", only default or plan are supported.\n\n{helpText}",
|
|
169
|
+
values: {
|
|
170
|
+
modeToken,
|
|
171
|
+
helpText
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
type: "new",
|
|
177
|
+
mode
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (command === COMMAND_MODE) {
|
|
181
|
+
const modeToken = parts[1];
|
|
182
|
+
if (!modeToken || parts.length > 2) return {
|
|
183
|
+
type: "invalid",
|
|
184
|
+
message: i18n._({
|
|
185
|
+
id: "gf810l",
|
|
186
|
+
message: "/mode requires one argument: default or plan.\n\n{helpText}",
|
|
187
|
+
values: { helpText }
|
|
188
|
+
})
|
|
189
|
+
};
|
|
190
|
+
const mode = parseMode(modeToken);
|
|
191
|
+
if (!mode) return {
|
|
192
|
+
type: "invalid",
|
|
193
|
+
message: i18n._({
|
|
194
|
+
id: "gHcqpZ",
|
|
195
|
+
message: "Invalid mode \"{modeToken}\", only default or plan are supported.\n\n{helpText}",
|
|
196
|
+
values: {
|
|
197
|
+
modeToken,
|
|
198
|
+
helpText
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
type: "mode",
|
|
204
|
+
mode
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (command === COMMAND_STATUS) {
|
|
208
|
+
if (parts.length > 1) return {
|
|
209
|
+
type: "invalid",
|
|
210
|
+
message: i18n._({
|
|
211
|
+
id: "Qq8U1p",
|
|
212
|
+
message: "/status does not accept arguments.\n\n{helpText}",
|
|
213
|
+
values: { helpText }
|
|
214
|
+
})
|
|
215
|
+
};
|
|
216
|
+
return { type: "status" };
|
|
217
|
+
}
|
|
218
|
+
if (command === COMMAND_PROJECTS) {
|
|
219
|
+
if (parts.length > 1) return {
|
|
220
|
+
type: "invalid",
|
|
221
|
+
message: i18n._({
|
|
222
|
+
id: "oKQkYZ",
|
|
223
|
+
message: "/projects does not accept arguments.\n\n{helpText}",
|
|
224
|
+
values: { helpText }
|
|
225
|
+
})
|
|
226
|
+
};
|
|
227
|
+
return { type: "projects" };
|
|
228
|
+
}
|
|
229
|
+
if (command === COMMAND_RESET) {
|
|
230
|
+
if (parts.length > 1) return {
|
|
231
|
+
type: "invalid",
|
|
232
|
+
message: i18n._({
|
|
233
|
+
id: "o4F8ck",
|
|
234
|
+
message: "/reset does not accept arguments.\n\n{helpText}",
|
|
235
|
+
values: { helpText }
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
return { type: "reset" };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
type: "invalid",
|
|
242
|
+
message: i18n._({
|
|
243
|
+
id: "G1WYAl",
|
|
244
|
+
message: "Unknown command \"{0}\".\n\n{helpText}",
|
|
245
|
+
values: {
|
|
246
|
+
helpText,
|
|
247
|
+
0: command ?? normalized
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function parseMode(input) {
|
|
253
|
+
const normalized = input.toLowerCase();
|
|
254
|
+
if (normalized === "default" || normalized === "plan") return normalized;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/bot/handler.ts
|
|
260
|
+
const MAX_SESSION_TITLE_LENGTH = 24;
|
|
261
|
+
const WRAPPING_QUOTE_PAIRS = [
|
|
262
|
+
["\"", "\""],
|
|
263
|
+
["'", "'"],
|
|
264
|
+
["“", "”"],
|
|
265
|
+
["‘", "’"],
|
|
266
|
+
["「", "」"],
|
|
267
|
+
["《", "》"]
|
|
268
|
+
];
|
|
269
|
+
async function handleIncomingText(input, deps) {
|
|
270
|
+
const sessionKey = getSessionKey({
|
|
271
|
+
chatType: input.chatType,
|
|
272
|
+
chatId: input.chatId,
|
|
273
|
+
userId: input.senderId
|
|
274
|
+
});
|
|
275
|
+
return deps.withSessionLock(sessionKey, async () => {
|
|
276
|
+
const parsed = parseCommand(input.text);
|
|
277
|
+
const currentSession = deps.getSession(sessionKey);
|
|
278
|
+
if (parsed.type === "invalid") return parsed.message;
|
|
279
|
+
if (parsed.type === "help") return getHelpText();
|
|
280
|
+
if (parsed.type === "status") {
|
|
281
|
+
if (!currentSession) return i18n._({
|
|
282
|
+
id: "YwjZm9",
|
|
283
|
+
message: "No active session. Send a normal message or use /new to create one."
|
|
284
|
+
});
|
|
285
|
+
const title = normalizeSessionTitle(currentSession.title) ?? i18n._({
|
|
286
|
+
id: "HEx9te",
|
|
287
|
+
message: "New Session"
|
|
288
|
+
});
|
|
289
|
+
return [
|
|
290
|
+
i18n._({
|
|
291
|
+
id: "OZZfXh",
|
|
292
|
+
message: "Current session status:"
|
|
293
|
+
}),
|
|
294
|
+
i18n._({
|
|
295
|
+
id: "36QmnO",
|
|
296
|
+
message: "thread: {0}",
|
|
297
|
+
values: { 0: currentSession.threadId }
|
|
298
|
+
}),
|
|
299
|
+
i18n._({
|
|
300
|
+
id: "tHEFWw",
|
|
301
|
+
message: "title: {title}",
|
|
302
|
+
values: { title }
|
|
303
|
+
}),
|
|
304
|
+
i18n._({
|
|
305
|
+
id: "K8IUjY",
|
|
306
|
+
message: "mode: {0}",
|
|
307
|
+
values: { 0: currentSession.mode }
|
|
308
|
+
}),
|
|
309
|
+
i18n._({
|
|
310
|
+
id: "Oeb7jH",
|
|
311
|
+
message: "model: {0}",
|
|
312
|
+
values: { 0: currentSession.model }
|
|
313
|
+
})
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
if (parsed.type === "projects") try {
|
|
317
|
+
const result = await deps.listOpenProjects();
|
|
318
|
+
if (result.roots.length === 0) return i18n._({
|
|
319
|
+
id: "JvAWQ5",
|
|
320
|
+
message: "No working directories are currently open."
|
|
321
|
+
});
|
|
322
|
+
const lines = result.roots.map((root, index) => i18n._({
|
|
323
|
+
id: "j3bsc1",
|
|
324
|
+
message: "{0}. {root}",
|
|
325
|
+
values: {
|
|
326
|
+
root,
|
|
327
|
+
0: index + 1
|
|
328
|
+
}
|
|
329
|
+
}));
|
|
330
|
+
return [i18n._({
|
|
331
|
+
id: "miupLy",
|
|
332
|
+
message: "Current working directories:"
|
|
333
|
+
}), ...lines].join("\n");
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return formatProjectsError(error);
|
|
336
|
+
}
|
|
337
|
+
if (parsed.type === "reset") {
|
|
338
|
+
deps.clearSession(sessionKey);
|
|
339
|
+
return i18n._({
|
|
340
|
+
id: "Ck6rmi",
|
|
341
|
+
message: "Current session has been cleared."
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (parsed.type === "mode") {
|
|
345
|
+
if (!currentSession) return i18n._({
|
|
346
|
+
id: "mehbut",
|
|
347
|
+
message: "No active session. Send a normal message or use /new to create one first."
|
|
348
|
+
});
|
|
349
|
+
deps.setSession(sessionKey, {
|
|
350
|
+
...currentSession,
|
|
351
|
+
mode: parsed.mode
|
|
352
|
+
});
|
|
353
|
+
return i18n._({
|
|
354
|
+
id: "dXeZCW",
|
|
355
|
+
message: "Switched to {0} mode.",
|
|
356
|
+
values: { 0: parsed.mode }
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (parsed.type === "new") try {
|
|
360
|
+
const created = await deps.createThread(parsed.mode);
|
|
361
|
+
deps.setSession(sessionKey, created);
|
|
362
|
+
return [
|
|
363
|
+
i18n._({
|
|
364
|
+
id: "dZhhUX",
|
|
365
|
+
message: "Created a new session."
|
|
366
|
+
}),
|
|
367
|
+
i18n._({
|
|
368
|
+
id: "36QmnO",
|
|
369
|
+
message: "thread: {0}",
|
|
370
|
+
values: { 0: created.threadId }
|
|
371
|
+
}),
|
|
372
|
+
i18n._({
|
|
373
|
+
id: "4Vrm3r",
|
|
374
|
+
message: "cwd: {0}",
|
|
375
|
+
values: { 0: created.cwd }
|
|
376
|
+
}),
|
|
377
|
+
i18n._({
|
|
378
|
+
id: "K8IUjY",
|
|
379
|
+
message: "mode: {0}",
|
|
380
|
+
values: { 0: created.mode }
|
|
381
|
+
}),
|
|
382
|
+
i18n._({
|
|
383
|
+
id: "Oeb7jH",
|
|
384
|
+
message: "model: {0}",
|
|
385
|
+
values: { 0: created.model }
|
|
386
|
+
})
|
|
387
|
+
].join("\n");
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return formatCodexError(error);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const mode = currentSession?.mode ?? "default";
|
|
393
|
+
const result = await deps.runTurn({
|
|
394
|
+
prompt: parsed.prompt,
|
|
395
|
+
mode,
|
|
396
|
+
session: currentSession ?? null
|
|
397
|
+
});
|
|
398
|
+
const title = await resolveSessionTitle({
|
|
399
|
+
currentSession,
|
|
400
|
+
prompt: parsed.prompt,
|
|
401
|
+
mode,
|
|
402
|
+
runTurn: deps.runTurn
|
|
403
|
+
});
|
|
404
|
+
deps.setSession(sessionKey, {
|
|
405
|
+
threadId: result.threadId,
|
|
406
|
+
model: result.model,
|
|
407
|
+
mode: result.mode,
|
|
408
|
+
cwd: result.cwd,
|
|
409
|
+
title
|
|
410
|
+
});
|
|
411
|
+
return result.message;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return formatCodexError(error);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function formatCodexError(error) {
|
|
418
|
+
if (error instanceof Error && error.message.trim().length > 0) return i18n._({
|
|
419
|
+
id: "doQwEN",
|
|
420
|
+
message: "Codex execution failed: {0}",
|
|
421
|
+
values: { 0: error.message }
|
|
422
|
+
});
|
|
423
|
+
return i18n._({
|
|
424
|
+
id: "orhkAj",
|
|
425
|
+
message: "Codex execution failed. Please try again later."
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function formatProjectsError(error) {
|
|
429
|
+
if (error instanceof Error && error.message.trim().length > 0) return i18n._({
|
|
430
|
+
id: "MZ28Ys",
|
|
431
|
+
message: "Failed to read open projects: {0}",
|
|
432
|
+
values: { 0: error.message }
|
|
433
|
+
});
|
|
434
|
+
return i18n._({
|
|
435
|
+
id: "1xKjU/",
|
|
436
|
+
message: "Failed to read open projects. Please try again later."
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function resolveSessionTitle(input) {
|
|
440
|
+
const currentTitle = normalizeSessionTitle(input.currentSession?.title);
|
|
441
|
+
if (currentTitle) return currentTitle;
|
|
442
|
+
if (!input.currentSession) return;
|
|
443
|
+
return generateSessionTitleWithFallback({
|
|
444
|
+
prompt: input.prompt,
|
|
445
|
+
mode: input.mode,
|
|
446
|
+
runTurn: input.runTurn
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
async function generateSessionTitleWithFallback(input) {
|
|
450
|
+
const fallbackTitle = buildFallbackSessionTitle(input.prompt);
|
|
451
|
+
const titlePrompt = buildTitleGenerationPrompt(input.prompt);
|
|
452
|
+
try {
|
|
453
|
+
const sanitizedTitle = sanitizeGeneratedTitle((await input.runTurn({
|
|
454
|
+
prompt: titlePrompt,
|
|
455
|
+
mode: input.mode,
|
|
456
|
+
session: null
|
|
457
|
+
})).message);
|
|
458
|
+
if (sanitizedTitle) return sanitizedTitle;
|
|
459
|
+
logTitleGenerationFallback("generated title is empty after post-processing, using fallback");
|
|
460
|
+
} catch (error) {
|
|
461
|
+
logTitleGenerationFallback(`title generation request failed: ${formatErrorMessage(error)}`);
|
|
462
|
+
}
|
|
463
|
+
return fallbackTitle;
|
|
464
|
+
}
|
|
465
|
+
function buildFallbackSessionTitle(prompt) {
|
|
466
|
+
const normalizedPrompt = normalizePrompt(prompt);
|
|
467
|
+
if (normalizedPrompt.length === 0) return i18n._({
|
|
468
|
+
id: "HEx9te",
|
|
469
|
+
message: "New Session"
|
|
470
|
+
});
|
|
471
|
+
return truncateTitle(normalizedPrompt);
|
|
472
|
+
}
|
|
473
|
+
function normalizeSessionTitle(title) {
|
|
474
|
+
if (!title) return null;
|
|
475
|
+
const normalized = title.trim();
|
|
476
|
+
if (normalized.length === 0) return null;
|
|
477
|
+
return normalized;
|
|
478
|
+
}
|
|
479
|
+
function buildTitleGenerationPrompt(prompt) {
|
|
480
|
+
return [
|
|
481
|
+
getCurrentLocale() === "zh" ? i18n._({
|
|
482
|
+
id: "rdodSw",
|
|
483
|
+
message: "You are a session title generator.\nGenerate a short Chinese title based on the user message.\nStrict requirements:\n1. Output title text only, with no explanation.\n2. Output a single line with no line breaks.\n3. Do not use quotes or title marks.\n4. Keep the title within 24 characters."
|
|
484
|
+
}) : i18n._({
|
|
485
|
+
id: "Rn/EAE",
|
|
486
|
+
message: "You are a session title generator.\nGenerate a short English title based on the user message.\nStrict requirements:\n1. Output title text only, with no explanation.\n2. Output a single line with no line breaks.\n3. Do not use quotes.\n4. Keep the title within 24 characters."
|
|
487
|
+
}),
|
|
488
|
+
"",
|
|
489
|
+
i18n._({
|
|
490
|
+
id: "uAkDSp",
|
|
491
|
+
message: "User message: {0}",
|
|
492
|
+
values: { 0: normalizePrompt(prompt) }
|
|
493
|
+
})
|
|
494
|
+
].join("\n");
|
|
495
|
+
}
|
|
496
|
+
function sanitizeGeneratedTitle(rawTitle) {
|
|
497
|
+
const trimmed = rawTitle.trim();
|
|
498
|
+
if (trimmed.length === 0) return null;
|
|
499
|
+
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
|
500
|
+
if (firstLine.length === 0) return null;
|
|
501
|
+
const unquoted = stripWrappingQuotes(normalizePrompt(firstLine));
|
|
502
|
+
if (unquoted.length === 0) return null;
|
|
503
|
+
return truncateTitle(unquoted);
|
|
504
|
+
}
|
|
505
|
+
function stripWrappingQuotes(input) {
|
|
506
|
+
let value = input.trim();
|
|
507
|
+
let changed = true;
|
|
508
|
+
while (changed && value.length > 0) {
|
|
509
|
+
changed = false;
|
|
510
|
+
for (const [left, right] of WRAPPING_QUOTE_PAIRS) if (value.startsWith(left) && value.endsWith(right)) {
|
|
511
|
+
const inner = value.slice(left.length, value.length - right.length).trim();
|
|
512
|
+
if (inner !== value) {
|
|
513
|
+
value = inner;
|
|
514
|
+
changed = true;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
function truncateTitle(input) {
|
|
522
|
+
const chars = Array.from(input);
|
|
523
|
+
if (chars.length <= MAX_SESSION_TITLE_LENGTH) return input;
|
|
524
|
+
if (MAX_SESSION_TITLE_LENGTH <= 3) return chars.slice(0, MAX_SESSION_TITLE_LENGTH).join("");
|
|
525
|
+
return `${chars.slice(0, MAX_SESSION_TITLE_LENGTH - 3).join("")}...`;
|
|
526
|
+
}
|
|
527
|
+
function normalizePrompt(input) {
|
|
528
|
+
return input.replace(/\s+/g, " ").trim();
|
|
529
|
+
}
|
|
530
|
+
function formatErrorMessage(error) {
|
|
531
|
+
if (error instanceof Error && error.message.trim().length > 0) return error.message;
|
|
532
|
+
return String(error);
|
|
533
|
+
}
|
|
534
|
+
function logTitleGenerationFallback(message) {
|
|
535
|
+
console.warn(`[relay] ${message}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/bot/message-filter.ts
|
|
540
|
+
function shouldProcessMessage(event, botOpenId) {
|
|
541
|
+
if (isMessageFromBot(event, botOpenId)) return false;
|
|
542
|
+
if (event.message.chat_type === "p2p") return true;
|
|
543
|
+
return shouldHandleGroupMessage(event.message.mentions, botOpenId);
|
|
544
|
+
}
|
|
545
|
+
function shouldHandleGroupMessage(mentions, botOpenId) {
|
|
546
|
+
if (!mentions || mentions.length === 0) return false;
|
|
547
|
+
if (!botOpenId) return true;
|
|
548
|
+
return mentions.some((mention) => mention.id?.open_id === botOpenId);
|
|
549
|
+
}
|
|
550
|
+
function resolveSenderId(senderId) {
|
|
551
|
+
if (!senderId) return null;
|
|
552
|
+
return senderId.open_id ?? senderId.user_id ?? senderId.union_id ?? null;
|
|
553
|
+
}
|
|
554
|
+
function isMessageFromBot(event, botOpenId) {
|
|
555
|
+
if (event.sender.sender_type?.toLowerCase() === "app") return true;
|
|
556
|
+
if (!botOpenId) return false;
|
|
557
|
+
return resolveSenderId(event.sender.sender_id) === botOpenId;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/bot/relay.ts
|
|
562
|
+
async function buildReplyForMessageEvent(event, deps) {
|
|
563
|
+
if (isMessageFromBot(event, deps.botOpenId)) return null;
|
|
564
|
+
if (event.message.message_type !== "text") return i18n._({
|
|
565
|
+
id: "rHDhWM",
|
|
566
|
+
message: "Failed to parse message. Please send a text message."
|
|
567
|
+
});
|
|
568
|
+
const text = parseTextContent(event.message.content);
|
|
569
|
+
if (!text) return i18n._({
|
|
570
|
+
id: "rHDhWM",
|
|
571
|
+
message: "Failed to parse message. Please send a text message."
|
|
572
|
+
});
|
|
573
|
+
if (event.message.chat_type !== "p2p" && !shouldHandleGroupMessage(event.message.mentions, deps.botOpenId)) return null;
|
|
574
|
+
const senderId = resolveSenderId(event.sender.sender_id);
|
|
575
|
+
if (!senderId) return i18n._({
|
|
576
|
+
id: "smfLBQ",
|
|
577
|
+
message: "Cannot identify sender. Please try again later."
|
|
578
|
+
});
|
|
579
|
+
const normalizedText = stripMentionTags(text).trim();
|
|
580
|
+
if (normalizedText.length === 0) return i18n._({
|
|
581
|
+
id: "oEMyJf",
|
|
582
|
+
message: "Please send a text message."
|
|
583
|
+
});
|
|
584
|
+
return deps.handleIncomingText({
|
|
585
|
+
chatType: event.message.chat_type,
|
|
586
|
+
chatId: event.message.chat_id,
|
|
587
|
+
senderId,
|
|
588
|
+
text: normalizedText
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
function stripMentionTags(text) {
|
|
592
|
+
return text.replace(/<at\b[^>]*>.*?<\/at>/g, "").trim();
|
|
593
|
+
}
|
|
594
|
+
function parseTextContent(content) {
|
|
595
|
+
try {
|
|
596
|
+
const parsed = JSON.parse(content);
|
|
597
|
+
if (!isRecord$1(parsed)) return null;
|
|
598
|
+
return typeof parsed.text === "string" ? parsed.text : null;
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function isRecord$1(value) {
|
|
604
|
+
return typeof value === "object" && value !== null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region src/codex/rpc.ts
|
|
609
|
+
function parseRpcLine(line) {
|
|
610
|
+
let parsed;
|
|
611
|
+
try {
|
|
612
|
+
parsed = JSON.parse(line);
|
|
613
|
+
} catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
if (!isRecord(parsed)) return null;
|
|
617
|
+
if (typeof parsed.method === "string") {
|
|
618
|
+
if (isRpcRequestId(parsed.id)) return {
|
|
619
|
+
id: parsed.id,
|
|
620
|
+
method: parsed.method,
|
|
621
|
+
params: parsed.params
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
method: parsed.method,
|
|
625
|
+
params: parsed.params
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (!isRpcRequestId(parsed.id)) return null;
|
|
629
|
+
if ("error" in parsed && isRpcErrorObject(parsed.error)) return {
|
|
630
|
+
id: parsed.id,
|
|
631
|
+
error: parsed.error
|
|
632
|
+
};
|
|
633
|
+
if ("result" in parsed) return {
|
|
634
|
+
id: parsed.id,
|
|
635
|
+
result: parsed.result
|
|
636
|
+
};
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
function formatRpcError(error) {
|
|
640
|
+
return `Codex RPC error (${error.code}): ${error.message}`;
|
|
641
|
+
}
|
|
642
|
+
function isRecord(value) {
|
|
643
|
+
return typeof value === "object" && value !== null;
|
|
644
|
+
}
|
|
645
|
+
function isRpcRequestId(value) {
|
|
646
|
+
return typeof value === "number" || typeof value === "string";
|
|
647
|
+
}
|
|
648
|
+
function isRpcErrorObject(value) {
|
|
649
|
+
if (!isRecord(value)) return false;
|
|
650
|
+
return typeof value.code === "number" && typeof value.message === "string";
|
|
651
|
+
}
|
|
652
|
+
function isRpcErrorResponse(value) {
|
|
653
|
+
return "error" in value;
|
|
654
|
+
}
|
|
655
|
+
function isRpcSuccessResponse(value) {
|
|
656
|
+
return "result" in value;
|
|
657
|
+
}
|
|
658
|
+
function isRpcServerRequest(value) {
|
|
659
|
+
return "method" in value && "id" in value;
|
|
660
|
+
}
|
|
661
|
+
function getServerRequestResult(method) {
|
|
662
|
+
if (method === "item/commandExecution/requestApproval") return {
|
|
663
|
+
decision: "accept",
|
|
664
|
+
acceptSettings: { forSession: true }
|
|
665
|
+
};
|
|
666
|
+
if (method === "item/fileChange/requestApproval") return { decision: "accept" };
|
|
667
|
+
if (method.endsWith("/requestApproval")) return { decision: "accept" };
|
|
668
|
+
if (method === "execCommandApproval") return { decision: "allow" };
|
|
669
|
+
if (method === "applyPatchApproval") return { decision: "allow" };
|
|
670
|
+
if (method.endsWith("Approval")) return { decision: "allow" };
|
|
671
|
+
if (method === "item/tool/requestUserInput") return { answers: {} };
|
|
672
|
+
if (method === "item/tool/call") return {
|
|
673
|
+
success: false,
|
|
674
|
+
contentItems: [{
|
|
675
|
+
type: "inputText",
|
|
676
|
+
text: "Dynamic tool calls are unavailable in relay-bot."
|
|
677
|
+
}]
|
|
678
|
+
};
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/codex/app-server-client.ts
|
|
684
|
+
var CodexAppServerClient = class {
|
|
685
|
+
setNotificationHandler(handler) {
|
|
686
|
+
this.notificationHandler = handler;
|
|
687
|
+
}
|
|
688
|
+
async request(method, params) {
|
|
689
|
+
if (this.exited) throw new Error(this.buildExitMessage(null, null));
|
|
690
|
+
const requestId = this.nextId;
|
|
691
|
+
this.nextId += 1;
|
|
692
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
693
|
+
this.pending.set(requestId, {
|
|
694
|
+
resolve: (value) => resolve(value),
|
|
695
|
+
reject
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
const payload = JSON.stringify({
|
|
699
|
+
jsonrpc: "2.0",
|
|
700
|
+
id: requestId,
|
|
701
|
+
method,
|
|
702
|
+
params
|
|
703
|
+
});
|
|
704
|
+
await new Promise((resolve, reject) => {
|
|
705
|
+
this.child.stdin.write(`${payload}\n`, (error) => {
|
|
706
|
+
if (error) {
|
|
707
|
+
this.pending.delete(requestId);
|
|
708
|
+
reject(error);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
resolve();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
return responsePromise;
|
|
715
|
+
}
|
|
716
|
+
dispose() {
|
|
717
|
+
this.lineReader.close();
|
|
718
|
+
if (!this.child.killed) this.child.kill("SIGTERM");
|
|
719
|
+
}
|
|
720
|
+
handleStdoutLine(line) {
|
|
721
|
+
const parsed = parseRpcLine(line);
|
|
722
|
+
if (!parsed) return;
|
|
723
|
+
if ("method" in parsed) {
|
|
724
|
+
if (isRpcServerRequest(parsed)) {
|
|
725
|
+
this.respondToServerRequest(parsed).catch((error) => {
|
|
726
|
+
this.stderrBuffer.push(`failed to respond to server request "${parsed.method}": ${String(error)}`);
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
this.notificationHandler?.(parsed);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const pending = this.pending.get(parsed.id);
|
|
734
|
+
if (!pending) return;
|
|
735
|
+
this.pending.delete(parsed.id);
|
|
736
|
+
if (isRpcErrorResponse(parsed)) {
|
|
737
|
+
pending.reject(new Error(formatRpcError(parsed.error)));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (isRpcSuccessResponse(parsed)) pending.resolve(parsed.result);
|
|
741
|
+
}
|
|
742
|
+
async respondToServerRequest(request) {
|
|
743
|
+
const result = getServerRequestResult(request.method);
|
|
744
|
+
if (result !== null) {
|
|
745
|
+
await this.sendRpcResult(request.id, result);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
await this.sendRpcError(request.id, -32601, `Unsupported server request method: ${request.method}`);
|
|
749
|
+
}
|
|
750
|
+
async sendRpcResult(id, result) {
|
|
751
|
+
await this.writeRpcPayload({
|
|
752
|
+
jsonrpc: "2.0",
|
|
753
|
+
id,
|
|
754
|
+
result
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
async sendRpcError(id, code, message) {
|
|
758
|
+
await this.writeRpcPayload({
|
|
759
|
+
jsonrpc: "2.0",
|
|
760
|
+
id,
|
|
761
|
+
error: {
|
|
762
|
+
code,
|
|
763
|
+
message
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
async writeRpcPayload(payload) {
|
|
768
|
+
if (this.exited) return;
|
|
769
|
+
const serialized = JSON.stringify(payload);
|
|
770
|
+
await new Promise((resolve, reject) => {
|
|
771
|
+
this.child.stdin.write(`${serialized}\n`, (error) => {
|
|
772
|
+
if (error) {
|
|
773
|
+
reject(error);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
resolve();
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
buildExitMessage(code, signal) {
|
|
781
|
+
const suffix = this.stderrBuffer.length > 0 ? `; stderr: ${this.stderrBuffer.at(-1)}` : "";
|
|
782
|
+
return `Codex app-server exited (code=${code ?? "null"}, signal=${signal ?? "null"})${suffix}`;
|
|
783
|
+
}
|
|
784
|
+
constructor(options) {
|
|
785
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
786
|
+
this.stderrBuffer = [];
|
|
787
|
+
this.nextId = 1;
|
|
788
|
+
this.notificationHandler = null;
|
|
789
|
+
this.exited = false;
|
|
790
|
+
this.options = options;
|
|
791
|
+
this.child = spawn(this.options.codexBin, ["app-server"], {
|
|
792
|
+
cwd: this.options.cwd,
|
|
793
|
+
stdio: [
|
|
794
|
+
"pipe",
|
|
795
|
+
"pipe",
|
|
796
|
+
"pipe"
|
|
797
|
+
]
|
|
798
|
+
});
|
|
799
|
+
this.lineReader = createInterface({
|
|
800
|
+
input: this.child.stdout,
|
|
801
|
+
crlfDelay: Infinity
|
|
802
|
+
});
|
|
803
|
+
this.lineReader.on("line", (line) => {
|
|
804
|
+
this.handleStdoutLine(line);
|
|
805
|
+
});
|
|
806
|
+
this.child.stderr.on("data", (chunk) => {
|
|
807
|
+
const text = String(chunk).trim();
|
|
808
|
+
if (text.length > 0) this.stderrBuffer.push(text);
|
|
809
|
+
});
|
|
810
|
+
this.child.on("exit", (code, signal) => {
|
|
811
|
+
this.exited = true;
|
|
812
|
+
const error = new Error(this.buildExitMessage(code, signal));
|
|
813
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
814
|
+
this.pending.clear();
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/codex/thread.ts
|
|
821
|
+
async function initializeClient(client) {
|
|
822
|
+
await client.request("initialize", {
|
|
823
|
+
clientInfo: {
|
|
824
|
+
name: "relay-bot",
|
|
825
|
+
title: "Relay Bot",
|
|
826
|
+
version: "0.0.0"
|
|
827
|
+
},
|
|
828
|
+
capabilities: { experimentalApi: true }
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
async function getCollaborationModes(client) {
|
|
832
|
+
const raw = await client.request("collaborationMode/list", {});
|
|
833
|
+
if (!isCollaborationModeListResponse(raw)) throw new Error("Invalid collaboration mode response from Codex");
|
|
834
|
+
return raw.data;
|
|
835
|
+
}
|
|
836
|
+
async function openThread(client, session, cwd) {
|
|
837
|
+
if (!session) return startThread(client, cwd);
|
|
838
|
+
if (session.cwd !== cwd) return startThread(client, cwd);
|
|
839
|
+
try {
|
|
840
|
+
const resumed = await resumeThread(client, session.threadId);
|
|
841
|
+
if (resumed.cwd !== cwd) return startThread(client, cwd);
|
|
842
|
+
return resumed;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (isThreadMissingError(error)) return startThread(client, cwd);
|
|
845
|
+
throw error;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async function startThread(client, cwd) {
|
|
849
|
+
return parseThreadResult(await client.request("thread/start", {
|
|
850
|
+
cwd,
|
|
851
|
+
approvalPolicy: "on-request",
|
|
852
|
+
sandbox: "workspace-write",
|
|
853
|
+
experimentalRawEvents: false
|
|
854
|
+
}));
|
|
855
|
+
}
|
|
856
|
+
function selectCollaborationModePayload(masks, mode, model) {
|
|
857
|
+
const selected = masks.find((mask) => {
|
|
858
|
+
if (mask.mode === mode) return true;
|
|
859
|
+
return mask.name.toLowerCase() === mode;
|
|
860
|
+
});
|
|
861
|
+
if (!selected) throw new Error(`Collaboration mode "${mode}" is unavailable`);
|
|
862
|
+
return {
|
|
863
|
+
mode,
|
|
864
|
+
settings: {
|
|
865
|
+
model,
|
|
866
|
+
reasoning_effort: selected.reasoning_effort,
|
|
867
|
+
developer_instructions: selected.developer_instructions
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
async function resumeThread(client, threadId) {
|
|
872
|
+
return parseThreadResult(await client.request("thread/resume", { threadId }));
|
|
873
|
+
}
|
|
874
|
+
function parseThreadResult(raw) {
|
|
875
|
+
if (!isThreadResult(raw)) throw new Error("Invalid thread response from Codex");
|
|
876
|
+
return {
|
|
877
|
+
threadId: raw.thread.id,
|
|
878
|
+
model: raw.model,
|
|
879
|
+
cwd: raw.cwd
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function isThreadMissingError(error) {
|
|
883
|
+
if (!(error instanceof Error)) return false;
|
|
884
|
+
return error.message.includes("thread not found");
|
|
885
|
+
}
|
|
886
|
+
function isCollaborationModeMask(value) {
|
|
887
|
+
if (!isRecord(value)) return false;
|
|
888
|
+
const modeIsValid = value.mode === null || value.mode === "default" || value.mode === "plan";
|
|
889
|
+
return typeof value.name === "string" && modeIsValid && (typeof value.model === "string" || value.model === null) && (typeof value.reasoning_effort === "string" || value.reasoning_effort === null) && (typeof value.developer_instructions === "string" || value.developer_instructions === null);
|
|
890
|
+
}
|
|
891
|
+
function isCollaborationModeListResponse(value) {
|
|
892
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return false;
|
|
893
|
+
return value.data.every(isCollaborationModeMask);
|
|
894
|
+
}
|
|
895
|
+
function isThreadResult(value) {
|
|
896
|
+
if (!isRecord(value) || !isRecord(value.thread)) return false;
|
|
897
|
+
return typeof value.thread.id === "string" && typeof value.model === "string";
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/codex/turn-state.ts
|
|
902
|
+
function createTurnAccumulator() {
|
|
903
|
+
return {
|
|
904
|
+
turnCompleted: false,
|
|
905
|
+
turnError: null,
|
|
906
|
+
lastAgentMessageByItem: null,
|
|
907
|
+
lastAgentMessageByTask: null
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function applyTurnNotification(accumulator, notification) {
|
|
911
|
+
if (notification.method === "error") {
|
|
912
|
+
if (isRecord(notification.params) && typeof notification.params.message === "string") accumulator.turnError = notification.params.message;
|
|
913
|
+
else accumulator.turnError = "Codex returned an unknown error event";
|
|
914
|
+
accumulator.turnCompleted = true;
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (notification.method === "item/completed") {
|
|
918
|
+
const item = notification.params.item;
|
|
919
|
+
if (item?.type === "agentMessage" && typeof item.text === "string") accumulator.lastAgentMessageByItem = item.text;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (notification.method === "codex/event/task_complete") {
|
|
923
|
+
const message = notification.params.msg?.last_agent_message;
|
|
924
|
+
if (typeof message === "string") accumulator.lastAgentMessageByTask = message;
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (notification.method === "turn/completed") {
|
|
928
|
+
const params = notification.params;
|
|
929
|
+
accumulator.turnCompleted = true;
|
|
930
|
+
if (params.turn?.error?.message) {
|
|
931
|
+
accumulator.turnError = params.turn.error.message;
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (params.turn?.status === "failed") accumulator.turnError = "Codex turn failed";
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function resolveTurnMessage(accumulator) {
|
|
938
|
+
return accumulator.lastAgentMessageByTask ?? accumulator.lastAgentMessageByItem;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region src/codex/app-server.ts
|
|
943
|
+
const DEFAULT_CODEX_BIN$1 = "codex";
|
|
944
|
+
async function createCodexThread(input) {
|
|
945
|
+
const client = new CodexAppServerClient({
|
|
946
|
+
cwd: input.cwd,
|
|
947
|
+
codexBin: input.codexBin ?? DEFAULT_CODEX_BIN$1
|
|
948
|
+
});
|
|
949
|
+
try {
|
|
950
|
+
return await runWithOptionalTimeout(async () => {
|
|
951
|
+
await initializeClient(client);
|
|
952
|
+
const opened = await startThread(client, input.cwd);
|
|
953
|
+
return {
|
|
954
|
+
threadId: opened.threadId,
|
|
955
|
+
model: opened.model,
|
|
956
|
+
mode: input.mode,
|
|
957
|
+
cwd: opened.cwd
|
|
958
|
+
};
|
|
959
|
+
}, input.timeoutMs, () => client.dispose());
|
|
960
|
+
} finally {
|
|
961
|
+
client.dispose();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function runCodexTurn(input) {
|
|
965
|
+
const client = new CodexAppServerClient({
|
|
966
|
+
cwd: input.cwd,
|
|
967
|
+
codexBin: input.codexBin ?? DEFAULT_CODEX_BIN$1
|
|
968
|
+
});
|
|
969
|
+
const accumulator = createTurnAccumulator();
|
|
970
|
+
const turnDone = createDeferred();
|
|
971
|
+
let turnDoneResolved = false;
|
|
972
|
+
client.setNotificationHandler((notification) => {
|
|
973
|
+
applyTurnNotification(accumulator, notification);
|
|
974
|
+
if (accumulator.turnCompleted && !turnDoneResolved) {
|
|
975
|
+
turnDoneResolved = true;
|
|
976
|
+
turnDone.resolve();
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
try {
|
|
980
|
+
return await runWithOptionalTimeout(async () => {
|
|
981
|
+
await initializeClient(client);
|
|
982
|
+
const modeMasks = await getCollaborationModes(client);
|
|
983
|
+
const opened = await openThread(client, input.session, input.cwd);
|
|
984
|
+
const collaborationMode = selectCollaborationModePayload(modeMasks, input.mode, opened.model);
|
|
985
|
+
await client.request("turn/start", {
|
|
986
|
+
threadId: opened.threadId,
|
|
987
|
+
input: [{
|
|
988
|
+
type: "text",
|
|
989
|
+
text: input.prompt,
|
|
990
|
+
text_elements: []
|
|
991
|
+
}],
|
|
992
|
+
collaborationMode
|
|
993
|
+
});
|
|
994
|
+
await turnDone.promise;
|
|
995
|
+
if (accumulator.turnError) throw new Error(accumulator.turnError);
|
|
996
|
+
const message = resolveTurnMessage(accumulator);
|
|
997
|
+
if (!message || message.trim().length === 0) throw new Error("Codex did not return a message");
|
|
998
|
+
return {
|
|
999
|
+
threadId: opened.threadId,
|
|
1000
|
+
model: opened.model,
|
|
1001
|
+
mode: input.mode,
|
|
1002
|
+
message,
|
|
1003
|
+
cwd: opened.cwd
|
|
1004
|
+
};
|
|
1005
|
+
}, input.timeoutMs, () => {
|
|
1006
|
+
if (!turnDoneResolved) {
|
|
1007
|
+
turnDoneResolved = true;
|
|
1008
|
+
turnDone.reject(/* @__PURE__ */ new Error("Codex execution timed out"));
|
|
1009
|
+
}
|
|
1010
|
+
client.dispose();
|
|
1011
|
+
});
|
|
1012
|
+
} finally {
|
|
1013
|
+
client.dispose();
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async function runWithOptionalTimeout(run, timeoutMs, onTimeout) {
|
|
1017
|
+
if (typeof timeoutMs !== "number" || timeoutMs <= 0) return run();
|
|
1018
|
+
return withTimeout(run, timeoutMs, onTimeout);
|
|
1019
|
+
}
|
|
1020
|
+
function createDeferred() {
|
|
1021
|
+
let resolve;
|
|
1022
|
+
let reject;
|
|
1023
|
+
return {
|
|
1024
|
+
promise: new Promise((innerResolve, innerReject) => {
|
|
1025
|
+
resolve = innerResolve;
|
|
1026
|
+
reject = innerReject;
|
|
1027
|
+
}),
|
|
1028
|
+
resolve,
|
|
1029
|
+
reject
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
async function withTimeout(run, timeoutMs, onTimeout) {
|
|
1033
|
+
let timeoutHandle;
|
|
1034
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1035
|
+
timeoutHandle = setTimeout(() => {
|
|
1036
|
+
onTimeout();
|
|
1037
|
+
reject(/* @__PURE__ */ new Error(`Codex request timed out after ${timeoutMs}ms`));
|
|
1038
|
+
}, timeoutMs);
|
|
1039
|
+
});
|
|
1040
|
+
try {
|
|
1041
|
+
return await Promise.race([run(), timeoutPromise]);
|
|
1042
|
+
} finally {
|
|
1043
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/codex/state.ts
|
|
1049
|
+
async function listOpenProjects() {
|
|
1050
|
+
return { roots: [process.cwd()] };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region src/core/config.ts
|
|
1055
|
+
const DEFAULT_CODEX_BIN = "codex";
|
|
1056
|
+
const TEMPLATE_ENV_CONFIG = {
|
|
1057
|
+
BASE_DOMAIN: "https://open.feishu.cn",
|
|
1058
|
+
APP_ID: "your_app_id",
|
|
1059
|
+
APP_SECRET: "your_app_secret",
|
|
1060
|
+
BOT_OPEN_ID: "ou_xxx",
|
|
1061
|
+
CODEX_BIN: DEFAULT_CODEX_BIN,
|
|
1062
|
+
CODEX_TIMEOUT_MS: null
|
|
1063
|
+
};
|
|
1064
|
+
const TEMPLATE_CONFIG = {
|
|
1065
|
+
locale: getDefaultLocale(),
|
|
1066
|
+
env: TEMPLATE_ENV_CONFIG
|
|
1067
|
+
};
|
|
1068
|
+
function loadRelayConfig(options = {}) {
|
|
1069
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
1070
|
+
const workspaceCwd = options.workspaceCwd ?? process.cwd();
|
|
1071
|
+
const configDir = path.join(homeDir, ".relay");
|
|
1072
|
+
const configPath = path.join(configDir, "config.json");
|
|
1073
|
+
if (!fs.existsSync(configPath)) {
|
|
1074
|
+
ensureConfigTemplate(configDir, configPath);
|
|
1075
|
+
throw new Error(i18n._({
|
|
1076
|
+
id: "AWIU5i",
|
|
1077
|
+
message: "Relay config is missing. Template created at {configPath}. Please edit this file and restart.",
|
|
1078
|
+
values: { configPath }
|
|
1079
|
+
}));
|
|
1080
|
+
}
|
|
1081
|
+
const parsed = parseConfigFile(configPath);
|
|
1082
|
+
const locale = readLocale(parsed.localeValue);
|
|
1083
|
+
initializeI18n(locale);
|
|
1084
|
+
const domain = readRequiredString(parsed.env.BASE_DOMAIN, "BASE_DOMAIN");
|
|
1085
|
+
return {
|
|
1086
|
+
baseConfig: {
|
|
1087
|
+
appId: readRequiredString(parsed.env.APP_ID, "APP_ID"),
|
|
1088
|
+
appSecret: readRequiredString(parsed.env.APP_SECRET, "APP_SECRET"),
|
|
1089
|
+
domain
|
|
1090
|
+
},
|
|
1091
|
+
botOpenId: readOptionalString(parsed.env.BOT_OPEN_ID, "BOT_OPEN_ID"),
|
|
1092
|
+
codexBin: readOptionalString(parsed.env.CODEX_BIN, "CODEX_BIN") ?? DEFAULT_CODEX_BIN,
|
|
1093
|
+
codexTimeoutMs: readTimeoutMs(parsed.env.CODEX_TIMEOUT_MS),
|
|
1094
|
+
workspaceCwd,
|
|
1095
|
+
locale
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function ensureConfigTemplate(configDir, configPath) {
|
|
1099
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1100
|
+
if (fs.existsSync(configPath)) return;
|
|
1101
|
+
fs.writeFileSync(configPath, `${JSON.stringify(TEMPLATE_CONFIG, null, 2)}\n`, {
|
|
1102
|
+
encoding: "utf-8",
|
|
1103
|
+
flag: "wx"
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
function parseConfigFile(configPath) {
|
|
1107
|
+
let raw;
|
|
1108
|
+
try {
|
|
1109
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
throw new Error(i18n._({
|
|
1112
|
+
id: "tZQUtS",
|
|
1113
|
+
message: "Failed to read relay config at {configPath}: {0}",
|
|
1114
|
+
values: {
|
|
1115
|
+
configPath,
|
|
1116
|
+
0: formatError(error)
|
|
1117
|
+
}
|
|
1118
|
+
}));
|
|
1119
|
+
}
|
|
1120
|
+
let parsed;
|
|
1121
|
+
try {
|
|
1122
|
+
parsed = JSON.parse(raw);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
throw new Error(i18n._({
|
|
1125
|
+
id: "z+7ZYe",
|
|
1126
|
+
message: "Invalid JSON in relay config at {configPath}: {0}",
|
|
1127
|
+
values: {
|
|
1128
|
+
configPath,
|
|
1129
|
+
0: formatError(error)
|
|
1130
|
+
}
|
|
1131
|
+
}));
|
|
1132
|
+
}
|
|
1133
|
+
if (!isObject(parsed)) throw new Error(i18n._({
|
|
1134
|
+
id: "4IDydv",
|
|
1135
|
+
message: "Invalid relay config at {configPath}: root must be a JSON object.",
|
|
1136
|
+
values: { configPath }
|
|
1137
|
+
}));
|
|
1138
|
+
const configObject = parsed;
|
|
1139
|
+
if (configObject.env === void 0) return {
|
|
1140
|
+
env: configObject,
|
|
1141
|
+
localeValue: configObject.locale ?? configObject.LOCALE
|
|
1142
|
+
};
|
|
1143
|
+
if (!isObject(configObject.env)) throw new Error(i18n._({
|
|
1144
|
+
id: "CfFOzJ",
|
|
1145
|
+
message: "Invalid relay config at {configPath}: env must be a JSON object.",
|
|
1146
|
+
values: { configPath }
|
|
1147
|
+
}));
|
|
1148
|
+
return {
|
|
1149
|
+
env: configObject.env,
|
|
1150
|
+
localeValue: configObject.locale ?? configObject.LOCALE
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function readRequiredString(value, field) {
|
|
1154
|
+
const normalized = readOptionalString(value, field);
|
|
1155
|
+
if (!normalized) throw new Error(i18n._({
|
|
1156
|
+
id: "6tcMXX",
|
|
1157
|
+
message: "Invalid relay config: {field} is required and must be a non-empty string.",
|
|
1158
|
+
values: { field }
|
|
1159
|
+
}));
|
|
1160
|
+
return normalized;
|
|
1161
|
+
}
|
|
1162
|
+
function readOptionalString(value, field) {
|
|
1163
|
+
if (value === void 0) return;
|
|
1164
|
+
if (typeof value !== "string") throw new TypeError(i18n._({
|
|
1165
|
+
id: "Ks6r4a",
|
|
1166
|
+
message: "Invalid relay config: {field} must be a string.",
|
|
1167
|
+
values: { field }
|
|
1168
|
+
}));
|
|
1169
|
+
const normalized = value.trim();
|
|
1170
|
+
if (normalized.length === 0) return;
|
|
1171
|
+
return normalized;
|
|
1172
|
+
}
|
|
1173
|
+
function readTimeoutMs(value) {
|
|
1174
|
+
if (value === void 0 || value === null) return;
|
|
1175
|
+
if (typeof value === "number") {
|
|
1176
|
+
if (Number.isInteger(value) && value > 0) return value;
|
|
1177
|
+
throw new Error(i18n._({
|
|
1178
|
+
id: "Hrkm8q",
|
|
1179
|
+
message: "Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer."
|
|
1180
|
+
}));
|
|
1181
|
+
}
|
|
1182
|
+
if (typeof value === "string") {
|
|
1183
|
+
const trimmed = value.trim();
|
|
1184
|
+
if (trimmed.length === 0) return;
|
|
1185
|
+
if (!/^[1-9]\d*$/.test(trimmed)) throw new Error(i18n._({
|
|
1186
|
+
id: "Hrkm8q",
|
|
1187
|
+
message: "Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer."
|
|
1188
|
+
}));
|
|
1189
|
+
return Number.parseInt(trimmed, 10);
|
|
1190
|
+
}
|
|
1191
|
+
throw new Error(i18n._({
|
|
1192
|
+
id: "Hrkm8q",
|
|
1193
|
+
message: "Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer."
|
|
1194
|
+
}));
|
|
1195
|
+
}
|
|
1196
|
+
function readLocale(value) {
|
|
1197
|
+
const defaultLocale = getDefaultLocale();
|
|
1198
|
+
if (value === void 0 || value === null) return defaultLocale;
|
|
1199
|
+
if (typeof value !== "string") {
|
|
1200
|
+
console.warn(i18n._({
|
|
1201
|
+
id: "Nkzhzf",
|
|
1202
|
+
message: "Invalid relay config: locale \"{0}\" is not supported. Falling back to en.",
|
|
1203
|
+
values: { 0: formatInvalidLocale(value) }
|
|
1204
|
+
}));
|
|
1205
|
+
return defaultLocale;
|
|
1206
|
+
}
|
|
1207
|
+
const normalized = value.trim();
|
|
1208
|
+
if (normalized.length === 0) return defaultLocale;
|
|
1209
|
+
if (isSupportedLocale(normalized)) return normalized;
|
|
1210
|
+
console.warn(i18n._({
|
|
1211
|
+
id: "D8aQGU",
|
|
1212
|
+
message: "Invalid relay config: locale \"{normalized}\" is not supported. Falling back to en.",
|
|
1213
|
+
values: { normalized }
|
|
1214
|
+
}));
|
|
1215
|
+
return defaultLocale;
|
|
1216
|
+
}
|
|
1217
|
+
function formatInvalidLocale(value) {
|
|
1218
|
+
if (typeof value === "string") return value;
|
|
1219
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1220
|
+
try {
|
|
1221
|
+
return JSON.stringify(value);
|
|
1222
|
+
} catch {
|
|
1223
|
+
return String(value);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function isObject(value) {
|
|
1227
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1228
|
+
}
|
|
1229
|
+
function formatError(error) {
|
|
1230
|
+
if (error instanceof Error) return error.message;
|
|
1231
|
+
return String(error);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
//#endregion
|
|
1235
|
+
//#region src/core/startup.ts
|
|
1236
|
+
function loadConfigOrExit() {
|
|
1237
|
+
try {
|
|
1238
|
+
return loadRelayConfig();
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
console.error(formatStartupError(error));
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function formatStartupError(error) {
|
|
1245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1246
|
+
return i18n._({
|
|
1247
|
+
id: "aQiwam",
|
|
1248
|
+
message: "Failed to start relay: {message}",
|
|
1249
|
+
values: { message }
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/feishu/reply.ts
|
|
1255
|
+
const FALLBACK_REPLY_TAG = "[no-thread]";
|
|
1256
|
+
async function sendReply(larkClient, data, text) {
|
|
1257
|
+
const content = JSON.stringify({ text: formatReplyTextWithThreadId(data, text) });
|
|
1258
|
+
if (data.message.chat_type === "p2p") {
|
|
1259
|
+
await larkClient.im.v1.message.create({
|
|
1260
|
+
params: { receive_id_type: "chat_id" },
|
|
1261
|
+
data: {
|
|
1262
|
+
receive_id: data.message.chat_id,
|
|
1263
|
+
msg_type: "text",
|
|
1264
|
+
content
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
await larkClient.im.v1.message.reply({
|
|
1270
|
+
path: { message_id: data.message.message_id },
|
|
1271
|
+
data: {
|
|
1272
|
+
msg_type: "text",
|
|
1273
|
+
content
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
function formatReplyTextWithThreadId(data, text) {
|
|
1278
|
+
const replyTag = resolveReplyTag(data);
|
|
1279
|
+
const normalizedText = text.trim();
|
|
1280
|
+
if (normalizedText.length === 0) return `${replyTag}\n`;
|
|
1281
|
+
return `${replyTag}\n\n${normalizedText}`;
|
|
1282
|
+
}
|
|
1283
|
+
function resolveReplyTag(data) {
|
|
1284
|
+
const senderId = resolveSenderId(data.sender.sender_id);
|
|
1285
|
+
if (!senderId) return FALLBACK_REPLY_TAG;
|
|
1286
|
+
const session = getSession(getSessionKey({
|
|
1287
|
+
chatType: data.message.chat_type,
|
|
1288
|
+
chatId: data.message.chat_id,
|
|
1289
|
+
userId: senderId
|
|
1290
|
+
}));
|
|
1291
|
+
if (!session || session.threadId.trim().length === 0) return FALLBACK_REPLY_TAG;
|
|
1292
|
+
return `[${session.threadId}]`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region src/index.ts
|
|
1297
|
+
const relayConfig = loadConfigOrExit();
|
|
1298
|
+
initializeI18n(relayConfig.locale);
|
|
1299
|
+
const BUSY_MESSAGE = i18n._({
|
|
1300
|
+
id: "H7VlDR",
|
|
1301
|
+
message: "Currently busy. Please try again later."
|
|
1302
|
+
});
|
|
1303
|
+
const client = new Lark.Client(relayConfig.baseConfig);
|
|
1304
|
+
const wsClient = new Lark.WSClient(relayConfig.baseConfig);
|
|
1305
|
+
let isTaskRunning = false;
|
|
1306
|
+
async function processIncomingEvent(data) {
|
|
1307
|
+
try {
|
|
1308
|
+
const reply = await buildReplyForMessageEvent(data, {
|
|
1309
|
+
botOpenId: relayConfig.botOpenId,
|
|
1310
|
+
handleIncomingText: (input) => handleIncomingText(input, {
|
|
1311
|
+
createThread: (mode) => createCodexThread({
|
|
1312
|
+
mode,
|
|
1313
|
+
cwd: relayConfig.workspaceCwd,
|
|
1314
|
+
codexBin: relayConfig.codexBin,
|
|
1315
|
+
timeoutMs: relayConfig.codexTimeoutMs
|
|
1316
|
+
}),
|
|
1317
|
+
runTurn: (params) => runCodexTurn({
|
|
1318
|
+
...params,
|
|
1319
|
+
cwd: relayConfig.workspaceCwd,
|
|
1320
|
+
codexBin: relayConfig.codexBin,
|
|
1321
|
+
timeoutMs: relayConfig.codexTimeoutMs
|
|
1322
|
+
}),
|
|
1323
|
+
getSession,
|
|
1324
|
+
setSession,
|
|
1325
|
+
clearSession,
|
|
1326
|
+
withSessionLock,
|
|
1327
|
+
listOpenProjects
|
|
1328
|
+
})
|
|
1329
|
+
});
|
|
1330
|
+
if (reply === null) return;
|
|
1331
|
+
await sendReply(client, data, reply);
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
console.error("failed to handle Feishu message", error);
|
|
1334
|
+
try {
|
|
1335
|
+
await sendReply(client, data, i18n._({
|
|
1336
|
+
id: "FZcpfm",
|
|
1337
|
+
message: "Failed to process message. Please try again later."
|
|
1338
|
+
}));
|
|
1339
|
+
} catch (replyError) {
|
|
1340
|
+
console.error("failed to send failure message", replyError);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const eventDispatcher = new Lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
|
|
1345
|
+
console.info("feishu message received\n", JSON.stringify(data, null, 2), "\n");
|
|
1346
|
+
if (!shouldProcessMessage(data, relayConfig.botOpenId)) return;
|
|
1347
|
+
if (isTaskRunning) {
|
|
1348
|
+
sendReply(client, data, BUSY_MESSAGE);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
isTaskRunning = true;
|
|
1352
|
+
processIncomingEvent(data).finally(() => {
|
|
1353
|
+
isTaskRunning = false;
|
|
1354
|
+
});
|
|
1355
|
+
} });
|
|
1356
|
+
wsClient.start({ eventDispatcher });
|
|
1357
|
+
|
|
1358
|
+
//#endregion
|
|
1359
|
+
export { };
|
|
1360
|
+
//# sourceMappingURL=index.mjs.map
|