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 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, shared-thread workflow, and optional Codex integration model.
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
- - only affects bridge-generated feedback such as `/h`, `/status`, and `/threads`
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` 是在这两个工程基础上继续演进的单包版本,重点调整成了本地应用、共享 thread 和可选 Codex 集成的形态。
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
- - 只影响 `/h`、`/status`、`/threads` 这类 bridge 自己生成的反馈
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)
@@ -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 bridge-generated feedback such as replies, /h, or /status.
89
- # Does not affect raw Codex replies.
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 bridge-generated feedback such as replies, /h, or /status.
115
- # Does not affect raw Codex replies. Default is false for WeChat.
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 loadThreadNameIndex(archivedThreadIds) {
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 new Map(Array.from(titles.entries()).map(([threadId, entry]) => [threadId, entry.title]));
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, threadNames, archivedThreadIds) {
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 = threadNames.get(threadId) || buildFallbackTitle(threadId, filePath, cwd);
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 = 12) {
14561
+ function listDesktopSessions(limit) {
14463
14562
  const root = getCodexSessionsRoot();
14464
14563
  if (!fs3.existsSync(root)) return [];
14465
14564
  const archivedThreadIds = loadArchivedThreadIds();
14466
- const threadNames = loadThreadNameIndex(archivedThreadIds);
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 sessions = [];
14574
+ const allSessions = /* @__PURE__ */ new Map();
14470
14575
  for (const filePath of files) {
14471
- const session = parseDesktopSession(filePath, threadNames, archivedThreadIds);
14472
- if (session) sessions.push(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) return existing;
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) {