codex-to-im 1.0.10 → 1.0.11
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 +2 -25
- package/README_CN.md +2 -25
- package/config.env.example +5 -4
- package/dist/cli.mjs +0 -1
- package/dist/daemon.mjs +216 -11
- package/dist/ui-server.mjs +675 -157
- package/docs/codex-to-im-prd.md +3 -21
- package/docs/install-windows.md +5 -29
- package/package.json +1 -2
- package/references/setup-guides.md +1 -8
- package/references/usage.md +4 -33
- package/scripts/install-codex.sh +0 -65
package/README.md
CHANGED
|
@@ -10,8 +10,6 @@ The product is no longer centered around a Codex skill. The main path is:
|
|
|
10
10
|
4. Start the bridge in the background
|
|
11
11
|
5. Bind real desktop Codex threads to Feishu or Weixin chats
|
|
12
12
|
|
|
13
|
-
`SKILL.md` is still kept in the repo, but only as an optional Codex integration entry.
|
|
14
|
-
|
|
15
13
|
## Project Origin
|
|
16
14
|
|
|
17
15
|
The current codebase is a consolidated continuation of two earlier repositories:
|
|
@@ -19,7 +17,7 @@ The current codebase is a consolidated continuation of two earlier repositories:
|
|
|
19
17
|
- `Claude-to-IM`
|
|
20
18
|
- `Claude-to-IM-skill`
|
|
21
19
|
|
|
22
|
-
`codex-to-im` is based on those two projects and has been reworked toward a single-package local app
|
|
20
|
+
`codex-to-im` is based on those two projects and has been reworked toward a single-package local app and shared-thread workflow.
|
|
23
21
|
|
|
24
22
|
Windows host installation guide: [docs/install-windows.md](D:/codex/Claude-to-IM-skill/docs/install-windows.md)
|
|
25
23
|
|
|
@@ -31,7 +29,6 @@ Windows host installation guide: [docs/install-windows.md](D:/codex/Claude-to-IM
|
|
|
31
29
|
- Weixin QR login flow
|
|
32
30
|
- Desktop session discovery from `~/.codex/sessions`
|
|
33
31
|
- Web-side binding updates for IM chats
|
|
34
|
-
- Optional Codex integration for opening `codex-to-im` or entering the Feishu handoff flow
|
|
35
32
|
|
|
36
33
|
## Install
|
|
37
34
|
|
|
@@ -190,8 +187,7 @@ This is closer to a full-power `code` workflow. It fits a controlled local proje
|
|
|
190
187
|
The channel pages also expose a “Use Markdown for bridge feedback” switch:
|
|
191
188
|
- enabled by default for Feishu
|
|
192
189
|
- disabled by default for WeChat
|
|
193
|
-
-
|
|
194
|
-
- does not affect raw Codex replies
|
|
190
|
+
- affects text sent through the bridge, including normal replies, shared-thread mirror messages, and system feedback such as `/h`, `/status`, and `/threads`
|
|
195
191
|
|
|
196
192
|
## Update
|
|
197
193
|
|
|
@@ -205,23 +201,6 @@ npm update -g codex-to-im
|
|
|
205
201
|
codex-to-im
|
|
206
202
|
```
|
|
207
203
|
|
|
208
|
-
## Optional Codex Integration
|
|
209
|
-
|
|
210
|
-
The repo still includes a lightweight optional integration under `SKILL.md`.
|
|
211
|
-
|
|
212
|
-
It is not required for the product to work.
|
|
213
|
-
|
|
214
|
-
If installed into `~/.codex/skills/codex-to-im`, it should only be used for two actions:
|
|
215
|
-
|
|
216
|
-
- Open `codex-to-im`
|
|
217
|
-
- Open the Feishu session-sharing entry for the current workflow
|
|
218
|
-
|
|
219
|
-
You can install that optional integration from the web UI, or with:
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
bash scripts/install-codex.sh --link
|
|
223
|
-
```
|
|
224
|
-
|
|
225
204
|
## Repo Layout
|
|
226
205
|
|
|
227
206
|
- `src/ui-server.ts` — local workbench UI and HTTP API
|
|
@@ -229,7 +208,6 @@ bash scripts/install-codex.sh --link
|
|
|
229
208
|
- `src/desktop-sessions.ts` — desktop thread discovery from Codex session files
|
|
230
209
|
- `src/session-bindings.ts` — binding summaries and web-side binding updates
|
|
231
210
|
- `src/lib/bridge/` — bridge runtime and IM channel routing
|
|
232
|
-
- `SKILL.md` — optional Codex integration only
|
|
233
211
|
- `docs/` — PRD and shared-thread design docs
|
|
234
212
|
|
|
235
213
|
## Development
|
|
@@ -246,6 +224,5 @@ Current product direction:
|
|
|
246
224
|
- Standalone local app first
|
|
247
225
|
- Web workbench first
|
|
248
226
|
- Shared Codex thread model first
|
|
249
|
-
- Codex integration is optional, not the primary installation path
|
|
250
227
|
|
|
251
228
|
[中文文档](README_CN.md)
|
package/README_CN.md
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
4. 在后台启动 bridge
|
|
11
11
|
5. 把真实的桌面 Codex thread 绑定到飞书或微信聊天
|
|
12
12
|
|
|
13
|
-
仓库里仍然保留了 `SKILL.md`,但它只是一个可选的 Codex 集成入口,不再是产品本体。
|
|
14
|
-
|
|
15
13
|
## 项目来源
|
|
16
14
|
|
|
17
15
|
当前这套代码是在两个早期仓库的基础上整理和改造出来的:
|
|
@@ -19,7 +17,7 @@
|
|
|
19
17
|
- `Claude-to-IM`
|
|
20
18
|
- `Claude-to-IM-skill`
|
|
21
19
|
|
|
22
|
-
现在的 `codex-to-im`
|
|
20
|
+
现在的 `codex-to-im` 是在这两个工程基础上继续演进的单包版本,重点调整成了本地应用和共享 thread 的形态。
|
|
23
21
|
|
|
24
22
|
Windows 主机安装说明见:[docs/install-windows.md](D:/codex/Claude-to-IM-skill/docs/install-windows.md)
|
|
25
23
|
|
|
@@ -31,7 +29,6 @@ Windows 主机安装说明见:[docs/install-windows.md](D:/codex/Claude-to-IM-
|
|
|
31
29
|
- 微信扫码登录
|
|
32
30
|
- 从 `~/.codex/sessions` 发现桌面会话
|
|
33
31
|
- 在网页中查看和切换 IM 绑定
|
|
34
|
-
- 可选的 Codex 集成,仅用于打开 `codex-to-im` 或进入飞书共享入口
|
|
35
32
|
|
|
36
33
|
## 安装
|
|
37
34
|
|
|
@@ -189,8 +186,7 @@ codex-to-im stop
|
|
|
189
186
|
通道页还支持“反馈使用 Markdown”开关:
|
|
190
187
|
- 飞书默认开启
|
|
191
188
|
- 微信默认关闭
|
|
192
|
-
-
|
|
193
|
-
- 不影响 Codex 原始回复内容
|
|
189
|
+
- 影响通过 bridge 发送到通道的文本反馈,包括普通回复、共享桌面线程镜像以及 `/h`、`/status`、`/threads` 这类系统反馈
|
|
194
190
|
|
|
195
191
|
## 更新
|
|
196
192
|
|
|
@@ -204,23 +200,6 @@ npm update -g codex-to-im
|
|
|
204
200
|
codex-to-im
|
|
205
201
|
```
|
|
206
202
|
|
|
207
|
-
## 可选 Codex 集成
|
|
208
|
-
|
|
209
|
-
仓库里仍然保留了一个很薄的可选集成,定义在 `SKILL.md`。
|
|
210
|
-
|
|
211
|
-
它不是必需的。
|
|
212
|
-
|
|
213
|
-
如果你把它装到 `~/.codex/skills/codex-to-im`,它只保留两个动作:
|
|
214
|
-
|
|
215
|
-
- 打开 `codex-to-im`
|
|
216
|
-
- 打开“共享当前会话到飞书”的入口
|
|
217
|
-
|
|
218
|
-
你可以在 Web UI 中安装这层可选集成,也可以手动执行:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
bash scripts/install-codex.sh --link
|
|
222
|
-
```
|
|
223
|
-
|
|
224
203
|
## 仓库结构
|
|
225
204
|
|
|
226
205
|
- `src/ui-server.ts` — 本地工作台 UI 和 HTTP API
|
|
@@ -228,7 +207,6 @@ bash scripts/install-codex.sh --link
|
|
|
228
207
|
- `src/desktop-sessions.ts` — 从 Codex 会话文件发现桌面 thread
|
|
229
208
|
- `src/session-bindings.ts` — 绑定摘要与网页侧切换
|
|
230
209
|
- `src/lib/bridge/` — bridge 运行时与 IM 路由
|
|
231
|
-
- `SKILL.md` — 可选 Codex 集成,不是主产品
|
|
232
210
|
- `docs/` — PRD 与共享 thread 技术设计
|
|
233
211
|
|
|
234
212
|
## 开发
|
|
@@ -243,6 +221,5 @@ npm run build
|
|
|
243
221
|
- 先做独立本地应用
|
|
244
222
|
- 先做 Web 工作台
|
|
245
223
|
- 先做共享 Codex thread
|
|
246
|
-
- Codex 集成是可选增强,不是主安装路径
|
|
247
224
|
|
|
248
225
|
[English](README.md)
|
package/config.env.example
CHANGED
|
@@ -85,8 +85,8 @@ CTI_TG_CHAT_ID=your-chat-id
|
|
|
85
85
|
# Current Codex runtime note: thinking/progress can update live, but
|
|
86
86
|
# assistant body text may still arrive only at completion.
|
|
87
87
|
# CTI_FEISHU_STREAMING_ENABLED=true
|
|
88
|
-
# Use Markdown for
|
|
89
|
-
#
|
|
88
|
+
# Use Markdown for text sent through the bridge, including normal replies,
|
|
89
|
+
# shared-thread mirror messages, and system feedback such as /h or /status.
|
|
90
90
|
# CTI_FEISHU_COMMAND_MARKDOWN_ENABLED=true
|
|
91
91
|
|
|
92
92
|
# ── QQ ──
|
|
@@ -111,8 +111,9 @@ CTI_TG_CHAT_ID=your-chat-id
|
|
|
111
111
|
# only accepts WeChat-provided speech-to-text text and otherwise returns an error.
|
|
112
112
|
# (default false for safety in CLI setups)
|
|
113
113
|
# CTI_WEIXIN_MEDIA_ENABLED=false
|
|
114
|
-
# Use Markdown for
|
|
115
|
-
#
|
|
114
|
+
# Use Markdown for text sent through the bridge, including normal replies,
|
|
115
|
+
# shared-thread mirror messages, and system feedback such as /h or /status.
|
|
116
|
+
# Default is false for WeChat.
|
|
116
117
|
# CTI_WEIXIN_COMMAND_MARKDOWN_ENABLED=false
|
|
117
118
|
|
|
118
119
|
# ── Permission ──
|
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { createRequire } from 'module'; const require = createRequire(import.met
|
|
|
3
3
|
|
|
4
4
|
// src/service-manager.ts
|
|
5
5
|
import fs2 from "node:fs";
|
|
6
|
-
import os2 from "node:os";
|
|
7
6
|
import path2 from "node:path";
|
|
8
7
|
import { spawn } from "node:child_process";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
package/dist/daemon.mjs
CHANGED
|
@@ -14228,6 +14228,7 @@ import fs3 from "node:fs";
|
|
|
14228
14228
|
import os2 from "node:os";
|
|
14229
14229
|
import path3 from "node:path";
|
|
14230
14230
|
import crypto7 from "node:crypto";
|
|
14231
|
+
import { DatabaseSync } from "node:sqlite";
|
|
14231
14232
|
var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
|
|
14232
14233
|
var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
|
|
14233
14234
|
var MAX_SESSION_TITLE_SCAN_BYTES = 512 * 1024;
|
|
@@ -14244,10 +14245,60 @@ function getArchivedSessionsRoot() {
|
|
|
14244
14245
|
function getSessionIndexPath() {
|
|
14245
14246
|
return path3.join(getCodexHome(), "session_index.jsonl");
|
|
14246
14247
|
}
|
|
14248
|
+
function getCodexGlobalStatePath() {
|
|
14249
|
+
return path3.join(getCodexHome(), ".codex-global-state.json");
|
|
14250
|
+
}
|
|
14251
|
+
function getDesktopStateDbPath() {
|
|
14252
|
+
const codexHome = getCodexHome();
|
|
14253
|
+
let entries;
|
|
14254
|
+
try {
|
|
14255
|
+
entries = fs3.readdirSync(codexHome, { withFileTypes: true });
|
|
14256
|
+
} catch {
|
|
14257
|
+
return null;
|
|
14258
|
+
}
|
|
14259
|
+
const candidates = entries.filter((entry) => entry.isFile() && /^state_\d+\.sqlite$/i.test(entry.name)).map((entry) => path3.join(codexHome, entry.name)).sort((left, right) => {
|
|
14260
|
+
try {
|
|
14261
|
+
return fs3.statSync(right).mtimeMs - fs3.statSync(left).mtimeMs;
|
|
14262
|
+
} catch {
|
|
14263
|
+
return 0;
|
|
14264
|
+
}
|
|
14265
|
+
});
|
|
14266
|
+
return candidates[0] || null;
|
|
14267
|
+
}
|
|
14247
14268
|
function extractThreadIdFromRolloutName(name) {
|
|
14248
14269
|
const match2 = name.match(/-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i);
|
|
14249
14270
|
return match2?.[1] || null;
|
|
14250
14271
|
}
|
|
14272
|
+
function normalizeComparablePath(value) {
|
|
14273
|
+
if (!value) return "";
|
|
14274
|
+
const stripped = value.replace(/^\\\\\?\\/, "");
|
|
14275
|
+
return path3.resolve(stripped).replace(/[\\/]+$/, "").toLowerCase();
|
|
14276
|
+
}
|
|
14277
|
+
function isInternalSkillWorkspace(cwd) {
|
|
14278
|
+
const normalizedCwd = normalizeComparablePath(cwd);
|
|
14279
|
+
if (!normalizedCwd) return false;
|
|
14280
|
+
const skillsRoot = normalizeComparablePath(path3.join(getCodexHome(), "skills"));
|
|
14281
|
+
if (!skillsRoot) return false;
|
|
14282
|
+
return normalizedCwd === skillsRoot || normalizedCwd.startsWith(`${skillsRoot}\\`) || normalizedCwd.startsWith(`${skillsRoot}/`);
|
|
14283
|
+
}
|
|
14284
|
+
function loadSavedWorkspaceRoots() {
|
|
14285
|
+
const statePath = getCodexGlobalStatePath();
|
|
14286
|
+
if (!fs3.existsSync(statePath)) return null;
|
|
14287
|
+
let parsed;
|
|
14288
|
+
try {
|
|
14289
|
+
parsed = JSON.parse(fs3.readFileSync(statePath, "utf-8"));
|
|
14290
|
+
} catch {
|
|
14291
|
+
return null;
|
|
14292
|
+
}
|
|
14293
|
+
const roots = Array.isArray(parsed["electron-saved-workspace-roots"]) ? parsed["electron-saved-workspace-roots"].map((value) => typeof value === "string" ? normalizeComparablePath(value) : "").filter(Boolean) : [];
|
|
14294
|
+
return roots.length > 0 ? roots : null;
|
|
14295
|
+
}
|
|
14296
|
+
function isWithinSavedWorkspaceRoots(cwd, roots) {
|
|
14297
|
+
if (!roots || roots.length === 0) return true;
|
|
14298
|
+
const normalizedCwd = normalizeComparablePath(cwd);
|
|
14299
|
+
if (!normalizedCwd) return false;
|
|
14300
|
+
return roots.some((root) => normalizedCwd === root || normalizedCwd.startsWith(`${root}\\`) || normalizedCwd.startsWith(`${root}/`));
|
|
14301
|
+
}
|
|
14251
14302
|
function loadArchivedThreadIds() {
|
|
14252
14303
|
const archivedRoot = getArchivedSessionsRoot();
|
|
14253
14304
|
if (!fs3.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
|
|
@@ -14330,7 +14381,7 @@ function isDesktopLike(meta) {
|
|
|
14330
14381
|
if (source === "exec") return false;
|
|
14331
14382
|
return originator.includes("desktop") || source === "vscode" || source === "desktop";
|
|
14332
14383
|
}
|
|
14333
|
-
function
|
|
14384
|
+
function loadThreadIndexEntries(archivedThreadIds) {
|
|
14334
14385
|
const indexPath = getSessionIndexPath();
|
|
14335
14386
|
if (!fs3.existsSync(indexPath)) return /* @__PURE__ */ new Map();
|
|
14336
14387
|
let content = "";
|
|
@@ -14357,7 +14408,52 @@ function loadThreadNameIndex(archivedThreadIds) {
|
|
|
14357
14408
|
titles.set(threadId, { title, updatedAt });
|
|
14358
14409
|
}
|
|
14359
14410
|
}
|
|
14360
|
-
return
|
|
14411
|
+
return titles;
|
|
14412
|
+
}
|
|
14413
|
+
function parseUpdatedAtValue(value) {
|
|
14414
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
14415
|
+
return value > 1e12 ? value : value * 1e3;
|
|
14416
|
+
}
|
|
14417
|
+
if (typeof value === "string") {
|
|
14418
|
+
const numeric = Number(value.trim());
|
|
14419
|
+
if (Number.isFinite(numeric)) {
|
|
14420
|
+
return numeric > 1e12 ? numeric : numeric * 1e3;
|
|
14421
|
+
}
|
|
14422
|
+
const parsed = Date.parse(value);
|
|
14423
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
14424
|
+
}
|
|
14425
|
+
return 0;
|
|
14426
|
+
}
|
|
14427
|
+
function loadVisibleDesktopThreads(limit) {
|
|
14428
|
+
const dbPath = getDesktopStateDbPath();
|
|
14429
|
+
if (!dbPath || !fs3.existsSync(dbPath)) return null;
|
|
14430
|
+
let db = null;
|
|
14431
|
+
try {
|
|
14432
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
14433
|
+
const hasLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0;
|
|
14434
|
+
const sql = `
|
|
14435
|
+
SELECT id, updated_at
|
|
14436
|
+
FROM threads
|
|
14437
|
+
WHERE archived = 0
|
|
14438
|
+
AND source != 'exec'
|
|
14439
|
+
ORDER BY updated_at DESC
|
|
14440
|
+
${hasLimit ? "LIMIT ?" : ""}
|
|
14441
|
+
`;
|
|
14442
|
+
const rows = hasLimit ? db.prepare(sql).all(Math.max(1, Math.floor(limit))) : db.prepare(sql).all();
|
|
14443
|
+
const ids = rows.map((row) => {
|
|
14444
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
14445
|
+
if (!id) return null;
|
|
14446
|
+
return {
|
|
14447
|
+
id,
|
|
14448
|
+
updatedAtMs: parseUpdatedAtValue(row.updated_at)
|
|
14449
|
+
};
|
|
14450
|
+
}).filter((row) => Boolean(row));
|
|
14451
|
+
return ids.length > 0 ? ids : null;
|
|
14452
|
+
} catch {
|
|
14453
|
+
return null;
|
|
14454
|
+
} finally {
|
|
14455
|
+
db?.close();
|
|
14456
|
+
}
|
|
14361
14457
|
}
|
|
14362
14458
|
function buildFallbackTitle(threadId, filePath, cwd) {
|
|
14363
14459
|
try {
|
|
@@ -14380,7 +14476,7 @@ function buildFallbackTitle(threadId, filePath, cwd) {
|
|
|
14380
14476
|
if (dirName) return dirName;
|
|
14381
14477
|
return `Session ${threadId.slice(0, 8)}`;
|
|
14382
14478
|
}
|
|
14383
|
-
function parseDesktopSession(filePath,
|
|
14479
|
+
function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
|
|
14384
14480
|
const firstLine = readFirstLine(filePath);
|
|
14385
14481
|
if (!firstLine) return null;
|
|
14386
14482
|
let parsed;
|
|
@@ -14402,10 +14498,13 @@ function parseDesktopSession(filePath, threadNames, archivedThreadIds) {
|
|
|
14402
14498
|
return null;
|
|
14403
14499
|
}
|
|
14404
14500
|
const cwd = parsed.payload.cwd || "";
|
|
14501
|
+
if (isInternalSkillWorkspace(cwd)) {
|
|
14502
|
+
return null;
|
|
14503
|
+
}
|
|
14405
14504
|
const lastEventAt = stat.mtime.toISOString();
|
|
14406
14505
|
const firstSeenAt = parsed.payload.timestamp || parsed.timestamp || stat.birthtime.toISOString();
|
|
14407
14506
|
const threadId = parsed.payload.id;
|
|
14408
|
-
const title =
|
|
14507
|
+
const title = threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
|
|
14409
14508
|
return {
|
|
14410
14509
|
threadId,
|
|
14411
14510
|
filePath,
|
|
@@ -14459,19 +14558,45 @@ function isSessionEventLine(line) {
|
|
|
14459
14558
|
function isSessionMessageLine(line) {
|
|
14460
14559
|
return line.type === "response_item";
|
|
14461
14560
|
}
|
|
14462
|
-
function listDesktopSessions(limit
|
|
14561
|
+
function listDesktopSessions(limit) {
|
|
14463
14562
|
const root = getCodexSessionsRoot();
|
|
14464
14563
|
if (!fs3.existsSync(root)) return [];
|
|
14465
14564
|
const archivedThreadIds = loadArchivedThreadIds();
|
|
14466
|
-
const
|
|
14565
|
+
const threadIndexEntries = loadThreadIndexEntries(archivedThreadIds);
|
|
14566
|
+
const savedWorkspaceRoots = loadSavedWorkspaceRoots();
|
|
14567
|
+
const visibleThreads = loadVisibleDesktopThreads(limit);
|
|
14568
|
+
const visibleThreadIds = visibleThreads?.map((thread) => thread.id) || null;
|
|
14569
|
+
const visibleThreadSet = visibleThreadIds ? new Set(visibleThreadIds) : null;
|
|
14570
|
+
const visibleThreadUpdatedAt = new Map(visibleThreads?.map((thread) => [thread.id, thread.updatedAtMs]) || []);
|
|
14571
|
+
const oldestVisibleUpdatedAtMs = visibleThreads && visibleThreads.length > 0 ? Math.min(...visibleThreads.map((thread) => thread.updatedAtMs || Number.MAX_SAFE_INTEGER)) : 0;
|
|
14467
14572
|
const files = [];
|
|
14468
14573
|
walkSessionFiles(root, files);
|
|
14469
|
-
const
|
|
14574
|
+
const allSessions = /* @__PURE__ */ new Map();
|
|
14470
14575
|
for (const filePath of files) {
|
|
14471
|
-
const session = parseDesktopSession(filePath,
|
|
14472
|
-
if (session)
|
|
14576
|
+
const session = parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds);
|
|
14577
|
+
if (!session) continue;
|
|
14578
|
+
if (!isWithinSavedWorkspaceRoots(session.cwd, savedWorkspaceRoots)) continue;
|
|
14579
|
+
allSessions.set(session.threadId, session);
|
|
14580
|
+
}
|
|
14581
|
+
const sessions = Array.from(allSessions.values());
|
|
14582
|
+
if (visibleThreadSet && visibleThreadIds) {
|
|
14583
|
+
const mergedThreadIds = new Set(visibleThreadIds);
|
|
14584
|
+
if (oldestVisibleUpdatedAtMs > 0) {
|
|
14585
|
+
for (const session of sessions) {
|
|
14586
|
+
if (visibleThreadSet.has(session.threadId)) continue;
|
|
14587
|
+
const candidateUpdatedAtMs = parseUpdatedAtValue(threadIndexEntries.get(session.threadId)?.updatedAt || session.lastEventAt);
|
|
14588
|
+
if (candidateUpdatedAtMs > oldestVisibleUpdatedAtMs) {
|
|
14589
|
+
mergedThreadIds.add(session.threadId);
|
|
14590
|
+
}
|
|
14591
|
+
}
|
|
14592
|
+
}
|
|
14593
|
+
return sessions.filter((session) => mergedThreadIds.has(session.threadId)).sort((left, right) => {
|
|
14594
|
+
const rightUpdatedAtMs = visibleThreadUpdatedAt.get(right.threadId) || parseUpdatedAtValue(threadIndexEntries.get(right.threadId)?.updatedAt || right.lastEventAt);
|
|
14595
|
+
const leftUpdatedAtMs = visibleThreadUpdatedAt.get(left.threadId) || parseUpdatedAtValue(threadIndexEntries.get(left.threadId)?.updatedAt || left.lastEventAt);
|
|
14596
|
+
return rightUpdatedAtMs - leftUpdatedAtMs;
|
|
14597
|
+
}).slice(0, typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.max(1, Math.floor(limit)) : void 0);
|
|
14473
14598
|
}
|
|
14474
|
-
return sessions.sort((a, b) => b.lastEventAt.localeCompare(a.lastEventAt)).slice(0, Math.max(1, limit));
|
|
14599
|
+
return sessions.sort((a, b) => b.lastEventAt.localeCompare(a.lastEventAt)).slice(0, typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.max(1, Math.floor(limit)) : void 0);
|
|
14475
14600
|
}
|
|
14476
14601
|
function getDesktopSessionByThreadId(threadId) {
|
|
14477
14602
|
const sessions = listDesktopSessions(200);
|
|
@@ -14717,12 +14842,39 @@ function readDesktopSessionEventStream(threadId) {
|
|
|
14717
14842
|
}
|
|
14718
14843
|
|
|
14719
14844
|
// src/session-bindings.ts
|
|
14845
|
+
function formatChannelLabel(channelType) {
|
|
14846
|
+
return channelType === "weixin" ? "\u5FAE\u4FE1" : channelType === "feishu" ? "\u98DE\u4E66" : channelType;
|
|
14847
|
+
}
|
|
14848
|
+
function findConflictingBinding(store, current, match2) {
|
|
14849
|
+
return store.listChannelBindings().find((binding) => {
|
|
14850
|
+
if (binding.channelType === current.channelType && binding.chatId === current.chatId) {
|
|
14851
|
+
return false;
|
|
14852
|
+
}
|
|
14853
|
+
return match2(binding);
|
|
14854
|
+
}) || null;
|
|
14855
|
+
}
|
|
14856
|
+
function assertBindingTargetAvailable(store, current, opts) {
|
|
14857
|
+
const conflict = findConflictingBinding(
|
|
14858
|
+
store,
|
|
14859
|
+
current,
|
|
14860
|
+
(binding) => (opts.sessionId ? binding.codepilotSessionId === opts.sessionId : false) || (opts.sdkSessionId ? binding.sdkSessionId === opts.sdkSessionId : false)
|
|
14861
|
+
);
|
|
14862
|
+
if (!conflict) return;
|
|
14863
|
+
throw new Error(
|
|
14864
|
+
`\u8BE5\u4F1A\u8BDD\u5DF2\u7ED1\u5B9A\u5230 ${formatChannelLabel(conflict.channelType)} \u804A\u5929 ${conflict.chatId}\u3002\u4E00\u4E2A\u4F1A\u8BDD\u53EA\u80FD\u7ED1\u5B9A\u4E00\u4E2A\u804A\u5929\u3002`
|
|
14865
|
+
);
|
|
14866
|
+
}
|
|
14720
14867
|
function getSessionMode(store, session) {
|
|
14721
14868
|
return session.preferred_mode || store.getSetting("bridge_default_mode") || "code";
|
|
14722
14869
|
}
|
|
14723
14870
|
function bindStoreToSession(store, channelType, chatId, sessionId) {
|
|
14724
14871
|
const session = store.getSession(sessionId);
|
|
14725
14872
|
if (!session) return null;
|
|
14873
|
+
assertBindingTargetAvailable(
|
|
14874
|
+
store,
|
|
14875
|
+
{ channelType, chatId },
|
|
14876
|
+
{ sessionId: session.id, sdkSessionId: session.sdk_session_id || void 0 }
|
|
14877
|
+
);
|
|
14726
14878
|
return store.upsertChannelBinding({
|
|
14727
14879
|
channelType,
|
|
14728
14880
|
chatId,
|
|
@@ -14734,6 +14886,11 @@ function bindStoreToSession(store, channelType, chatId, sessionId) {
|
|
|
14734
14886
|
});
|
|
14735
14887
|
}
|
|
14736
14888
|
function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
|
|
14889
|
+
assertBindingTargetAvailable(
|
|
14890
|
+
store,
|
|
14891
|
+
{ channelType, chatId },
|
|
14892
|
+
{ sdkSessionId }
|
|
14893
|
+
);
|
|
14737
14894
|
const existing = store.findSessionBySdkSessionId(sdkSessionId);
|
|
14738
14895
|
if (existing) {
|
|
14739
14896
|
return store.upsertChannelBinding({
|
|
@@ -14856,7 +15013,20 @@ function resolve(address) {
|
|
|
14856
15013
|
const existing = store.getChannelBinding(address.channelType, address.chatId);
|
|
14857
15014
|
if (existing) {
|
|
14858
15015
|
const session = store.getSession(existing.codepilotSessionId);
|
|
14859
|
-
if (session)
|
|
15016
|
+
if (session) {
|
|
15017
|
+
const updates = {};
|
|
15018
|
+
if (address.userId && address.userId !== existing.chatUserId) {
|
|
15019
|
+
updates.chatUserId = address.userId;
|
|
15020
|
+
}
|
|
15021
|
+
if (address.displayName && address.displayName !== existing.chatDisplayName) {
|
|
15022
|
+
updates.chatDisplayName = address.displayName;
|
|
15023
|
+
}
|
|
15024
|
+
if (Object.keys(updates).length > 0) {
|
|
15025
|
+
store.updateChannelBinding(existing.id, updates);
|
|
15026
|
+
return store.getChannelBinding(address.channelType, address.chatId) || { ...existing, ...updates };
|
|
15027
|
+
}
|
|
15028
|
+
return existing;
|
|
15029
|
+
}
|
|
14860
15030
|
return createBinding(address);
|
|
14861
15031
|
}
|
|
14862
15032
|
return createBinding(address);
|
|
@@ -14878,6 +15048,8 @@ function createBinding(address, workingDirectory) {
|
|
|
14878
15048
|
return store.upsertChannelBinding({
|
|
14879
15049
|
channelType: address.channelType,
|
|
14880
15050
|
chatId: address.chatId,
|
|
15051
|
+
chatUserId: address.userId,
|
|
15052
|
+
chatDisplayName: address.displayName,
|
|
14881
15053
|
codepilotSessionId: session.id,
|
|
14882
15054
|
sdkSessionId: "",
|
|
14883
15055
|
workingDirectory: session.working_directory,
|
|
@@ -18613,6 +18785,25 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
|
|
|
18613
18785
|
}
|
|
18614
18786
|
break;
|
|
18615
18787
|
}
|
|
18788
|
+
case "/unbind": {
|
|
18789
|
+
if (!currentBinding) {
|
|
18790
|
+
response = "\u5F53\u524D\u804A\u5929\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u4EFB\u4F55\u4F1A\u8BDD\u3002";
|
|
18791
|
+
break;
|
|
18792
|
+
}
|
|
18793
|
+
store.deleteChannelBinding(currentBinding.id);
|
|
18794
|
+
response = buildCommandFields(
|
|
18795
|
+
"\u5DF2\u89E3\u7ED1\u5F53\u524D\u804A\u5929",
|
|
18796
|
+
[
|
|
18797
|
+
["\u804A\u5929", msg.address.chatId]
|
|
18798
|
+
],
|
|
18799
|
+
[
|
|
18800
|
+
"\u8FD9\u4E2A\u804A\u5929\u5DF2\u91CA\u653E\u5F53\u524D\u4F1A\u8BDD\u7ED1\u5B9A\u3002",
|
|
18801
|
+
"\u4E4B\u540E\u5982\u679C\u76F4\u63A5\u53D1\u9001\u6587\u672C\uFF0C\u4F1A\u81EA\u52A8\u8FDB\u5165\u65B0\u7684\u4E34\u65F6\u8349\u7A3F\u7EBF\u7A0B\u3002"
|
|
18802
|
+
],
|
|
18803
|
+
responseParseMode === "Markdown"
|
|
18804
|
+
);
|
|
18805
|
+
break;
|
|
18806
|
+
}
|
|
18616
18807
|
case "/help":
|
|
18617
18808
|
responseParseMode = getFeedbackParseMode(adapter.channelType);
|
|
18618
18809
|
response = [
|
|
@@ -18634,6 +18825,7 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
|
|
|
18634
18825
|
"- `/model` \u67E5\u770B\u5F53\u524D\u6A21\u578B\uFF1B`/model gpt-5.4` \u53EF\u5207\u6362\uFF0C`/model default` \u56DE\u9000\u5230\u9ED8\u8BA4\u6A21\u578B",
|
|
18635
18826
|
"- `/t 0` \u4E34\u65F6\u8349\u7A3F\u7EBF\u7A0B",
|
|
18636
18827
|
"- `/t 0 reset` \u91CD\u7F6E\u8349\u7A3F\u7EBF\u7A0B",
|
|
18828
|
+
"- `/unbind` \u89E3\u7ED1\u5F53\u524D\u804A\u5929\uFF0C\u91CA\u653E\u5F53\u524D\u4F1A\u8BDD",
|
|
18637
18829
|
"- `/stop` \u505C\u6B62\u5F53\u524D\u4EFB\u52A1",
|
|
18638
18830
|
"",
|
|
18639
18831
|
"**\u5176\u5B83**",
|
|
@@ -18832,6 +19024,8 @@ var JsonFileStore = class {
|
|
|
18832
19024
|
...existing,
|
|
18833
19025
|
codepilotSessionId: data.codepilotSessionId,
|
|
18834
19026
|
sdkSessionId: data.sdkSessionId ?? existing.sdkSessionId,
|
|
19027
|
+
chatUserId: data.chatUserId ?? existing.chatUserId,
|
|
19028
|
+
chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
|
|
18835
19029
|
workingDirectory: data.workingDirectory,
|
|
18836
19030
|
model: data.model,
|
|
18837
19031
|
mode: data.mode ?? existing.mode,
|
|
@@ -18845,6 +19039,8 @@ var JsonFileStore = class {
|
|
|
18845
19039
|
id: uuid(),
|
|
18846
19040
|
channelType: data.channelType,
|
|
18847
19041
|
chatId: data.chatId,
|
|
19042
|
+
chatUserId: data.chatUserId,
|
|
19043
|
+
chatDisplayName: data.chatDisplayName,
|
|
18848
19044
|
codepilotSessionId: data.codepilotSessionId,
|
|
18849
19045
|
sdkSessionId: data.sdkSessionId ?? "",
|
|
18850
19046
|
workingDirectory: data.workingDirectory,
|
|
@@ -18858,6 +19054,15 @@ var JsonFileStore = class {
|
|
|
18858
19054
|
this.persistBindings();
|
|
18859
19055
|
return binding;
|
|
18860
19056
|
}
|
|
19057
|
+
deleteChannelBinding(id) {
|
|
19058
|
+
this.reloadBindings();
|
|
19059
|
+
for (const [key, binding] of this.bindings) {
|
|
19060
|
+
if (binding.id !== id) continue;
|
|
19061
|
+
this.bindings.delete(key);
|
|
19062
|
+
this.persistBindings();
|
|
19063
|
+
return;
|
|
19064
|
+
}
|
|
19065
|
+
}
|
|
18861
19066
|
updateChannelBinding(id, updates) {
|
|
18862
19067
|
this.reloadBindings();
|
|
18863
19068
|
for (const [key, b] of this.bindings) {
|