@debbl/relay 0.0.0 → 0.0.2
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 +10 -3
- package/README.zh.md +10 -3
- package/dist/index.mjs +367 -195
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,84 +1,13 @@
|
|
|
1
|
+
import process from "node:process";
|
|
1
2
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
3
|
import { i18n } from "@lingui/core";
|
|
4
|
+
import { isPlainObject } from "es-toolkit/predicate";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
3
7
|
import { spawn } from "node:child_process";
|
|
4
8
|
import { createInterface } from "node:readline";
|
|
5
|
-
import process from "node:process";
|
|
6
|
-
import fs from "node:fs";
|
|
7
9
|
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
10
|
|
|
81
|
-
//#endregion
|
|
82
11
|
//#region src/bot/commands.ts
|
|
83
12
|
const COMMAND_HELP = "/help";
|
|
84
13
|
const COMMAND_NEW = "/new";
|
|
@@ -255,17 +184,238 @@ function parseMode(input) {
|
|
|
255
184
|
return null;
|
|
256
185
|
}
|
|
257
186
|
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/session/store.ts
|
|
189
|
+
const sessionStore = /* @__PURE__ */ new Map();
|
|
190
|
+
const sessionQueue = /* @__PURE__ */ new Map();
|
|
191
|
+
const SESSION_FILE_NAME = "sessions.json";
|
|
192
|
+
const SESSION_FILE_VERSION = 1;
|
|
193
|
+
let persistenceState = null;
|
|
194
|
+
function initializeSessionStore(input) {
|
|
195
|
+
const relayDir = path.join(input.homeDir, ".relay");
|
|
196
|
+
const filePath = path.join(relayDir, SESSION_FILE_NAME);
|
|
197
|
+
fs.mkdirSync(relayDir, { recursive: true });
|
|
198
|
+
ensureSessionFileExists(filePath);
|
|
199
|
+
const persisted = readPersistedSessionsFile(filePath);
|
|
200
|
+
const workspaceSessions = persisted.workspaces[input.workspaceCwd];
|
|
201
|
+
sessionStore.clear();
|
|
202
|
+
sessionQueue.clear();
|
|
203
|
+
if (workspaceSessions) {
|
|
204
|
+
if (workspaceSessions.activeBySessionKey) {
|
|
205
|
+
const sessionRef = workspaceSessions.activeBySessionKey;
|
|
206
|
+
sessionStore.set(sessionRef.sessionKey, hydrateSession(sessionRef, input.workspaceCwd));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
persistenceState = {
|
|
210
|
+
filePath,
|
|
211
|
+
workspaceCwd: input.workspaceCwd,
|
|
212
|
+
data: persisted
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function getSessionKey(input) {
|
|
216
|
+
if (input.chatType === "p2p") return `p2p:${input.chatId}`;
|
|
217
|
+
return `group:${input.chatId}:${input.userId}`;
|
|
218
|
+
}
|
|
219
|
+
function getSession(sessionKey) {
|
|
220
|
+
return sessionStore.get(sessionKey);
|
|
221
|
+
}
|
|
222
|
+
function setSession(sessionKey, session) {
|
|
223
|
+
sessionStore.set(sessionKey, session);
|
|
224
|
+
persistSetSession(sessionKey, session);
|
|
225
|
+
}
|
|
226
|
+
function clearSession(sessionKey) {
|
|
227
|
+
sessionStore.delete(sessionKey);
|
|
228
|
+
persistClearSession(sessionKey);
|
|
229
|
+
}
|
|
230
|
+
async function withSessionLock(sessionKey, run) {
|
|
231
|
+
const running = (sessionQueue.get(sessionKey) ?? Promise.resolve()).then(() => run(), () => run());
|
|
232
|
+
const queueItem = running.then(() => void 0, () => void 0);
|
|
233
|
+
sessionQueue.set(sessionKey, queueItem);
|
|
234
|
+
try {
|
|
235
|
+
return await running;
|
|
236
|
+
} finally {
|
|
237
|
+
if (sessionQueue.get(sessionKey) === queueItem) sessionQueue.delete(sessionKey);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function persistSetSession(sessionKey, session) {
|
|
241
|
+
const state = persistenceState;
|
|
242
|
+
if (!state) return;
|
|
243
|
+
const savedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
244
|
+
const activeSession = toPersistedActiveSession(sessionKey, session, savedAt);
|
|
245
|
+
const historySession = toPersistedSessionSnapshot(session, savedAt);
|
|
246
|
+
const workspaceSessions = getOrCreateWorkspaceSessions(state.data, state.workspaceCwd);
|
|
247
|
+
workspaceSessions.activeBySessionKey = activeSession;
|
|
248
|
+
const history = workspaceSessions.historyBySessionKey[session.threadId] ?? [];
|
|
249
|
+
history.push(historySession);
|
|
250
|
+
workspaceSessions.historyBySessionKey[session.threadId] = history;
|
|
251
|
+
state.data.updatedAt = savedAt;
|
|
252
|
+
writePersistedSessionsFile(state.filePath, state.data);
|
|
253
|
+
}
|
|
254
|
+
function persistClearSession(sessionKey) {
|
|
255
|
+
const state = persistenceState;
|
|
256
|
+
if (!state) return;
|
|
257
|
+
const workspaceSessions = state.data.workspaces[state.workspaceCwd];
|
|
258
|
+
if (!workspaceSessions) return;
|
|
259
|
+
if (!workspaceSessions.activeBySessionKey || workspaceSessions.activeBySessionKey.sessionKey !== sessionKey) return;
|
|
260
|
+
workspaceSessions.activeBySessionKey = null;
|
|
261
|
+
state.data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
262
|
+
writePersistedSessionsFile(state.filePath, state.data);
|
|
263
|
+
}
|
|
264
|
+
function ensureSessionFileExists(filePath) {
|
|
265
|
+
if (fs.existsSync(filePath)) return;
|
|
266
|
+
const initialContent = `${JSON.stringify(createEmptyPersistedSessionsFile(), null, 2)}\n`;
|
|
267
|
+
fs.writeFileSync(filePath, initialContent, {
|
|
268
|
+
encoding: "utf-8",
|
|
269
|
+
flag: "wx"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function createEmptyPersistedSessionsFile() {
|
|
273
|
+
return {
|
|
274
|
+
version: SESSION_FILE_VERSION,
|
|
275
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
276
|
+
workspaces: {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function readPersistedSessionsFile(filePath) {
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new Error(`Failed to read relay session index at ${filePath}: ${formatError$1(error)}`);
|
|
285
|
+
}
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(raw);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw new Error(`Invalid JSON in relay session index at ${filePath}: ${formatError$1(error)}`);
|
|
291
|
+
}
|
|
292
|
+
return parsePersistedSessionsFile(parsed, filePath);
|
|
293
|
+
}
|
|
294
|
+
function parsePersistedSessionsFile(value, filePath) {
|
|
295
|
+
if (!isObject$1(value)) throw new Error(`Invalid relay session index at ${filePath}: root must be a JSON object.`);
|
|
296
|
+
if (value.version !== SESSION_FILE_VERSION) throw new Error(`Invalid relay session index at ${filePath}: version must be ${SESSION_FILE_VERSION}.`);
|
|
297
|
+
if (typeof value.updatedAt !== "string") throw new TypeError(`Invalid relay session index at ${filePath}: updatedAt must be a string.`);
|
|
298
|
+
if (!isObject$1(value.workspaces)) throw new Error(`Invalid relay session index at ${filePath}: workspaces must be a JSON object.`);
|
|
299
|
+
const workspaces = {};
|
|
300
|
+
for (const [workspaceCwd, workspaceValue] of Object.entries(value.workspaces)) workspaces[workspaceCwd] = parseWorkspaceSessions(workspaceValue, filePath, workspaceCwd);
|
|
301
|
+
return {
|
|
302
|
+
version: SESSION_FILE_VERSION,
|
|
303
|
+
updatedAt: value.updatedAt,
|
|
304
|
+
workspaces
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function parseWorkspaceSessions(value, filePath, workspaceCwd) {
|
|
308
|
+
if (!isObject$1(value)) throw new Error(`Invalid relay session index at ${filePath}: workspace "${workspaceCwd}" must be a JSON object.`);
|
|
309
|
+
return {
|
|
310
|
+
activeBySessionKey: parseWorkspaceActiveSession(value.activeBySessionKey, filePath, workspaceCwd),
|
|
311
|
+
historyBySessionKey: parseWorkspaceHistorySessions(value.historyBySessionKey, filePath, workspaceCwd)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function parseWorkspaceActiveSession(value, filePath, workspaceCwd) {
|
|
315
|
+
if (value === null || value === void 0) return null;
|
|
316
|
+
if (!isObject$1(value)) throw new Error(`Invalid relay session index at ${filePath}: activeBySessionKey for workspace "${workspaceCwd}" must be a JSON object or null.`);
|
|
317
|
+
return parsePersistedActiveSession(value, filePath, `activeBySessionKey for workspace "${workspaceCwd}"`);
|
|
318
|
+
}
|
|
319
|
+
function parseWorkspaceHistorySessions(value, filePath, workspaceCwd) {
|
|
320
|
+
if (!isObject$1(value)) throw new Error(`Invalid relay session index at ${filePath}: historyBySessionKey for workspace "${workspaceCwd}" must be a JSON object.`);
|
|
321
|
+
const historyBySessionKey = {};
|
|
322
|
+
for (const [entryKey, historyValue] of Object.entries(value)) {
|
|
323
|
+
if (entryKey.trim().length === 0) throw new Error(`Invalid relay session index at ${filePath}: historyBySessionKey key in workspace "${workspaceCwd}" must be a non-empty threadId.`);
|
|
324
|
+
if (!Array.isArray(historyValue)) throw new TypeError(`Invalid relay session index at ${filePath}: historyBySessionKey.${entryKey} must be an array.`);
|
|
325
|
+
historyBySessionKey[entryKey] = historyValue.map((item, index) => parsePersistedSessionSnapshot(item, filePath, `historyBySessionKey.${entryKey}[${index}]`));
|
|
326
|
+
}
|
|
327
|
+
return historyBySessionKey;
|
|
328
|
+
}
|
|
329
|
+
function parsePersistedActiveSession(value, filePath, location) {
|
|
330
|
+
if (!isObject$1(value)) throw new Error(`Invalid relay session index at ${filePath}: ${location} must be a JSON object.`);
|
|
331
|
+
return {
|
|
332
|
+
sessionKey: parseNonEmptyString(value.sessionKey, filePath, `${location}.sessionKey`),
|
|
333
|
+
threadId: parseNonEmptyString(value.threadId, filePath, `${location}.threadId`),
|
|
334
|
+
...parsePersistedSessionSnapshot(value, filePath, location)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function parsePersistedSessionSnapshot(value, filePath, location) {
|
|
338
|
+
if (!isObject$1(value)) throw new Error(`Invalid relay session index at ${filePath}: ${location} must be a JSON object.`);
|
|
339
|
+
const model = parseNonEmptyString(value.model, filePath, `${location}.model`);
|
|
340
|
+
if (value.mode !== "default" && value.mode !== "plan") throw new Error(`Invalid relay session index at ${filePath}: ${location}.mode must be "default" or "plan".`);
|
|
341
|
+
if (typeof value.savedAt !== "string") throw new TypeError(`Invalid relay session index at ${filePath}: ${location}.savedAt must be a string.`);
|
|
342
|
+
const title = normalizeOptionalTitle(value.title);
|
|
343
|
+
return {
|
|
344
|
+
mode: value.mode,
|
|
345
|
+
model,
|
|
346
|
+
title,
|
|
347
|
+
savedAt: value.savedAt
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function parseNonEmptyString(value, filePath, location) {
|
|
351
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`Invalid relay session index at ${filePath}: ${location} must be a non-empty string.`);
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function hydrateSession(sessionRef, cwd) {
|
|
355
|
+
return {
|
|
356
|
+
threadId: sessionRef.threadId,
|
|
357
|
+
mode: sessionRef.mode,
|
|
358
|
+
model: sessionRef.model,
|
|
359
|
+
cwd,
|
|
360
|
+
title: normalizeOptionalTitle(sessionRef.title)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function toPersistedActiveSession(sessionKey, session, savedAt) {
|
|
364
|
+
return {
|
|
365
|
+
sessionKey,
|
|
366
|
+
threadId: session.threadId,
|
|
367
|
+
...toPersistedSessionSnapshot(session, savedAt)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function toPersistedSessionSnapshot(session, savedAt) {
|
|
371
|
+
const title = normalizeOptionalTitle(session.title);
|
|
372
|
+
return {
|
|
373
|
+
mode: session.mode,
|
|
374
|
+
model: session.model,
|
|
375
|
+
title,
|
|
376
|
+
savedAt
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function normalizeOptionalTitle(title) {
|
|
380
|
+
if (typeof title !== "string") return;
|
|
381
|
+
const normalized = title.trim();
|
|
382
|
+
if (normalized.length === 0) return;
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
|
385
|
+
function getOrCreateWorkspaceSessions(data, workspaceCwd) {
|
|
386
|
+
const existing = data.workspaces[workspaceCwd];
|
|
387
|
+
if (existing) return existing;
|
|
388
|
+
const created = {
|
|
389
|
+
activeBySessionKey: null,
|
|
390
|
+
historyBySessionKey: {}
|
|
391
|
+
};
|
|
392
|
+
data.workspaces[workspaceCwd] = created;
|
|
393
|
+
return created;
|
|
394
|
+
}
|
|
395
|
+
function writePersistedSessionsFile(filePath, data) {
|
|
396
|
+
const tempPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
397
|
+
const content = `${JSON.stringify(data, null, 2)}\n`;
|
|
398
|
+
try {
|
|
399
|
+
fs.writeFileSync(tempPath, content, "utf-8");
|
|
400
|
+
fs.renameSync(tempPath, filePath);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
try {
|
|
403
|
+
if (fs.existsSync(tempPath)) fs.rmSync(tempPath, { force: true });
|
|
404
|
+
} catch {}
|
|
405
|
+
throw new Error(`Failed to write relay session index at ${filePath}: ${formatError$1(error)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function isObject$1(value) {
|
|
409
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
410
|
+
}
|
|
411
|
+
function formatError$1(error) {
|
|
412
|
+
if (error instanceof Error) return error.message;
|
|
413
|
+
return String(error);
|
|
414
|
+
}
|
|
415
|
+
|
|
258
416
|
//#endregion
|
|
259
417
|
//#region src/bot/handler.ts
|
|
260
418
|
const MAX_SESSION_TITLE_LENGTH = 24;
|
|
261
|
-
const WRAPPING_QUOTE_PAIRS = [
|
|
262
|
-
["\"", "\""],
|
|
263
|
-
["'", "'"],
|
|
264
|
-
["“", "”"],
|
|
265
|
-
["‘", "’"],
|
|
266
|
-
["「", "」"],
|
|
267
|
-
["《", "》"]
|
|
268
|
-
];
|
|
269
419
|
async function handleIncomingText(input, deps) {
|
|
270
420
|
const sessionKey = getSessionKey({
|
|
271
421
|
chatType: input.chatType,
|
|
@@ -397,9 +547,7 @@ async function handleIncomingText(input, deps) {
|
|
|
397
547
|
});
|
|
398
548
|
const title = await resolveSessionTitle({
|
|
399
549
|
currentSession,
|
|
400
|
-
prompt: parsed.prompt
|
|
401
|
-
mode,
|
|
402
|
-
runTurn: deps.runTurn
|
|
550
|
+
prompt: parsed.prompt
|
|
403
551
|
});
|
|
404
552
|
deps.setSession(sessionKey, {
|
|
405
553
|
threadId: result.threadId,
|
|
@@ -440,27 +588,7 @@ async function resolveSessionTitle(input) {
|
|
|
440
588
|
const currentTitle = normalizeSessionTitle(input.currentSession?.title);
|
|
441
589
|
if (currentTitle) return currentTitle;
|
|
442
590
|
if (!input.currentSession) return;
|
|
443
|
-
return
|
|
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;
|
|
591
|
+
return buildFallbackSessionTitle(input.prompt);
|
|
464
592
|
}
|
|
465
593
|
function buildFallbackSessionTitle(prompt) {
|
|
466
594
|
const normalizedPrompt = normalizePrompt(prompt);
|
|
@@ -476,48 +604,6 @@ function normalizeSessionTitle(title) {
|
|
|
476
604
|
if (normalized.length === 0) return null;
|
|
477
605
|
return normalized;
|
|
478
606
|
}
|
|
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
607
|
function truncateTitle(input) {
|
|
522
608
|
const chars = Array.from(input);
|
|
523
609
|
if (chars.length <= MAX_SESSION_TITLE_LENGTH) return input;
|
|
@@ -527,13 +613,6 @@ function truncateTitle(input) {
|
|
|
527
613
|
function normalizePrompt(input) {
|
|
528
614
|
return input.replace(/\s+/g, " ").trim();
|
|
529
615
|
}
|
|
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
616
|
|
|
538
617
|
//#endregion
|
|
539
618
|
//#region src/bot/message-filter.ts
|
|
@@ -594,15 +673,12 @@ function stripMentionTags(text) {
|
|
|
594
673
|
function parseTextContent(content) {
|
|
595
674
|
try {
|
|
596
675
|
const parsed = JSON.parse(content);
|
|
597
|
-
if (!
|
|
676
|
+
if (!isPlainObject(parsed)) return null;
|
|
598
677
|
return typeof parsed.text === "string" ? parsed.text : null;
|
|
599
678
|
} catch {
|
|
600
679
|
return null;
|
|
601
680
|
}
|
|
602
681
|
}
|
|
603
|
-
function isRecord$1(value) {
|
|
604
|
-
return typeof value === "object" && value !== null;
|
|
605
|
-
}
|
|
606
682
|
|
|
607
683
|
//#endregion
|
|
608
684
|
//#region src/codex/rpc.ts
|
|
@@ -613,7 +689,7 @@ function parseRpcLine(line) {
|
|
|
613
689
|
} catch {
|
|
614
690
|
return null;
|
|
615
691
|
}
|
|
616
|
-
if (!
|
|
692
|
+
if (!isPlainObject(parsed)) return null;
|
|
617
693
|
if (typeof parsed.method === "string") {
|
|
618
694
|
if (isRpcRequestId(parsed.id)) return {
|
|
619
695
|
id: parsed.id,
|
|
@@ -639,14 +715,11 @@ function parseRpcLine(line) {
|
|
|
639
715
|
function formatRpcError(error) {
|
|
640
716
|
return `Codex RPC error (${error.code}): ${error.message}`;
|
|
641
717
|
}
|
|
642
|
-
function isRecord(value) {
|
|
643
|
-
return typeof value === "object" && value !== null;
|
|
644
|
-
}
|
|
645
718
|
function isRpcRequestId(value) {
|
|
646
719
|
return typeof value === "number" || typeof value === "string";
|
|
647
720
|
}
|
|
648
721
|
function isRpcErrorObject(value) {
|
|
649
|
-
if (!
|
|
722
|
+
if (!isPlainObject(value)) return false;
|
|
650
723
|
return typeof value.code === "number" && typeof value.message === "string";
|
|
651
724
|
}
|
|
652
725
|
function isRpcErrorResponse(value) {
|
|
@@ -884,16 +957,16 @@ function isThreadMissingError(error) {
|
|
|
884
957
|
return error.message.includes("thread not found");
|
|
885
958
|
}
|
|
886
959
|
function isCollaborationModeMask(value) {
|
|
887
|
-
if (!
|
|
960
|
+
if (!isPlainObject(value)) return false;
|
|
888
961
|
const modeIsValid = value.mode === null || value.mode === "default" || value.mode === "plan";
|
|
889
962
|
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
963
|
}
|
|
891
964
|
function isCollaborationModeListResponse(value) {
|
|
892
|
-
if (!
|
|
965
|
+
if (!isPlainObject(value) || !Array.isArray(value.data)) return false;
|
|
893
966
|
return value.data.every(isCollaborationModeMask);
|
|
894
967
|
}
|
|
895
968
|
function isThreadResult(value) {
|
|
896
|
-
if (!
|
|
969
|
+
if (!isPlainObject(value) || !isPlainObject(value.thread)) return false;
|
|
897
970
|
return typeof value.thread.id === "string" && typeof value.model === "string";
|
|
898
971
|
}
|
|
899
972
|
|
|
@@ -909,7 +982,7 @@ function createTurnAccumulator() {
|
|
|
909
982
|
}
|
|
910
983
|
function applyTurnNotification(accumulator, notification) {
|
|
911
984
|
if (notification.method === "error") {
|
|
912
|
-
if (
|
|
985
|
+
if (isPlainObject(notification.params) && typeof notification.params.message === "string") accumulator.turnError = notification.params.message;
|
|
913
986
|
else accumulator.turnError = "Codex returned an unknown error event";
|
|
914
987
|
accumulator.turnCompleted = true;
|
|
915
988
|
return;
|
|
@@ -1050,21 +1123,74 @@ async function listOpenProjects() {
|
|
|
1050
1123
|
return { roots: [process.cwd()] };
|
|
1051
1124
|
}
|
|
1052
1125
|
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/locales/en/messages.po
|
|
1128
|
+
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\"]]}");
|
|
1129
|
+
|
|
1130
|
+
//#endregion
|
|
1131
|
+
//#region src/locales/zh/messages.po
|
|
1132
|
+
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\"]]}");
|
|
1133
|
+
|
|
1134
|
+
//#endregion
|
|
1135
|
+
//#region src/i18n/runtime.ts
|
|
1136
|
+
const DEFAULT_LOCALE = detectDefaultLocale();
|
|
1137
|
+
const CATALOGS = {
|
|
1138
|
+
en: messages$1,
|
|
1139
|
+
zh: messages
|
|
1140
|
+
};
|
|
1141
|
+
let activeLocale = null;
|
|
1142
|
+
initializeI18n(DEFAULT_LOCALE);
|
|
1143
|
+
function initializeI18n(locale) {
|
|
1144
|
+
const resolved = resolveLocale(locale);
|
|
1145
|
+
i18n.loadAndActivate({
|
|
1146
|
+
locale: resolved,
|
|
1147
|
+
messages: CATALOGS[resolved]
|
|
1148
|
+
});
|
|
1149
|
+
activeLocale = resolved;
|
|
1150
|
+
return resolved;
|
|
1151
|
+
}
|
|
1152
|
+
function isSupportedLocale(locale) {
|
|
1153
|
+
return locale === "en" || locale === "zh";
|
|
1154
|
+
}
|
|
1155
|
+
function getDefaultLocale() {
|
|
1156
|
+
return DEFAULT_LOCALE;
|
|
1157
|
+
}
|
|
1158
|
+
function resolveLocale(locale) {
|
|
1159
|
+
if (!locale) return DEFAULT_LOCALE;
|
|
1160
|
+
const mappedLocale = mapToAppLocale(locale);
|
|
1161
|
+
if (mappedLocale) return mappedLocale;
|
|
1162
|
+
return DEFAULT_LOCALE;
|
|
1163
|
+
}
|
|
1164
|
+
function detectDefaultLocale() {
|
|
1165
|
+
const systemLocale = readSystemLocale();
|
|
1166
|
+
if (!systemLocale) return "en";
|
|
1167
|
+
return mapToAppLocale(systemLocale) ?? "en";
|
|
1168
|
+
}
|
|
1169
|
+
function readSystemLocale() {
|
|
1170
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1171
|
+
if (typeof locale !== "string") return;
|
|
1172
|
+
const normalized = locale.trim();
|
|
1173
|
+
if (normalized.length === 0) return;
|
|
1174
|
+
return normalized;
|
|
1175
|
+
}
|
|
1176
|
+
function mapToAppLocale(locale) {
|
|
1177
|
+
const normalized = locale.trim().toLowerCase().replaceAll("_", "-");
|
|
1178
|
+
if (normalized === "zh" || normalized.startsWith("zh-")) return "zh";
|
|
1179
|
+
if (normalized === "en" || normalized.startsWith("en-")) return "en";
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1053
1183
|
//#endregion
|
|
1054
1184
|
//#region src/core/config.ts
|
|
1055
1185
|
const DEFAULT_CODEX_BIN = "codex";
|
|
1056
|
-
const
|
|
1186
|
+
const TEMPLATE_CONFIG = { env: {
|
|
1057
1187
|
BASE_DOMAIN: "https://open.feishu.cn",
|
|
1058
1188
|
APP_ID: "your_app_id",
|
|
1059
1189
|
APP_SECRET: "your_app_secret",
|
|
1060
1190
|
BOT_OPEN_ID: "ou_xxx",
|
|
1061
1191
|
CODEX_BIN: DEFAULT_CODEX_BIN,
|
|
1062
1192
|
CODEX_TIMEOUT_MS: null
|
|
1063
|
-
};
|
|
1064
|
-
const TEMPLATE_CONFIG = {
|
|
1065
|
-
locale: getDefaultLocale(),
|
|
1066
|
-
env: TEMPLATE_ENV_CONFIG
|
|
1067
|
-
};
|
|
1193
|
+
} };
|
|
1068
1194
|
function loadRelayConfig(options = {}) {
|
|
1069
1195
|
const homeDir = options.homeDir ?? os.homedir();
|
|
1070
1196
|
const workspaceCwd = options.workspaceCwd ?? process.cwd();
|
|
@@ -1088,6 +1214,7 @@ function loadRelayConfig(options = {}) {
|
|
|
1088
1214
|
appSecret: readRequiredString(parsed.env.APP_SECRET, "APP_SECRET"),
|
|
1089
1215
|
domain
|
|
1090
1216
|
},
|
|
1217
|
+
homeDir,
|
|
1091
1218
|
botOpenId: readOptionalString(parsed.env.BOT_OPEN_ID, "BOT_OPEN_ID"),
|
|
1092
1219
|
codexBin: readOptionalString(parsed.env.CODEX_BIN, "CODEX_BIN") ?? DEFAULT_CODEX_BIN,
|
|
1093
1220
|
codexTimeoutMs: readTimeoutMs(parsed.env.CODEX_TIMEOUT_MS),
|
|
@@ -1138,7 +1265,7 @@ function parseConfigFile(configPath) {
|
|
|
1138
1265
|
const configObject = parsed;
|
|
1139
1266
|
if (configObject.env === void 0) return {
|
|
1140
1267
|
env: configObject,
|
|
1141
|
-
localeValue: configObject.locale
|
|
1268
|
+
localeValue: configObject.locale
|
|
1142
1269
|
};
|
|
1143
1270
|
if (!isObject(configObject.env)) throw new Error(i18n._({
|
|
1144
1271
|
id: "CfFOzJ",
|
|
@@ -1147,7 +1274,7 @@ function parseConfigFile(configPath) {
|
|
|
1147
1274
|
}));
|
|
1148
1275
|
return {
|
|
1149
1276
|
env: configObject.env,
|
|
1150
|
-
localeValue: configObject.locale
|
|
1277
|
+
localeValue: configObject.locale
|
|
1151
1278
|
};
|
|
1152
1279
|
}
|
|
1153
1280
|
function readRequiredString(value, field) {
|
|
@@ -1194,25 +1321,32 @@ function readTimeoutMs(value) {
|
|
|
1194
1321
|
}));
|
|
1195
1322
|
}
|
|
1196
1323
|
function readLocale(value) {
|
|
1197
|
-
const
|
|
1198
|
-
if (value === void 0 || value === null) return
|
|
1324
|
+
const systemLocale = getDefaultLocale();
|
|
1325
|
+
if (value === void 0 || value === null) return systemLocale;
|
|
1199
1326
|
if (typeof value !== "string") {
|
|
1200
1327
|
console.warn(i18n._({
|
|
1201
|
-
id: "
|
|
1202
|
-
message: "Invalid relay config: locale \"{0}\" is not supported. Falling back to
|
|
1203
|
-
values: {
|
|
1328
|
+
id: "CgiJb7",
|
|
1329
|
+
message: "Invalid relay config: locale \"{0}\" is not supported. Falling back to {systemLocale}.",
|
|
1330
|
+
values: {
|
|
1331
|
+
systemLocale,
|
|
1332
|
+
0: formatInvalidLocale(value)
|
|
1333
|
+
}
|
|
1204
1334
|
}));
|
|
1205
|
-
return
|
|
1335
|
+
return systemLocale;
|
|
1206
1336
|
}
|
|
1207
1337
|
const normalized = value.trim();
|
|
1208
|
-
if (normalized.length === 0) return
|
|
1209
|
-
|
|
1338
|
+
if (normalized.length === 0) return systemLocale;
|
|
1339
|
+
const mapped = mapLocaleToAppLocale(normalized);
|
|
1340
|
+
if (mapped) return mapped;
|
|
1210
1341
|
console.warn(i18n._({
|
|
1211
|
-
id: "
|
|
1212
|
-
message: "Invalid relay config: locale \"{normalized}\" is not supported. Falling back to
|
|
1213
|
-
values: {
|
|
1342
|
+
id: "xBlpjC",
|
|
1343
|
+
message: "Invalid relay config: locale \"{normalized}\" is not supported. Falling back to {systemLocale}.",
|
|
1344
|
+
values: {
|
|
1345
|
+
normalized,
|
|
1346
|
+
systemLocale
|
|
1347
|
+
}
|
|
1214
1348
|
}));
|
|
1215
|
-
return
|
|
1349
|
+
return systemLocale;
|
|
1216
1350
|
}
|
|
1217
1351
|
function formatInvalidLocale(value) {
|
|
1218
1352
|
if (typeof value === "string") return value;
|
|
@@ -1226,6 +1360,13 @@ function formatInvalidLocale(value) {
|
|
|
1226
1360
|
function isObject(value) {
|
|
1227
1361
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1228
1362
|
}
|
|
1363
|
+
function mapLocaleToAppLocale(value) {
|
|
1364
|
+
if (isSupportedLocale(value)) return value;
|
|
1365
|
+
const normalized = value.toLowerCase().replaceAll("_", "-");
|
|
1366
|
+
if (normalized === "zh" || normalized.startsWith("zh-")) return "zh";
|
|
1367
|
+
if (normalized === "en" || normalized.startsWith("en-")) return "en";
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1229
1370
|
function formatError(error) {
|
|
1230
1371
|
if (error instanceof Error) return error.message;
|
|
1231
1372
|
return String(error);
|
|
@@ -1252,9 +1393,9 @@ function formatStartupError(error) {
|
|
|
1252
1393
|
|
|
1253
1394
|
//#endregion
|
|
1254
1395
|
//#region src/feishu/reply.ts
|
|
1255
|
-
const FALLBACK_REPLY_TAG = "
|
|
1256
|
-
async function sendReply(larkClient, data, text) {
|
|
1257
|
-
const content = JSON.stringify({ text: formatReplyTextWithThreadId(data, text) });
|
|
1396
|
+
const FALLBACK_REPLY_TAG = "no-thread";
|
|
1397
|
+
async function sendReply(larkClient, data, text, options) {
|
|
1398
|
+
const content = JSON.stringify({ text: formatReplyTextWithThreadId(data, text, options) });
|
|
1258
1399
|
if (data.message.chat_type === "p2p") {
|
|
1259
1400
|
await larkClient.im.v1.message.create({
|
|
1260
1401
|
params: { receive_id_type: "chat_id" },
|
|
@@ -1274,7 +1415,8 @@ async function sendReply(larkClient, data, text) {
|
|
|
1274
1415
|
}
|
|
1275
1416
|
});
|
|
1276
1417
|
}
|
|
1277
|
-
function formatReplyTextWithThreadId(data, text) {
|
|
1418
|
+
function formatReplyTextWithThreadId(data, text, options) {
|
|
1419
|
+
if (!options?.includeThreadTag) return text.trim();
|
|
1278
1420
|
const replyTag = resolveReplyTag(data);
|
|
1279
1421
|
const normalizedText = text.trim();
|
|
1280
1422
|
if (normalizedText.length === 0) return `${replyTag}\n`;
|
|
@@ -1289,13 +1431,27 @@ function resolveReplyTag(data) {
|
|
|
1289
1431
|
userId: senderId
|
|
1290
1432
|
}));
|
|
1291
1433
|
if (!session || session.threadId.trim().length === 0) return FALLBACK_REPLY_TAG;
|
|
1292
|
-
return
|
|
1434
|
+
return session.threadId;
|
|
1293
1435
|
}
|
|
1294
1436
|
|
|
1295
1437
|
//#endregion
|
|
1296
1438
|
//#region src/index.ts
|
|
1297
1439
|
const relayConfig = loadConfigOrExit();
|
|
1298
1440
|
initializeI18n(relayConfig.locale);
|
|
1441
|
+
try {
|
|
1442
|
+
initializeSessionStore({
|
|
1443
|
+
homeDir: relayConfig.homeDir,
|
|
1444
|
+
workspaceCwd: relayConfig.workspaceCwd
|
|
1445
|
+
});
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1448
|
+
console.error(i18n._({
|
|
1449
|
+
id: "aQiwam",
|
|
1450
|
+
message: "Failed to start relay: {message}",
|
|
1451
|
+
values: { message }
|
|
1452
|
+
}));
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1299
1455
|
const BUSY_MESSAGE = i18n._({
|
|
1300
1456
|
id: "H7VlDR",
|
|
1301
1457
|
message: "Currently busy. Please try again later."
|
|
@@ -1328,7 +1484,7 @@ async function processIncomingEvent(data) {
|
|
|
1328
1484
|
})
|
|
1329
1485
|
});
|
|
1330
1486
|
if (reply === null) return;
|
|
1331
|
-
await sendReply(client, data, reply);
|
|
1487
|
+
await sendReply(client, data, reply, { includeThreadTag: shouldAttachThreadTag(data) });
|
|
1332
1488
|
} catch (error) {
|
|
1333
1489
|
console.error("failed to handle Feishu message", error);
|
|
1334
1490
|
try {
|
|
@@ -1341,6 +1497,22 @@ async function processIncomingEvent(data) {
|
|
|
1341
1497
|
}
|
|
1342
1498
|
}
|
|
1343
1499
|
}
|
|
1500
|
+
function shouldAttachThreadTag(data) {
|
|
1501
|
+
const rawText = parseEventText(data.message.content);
|
|
1502
|
+
if (rawText === null) return false;
|
|
1503
|
+
const normalizedText = stripMentionTags(rawText).trim();
|
|
1504
|
+
if (normalizedText.length === 0) return false;
|
|
1505
|
+
return parseCommand(normalizedText).type === "prompt";
|
|
1506
|
+
}
|
|
1507
|
+
function parseEventText(content) {
|
|
1508
|
+
try {
|
|
1509
|
+
const parsed = JSON.parse(content);
|
|
1510
|
+
if (!isPlainObject(parsed)) return null;
|
|
1511
|
+
return typeof parsed.text === "string" ? parsed.text : null;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1344
1516
|
const eventDispatcher = new Lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
|
|
1345
1517
|
console.info("feishu message received\n", JSON.stringify(data, null, 2), "\n");
|
|
1346
1518
|
if (!shouldProcessMessage(data, relayConfig.botOpenId)) return;
|