codex-to-im 0.1.0 → 0.1.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.
@@ -4616,16 +4616,16 @@ function parsePositiveInt(value) {
4616
4616
  }
4617
4617
  function loadConfig() {
4618
4618
  const env = loadRawConfigEnv();
4619
- const rawRuntime = env.get("CTI_RUNTIME") || "claude";
4620
- const runtime = ["claude", "codex", "auto"].includes(rawRuntime) ? rawRuntime : "claude";
4619
+ const rawRuntime = env.get("CTI_RUNTIME") || "codex";
4620
+ const runtime = ["claude", "codex", "auto"].includes(rawRuntime) ? rawRuntime : "codex";
4621
4621
  return {
4622
4622
  runtime,
4623
- enabledChannels: splitCsv(env.get("CTI_ENABLED_CHANNELS")) ?? [],
4623
+ enabledChannels: splitCsv(env.get("CTI_ENABLED_CHANNELS")) ?? ["feishu"],
4624
4624
  defaultWorkDir: env.get("CTI_DEFAULT_WORKDIR") || process.cwd(),
4625
4625
  defaultModel: env.get("CTI_DEFAULT_MODEL") || void 0,
4626
4626
  defaultMode: env.get("CTI_DEFAULT_MODE") || "code",
4627
4627
  historyMessageLimit: parsePositiveInt(env.get("CTI_HISTORY_MESSAGE_LIMIT")) ?? 8,
4628
- codexSkipGitRepoCheck: env.has("CTI_CODEX_SKIP_GIT_REPO_CHECK") ? env.get("CTI_CODEX_SKIP_GIT_REPO_CHECK") === "true" : void 0,
4628
+ codexSkipGitRepoCheck: env.has("CTI_CODEX_SKIP_GIT_REPO_CHECK") ? env.get("CTI_CODEX_SKIP_GIT_REPO_CHECK") === "true" : true,
4629
4629
  tgBotToken: env.get("CTI_TG_BOT_TOKEN") || void 0,
4630
4630
  tgChatId: env.get("CTI_TG_CHAT_ID") || void 0,
4631
4631
  tgAllowedUsers: splitCsv(env.get("CTI_TG_ALLOWED_USERS")),
@@ -4891,7 +4891,7 @@ var CodexProvider = class {
4891
4891
  /** Maps session IDs to Codex thread IDs for resume. */
4892
4892
  threadIds = /* @__PURE__ */ new Map();
4893
4893
  /**
4894
- * Lazily load the Codex SDK. Throws a clear error if not installed.
4894
+ * Lazily load the Codex SDK. Throws a clear error if the installation is incomplete.
4895
4895
  */
4896
4896
  async ensureSDK() {
4897
4897
  if (this.sdk && this.codex) {
@@ -4901,7 +4901,7 @@ var CodexProvider = class {
4901
4901
  this.sdk = await Function('return import("@openai/codex-sdk")')();
4902
4902
  } catch {
4903
4903
  throw new Error(
4904
- "[CodexProvider] @openai/codex-sdk is not installed. Install it with: npm install @openai/codex-sdk"
4904
+ "[CodexProvider] @openai/codex-sdk is missing from this codex-to-im installation. Reinstall codex-to-im or run npm install in the project root."
4905
4905
  );
4906
4906
  }
4907
4907
  const apiKey = process.env.CTI_CODEX_API_KEY || process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || void 0;
@@ -4940,6 +4940,10 @@ var CodexProvider = class {
4940
4940
  { type: "text", text: params.prompt }
4941
4941
  ];
4942
4942
  for (const file of imageFiles) {
4943
+ if (file.filePath && fs2.existsSync(file.filePath)) {
4944
+ parts.push({ type: "local_image", path: file.filePath });
4945
+ continue;
4946
+ }
4943
4947
  const ext = MIME_EXT[file.type] || ".png";
4944
4948
  const tmpPath = path2.join(os2.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
4945
4949
  fs2.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
@@ -5128,9 +5132,39 @@ import os3 from "node:os";
5128
5132
  import path3 from "node:path";
5129
5133
  var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
5130
5134
  var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
5135
+ var TITLE_MAX_CHARS = 72;
5136
+ function getCodexHome() {
5137
+ return process.env.CODEX_HOME || path3.join(os3.homedir(), ".codex");
5138
+ }
5131
5139
  function getCodexSessionsRoot() {
5132
- const codexHome = process.env.CODEX_HOME || path3.join(os3.homedir(), ".codex");
5133
- return path3.join(codexHome, "sessions");
5140
+ return path3.join(getCodexHome(), "sessions");
5141
+ }
5142
+ function getArchivedSessionsRoot() {
5143
+ return path3.join(getCodexHome(), "archived_sessions");
5144
+ }
5145
+ function getSessionIndexPath() {
5146
+ return path3.join(getCodexHome(), "session_index.jsonl");
5147
+ }
5148
+ function extractThreadIdFromRolloutName(name) {
5149
+ const match = name.match(/-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i);
5150
+ return match?.[1] || null;
5151
+ }
5152
+ function loadArchivedThreadIds() {
5153
+ const archivedRoot = getArchivedSessionsRoot();
5154
+ if (!fs3.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
5155
+ let entries;
5156
+ try {
5157
+ entries = fs3.readdirSync(archivedRoot, { withFileTypes: true });
5158
+ } catch {
5159
+ return /* @__PURE__ */ new Set();
5160
+ }
5161
+ const ids = /* @__PURE__ */ new Set();
5162
+ for (const entry of entries) {
5163
+ if (!entry.isFile()) continue;
5164
+ const threadId = extractThreadIdFromRolloutName(entry.name);
5165
+ if (threadId) ids.add(threadId);
5166
+ }
5167
+ return ids;
5134
5168
  }
5135
5169
  function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
5136
5170
  const fd = fs3.openSync(filePath, "r");
@@ -5178,7 +5212,36 @@ function isDesktopLike(meta) {
5178
5212
  const source = meta?.source?.toLowerCase() || "";
5179
5213
  return originator.includes("desktop") || source === "vscode" || source === "desktop";
5180
5214
  }
5181
- function parseDesktopSession(filePath) {
5215
+ function loadThreadNameIndex(archivedThreadIds) {
5216
+ const indexPath = getSessionIndexPath();
5217
+ if (!fs3.existsSync(indexPath)) return /* @__PURE__ */ new Map();
5218
+ let content = "";
5219
+ try {
5220
+ content = fs3.readFileSync(indexPath, "utf-8");
5221
+ } catch {
5222
+ return /* @__PURE__ */ new Map();
5223
+ }
5224
+ const titles = /* @__PURE__ */ new Map();
5225
+ for (const line of content.split(/\r?\n/)) {
5226
+ if (!line.trim()) continue;
5227
+ let parsed;
5228
+ try {
5229
+ parsed = JSON.parse(line);
5230
+ } catch {
5231
+ continue;
5232
+ }
5233
+ const threadId = parsed.id?.trim();
5234
+ const title = trimTitle(parsed.thread_name || "");
5235
+ if (!threadId || !title || archivedThreadIds.has(threadId)) continue;
5236
+ const updatedAt = parsed.updated_at || "";
5237
+ const existing = titles.get(threadId);
5238
+ if (!existing || updatedAt >= existing.updatedAt) {
5239
+ titles.set(threadId, { title, updatedAt });
5240
+ }
5241
+ }
5242
+ return new Map(Array.from(titles.entries()).map(([threadId, entry]) => [threadId, entry.title]));
5243
+ }
5244
+ function parseDesktopSession(filePath, threadNames, archivedThreadIds) {
5182
5245
  const firstLine = readFirstLine(filePath);
5183
5246
  if (!firstLine) return null;
5184
5247
  let parsed;
@@ -5190,6 +5253,9 @@ function parseDesktopSession(filePath) {
5190
5253
  if (parsed.type !== "session_meta" || !parsed.payload?.id || !isDesktopLike(parsed.payload)) {
5191
5254
  return null;
5192
5255
  }
5256
+ if (archivedThreadIds.has(parsed.payload.id)) {
5257
+ return null;
5258
+ }
5193
5259
  let stat;
5194
5260
  try {
5195
5261
  stat = fs3.statSync(filePath);
@@ -5200,6 +5266,8 @@ function parseDesktopSession(filePath) {
5200
5266
  const lastEventAt = stat.mtime.toISOString();
5201
5267
  const firstSeenAt = parsed.payload.timestamp || parsed.timestamp || stat.birthtime.toISOString();
5202
5268
  const threadId = parsed.payload.id;
5269
+ const title = threadNames.get(threadId);
5270
+ if (!title) return null;
5203
5271
  return {
5204
5272
  threadId,
5205
5273
  filePath,
@@ -5209,18 +5277,27 @@ function parseDesktopSession(filePath) {
5209
5277
  cliVersion: parsed.payload.cli_version || void 0,
5210
5278
  firstSeenAt,
5211
5279
  lastEventAt,
5212
- title: cwd ? path3.basename(cwd) : `Session ${threadId.slice(0, 8)}`,
5280
+ title,
5213
5281
  activeEstimate: Date.now() - stat.mtimeMs < ACTIVE_WINDOW_MS
5214
5282
  };
5215
5283
  }
5284
+ function trimTitle(text2) {
5285
+ const normalized = text2.replace(/\s+/g, " ").trim();
5286
+ if (!normalized) return "";
5287
+ if (normalized.length <= TITLE_MAX_CHARS) return normalized;
5288
+ return `${normalized.slice(0, TITLE_MAX_CHARS - 3).trimEnd()}...`;
5289
+ }
5216
5290
  function listDesktopSessions(limit = 12) {
5217
5291
  const root = getCodexSessionsRoot();
5218
5292
  if (!fs3.existsSync(root)) return [];
5293
+ const archivedThreadIds = loadArchivedThreadIds();
5294
+ const threadNames = loadThreadNameIndex(archivedThreadIds);
5295
+ if (threadNames.size === 0) return [];
5219
5296
  const files = [];
5220
5297
  walkSessionFiles(root, files);
5221
5298
  const sessions = [];
5222
5299
  for (const filePath of files) {
5223
- const session = parseDesktopSession(filePath);
5300
+ const session = parseDesktopSession(filePath, threadNames, archivedThreadIds);
5224
5301
  if (session) sessions.push(session);
5225
5302
  }
5226
5303
  return sessions.sort((a, b) => b.lastEventAt.localeCompare(a.lastEventAt)).slice(0, Math.max(1, limit));
@@ -5229,6 +5306,9 @@ function getDesktopSessionByThreadId(threadId) {
5229
5306
  const sessions = listDesktopSessions(200);
5230
5307
  return sessions.find((session) => session.threadId === threadId) || null;
5231
5308
  }
5309
+ function isArchivedDesktopThread(threadId) {
5310
+ return loadArchivedThreadIds().has(threadId);
5311
+ }
5232
5312
 
5233
5313
  // src/session-bindings.ts
5234
5314
  import path4 from "node:path";
@@ -5237,9 +5317,6 @@ function getSessionName(session) {
5237
5317
  if (session.working_directory) return path4.basename(session.working_directory);
5238
5318
  return session.id.slice(0, 8);
5239
5319
  }
5240
- function getSessionDescription(session) {
5241
- return session.working_directory || "(no working directory)";
5242
- }
5243
5320
  function bindStoreToSession(store, channelType, chatId, sessionId) {
5244
5321
  const session = store.getSession(sessionId);
5245
5322
  if (!session) return null;
@@ -5284,29 +5361,29 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
5284
5361
  model: model || session.model
5285
5362
  });
5286
5363
  }
5287
- function listBindingTargetOptions(store, desktopLimit = 12) {
5288
- const desktopOptions = listDesktopSessions(desktopLimit).map((session) => ({
5364
+ function listBindingTargetOptions(_store, desktopLimit = 12) {
5365
+ return listDesktopSessions(desktopLimit).map((session) => ({
5289
5366
  key: `desktop:${session.threadId}`,
5290
5367
  kind: "desktop",
5291
5368
  id: session.threadId,
5292
- label: `[Desktop] ${session.title}`,
5293
- description: `${session.threadId.slice(0, 8)}... \xB7 ${session.cwd || "(no cwd)"}`
5294
- }));
5295
- const sessionOptions = store.listSessions().map((session) => ({
5296
- key: `session:${session.id}`,
5297
- kind: "session",
5298
- id: session.id,
5299
- label: `[Internal] ${getSessionName(session)}`,
5300
- description: `${session.id.slice(0, 8)}... \xB7 ${getSessionDescription(session)}${session.sdk_session_id ? ` \xB7 thread ${session.sdk_session_id.slice(0, 8)}...` : ""}`
5369
+ label: session.title,
5370
+ description: `${session.threadId.slice(0, 8)}... \xB7 ${session.cwd || "(no cwd)"}`,
5371
+ cwd: session.cwd,
5372
+ threadId: session.threadId
5301
5373
  }));
5302
- return [...desktopOptions, ...sessionOptions];
5303
5374
  }
5304
5375
  function listBindingSummaries(store) {
5305
5376
  return store.listChannelBindings().map((binding) => {
5306
5377
  const session = store.getSession(binding.codepilotSessionId);
5307
5378
  const currentThreadId = binding.sdkSessionId || session?.sdk_session_id || void 0;
5379
+ const desktop = currentThreadId ? getDesktopSessionByThreadId(currentThreadId) : null;
5380
+ const archived = currentThreadId ? isArchivedDesktopThread(currentThreadId) : false;
5308
5381
  const currentTargetKey = currentThreadId ? `desktop:${currentThreadId}` : `session:${binding.codepilotSessionId}`;
5309
- const currentTargetLabel = currentThreadId ? `Desktop thread ${currentThreadId.slice(0, 8)}...` : `Internal session ${binding.codepilotSessionId.slice(0, 8)}...`;
5382
+ const currentTargetLabel = currentThreadId ? desktop?.title || (archived ? `\u5DF2\u5F52\u6863\u684C\u9762\u7EBF\u7A0B ${currentThreadId.slice(0, 8)}...` : `Desktop thread ${currentThreadId.slice(0, 8)}...`) : getSessionName(session || {
5383
+ id: binding.codepilotSessionId,
5384
+ working_directory: binding.workingDirectory,
5385
+ model: binding.model
5386
+ });
5310
5387
  return {
5311
5388
  id: binding.id,
5312
5389
  channelType: binding.channelType,
@@ -5509,6 +5586,10 @@ async function stopBridge() {
5509
5586
  }
5510
5587
  return getBridgeStatus();
5511
5588
  }
5589
+ async function restartBridge() {
5590
+ await stopBridge();
5591
+ return await startBridge();
5592
+ }
5512
5593
  function getBridgeLogs(lines = 200) {
5513
5594
  ensureDirs();
5514
5595
  const filePath = path5.join(logsDir, "bridge.log");
@@ -6483,6 +6564,18 @@ function mergeConfig(payload) {
6483
6564
  weixinMediaEnabled: payload.weixinMediaEnabled === true
6484
6565
  };
6485
6566
  }
6567
+ function getWeixinAccountsPayload() {
6568
+ return listWeixinAccounts().map((account) => ({
6569
+ accountId: account.accountId,
6570
+ name: account.name,
6571
+ userId: account.userId,
6572
+ enabled: account.enabled,
6573
+ baseUrl: account.baseUrl,
6574
+ cdnBaseUrl: account.cdnBaseUrl,
6575
+ lastLoginAt: account.lastLoginAt,
6576
+ updatedAt: account.updatedAt
6577
+ }));
6578
+ }
6486
6579
  async function validateFeishuCredentials(config) {
6487
6580
  if (!config.feishuAppId || !config.feishuAppSecret) {
6488
6581
  return { ok: false, message: "Feishu App ID / App Secret \u4E0D\u80FD\u4E3A\u7A7A\u3002" };
@@ -6559,155 +6652,253 @@ function renderHtml() {
6559
6652
  <title>Codex to IM</title>
6560
6653
  <style>
6561
6654
  :root {
6562
- --bg: #faf8f5;
6655
+ --bg: #f5f7fa;
6563
6656
  --surface: #ffffff;
6564
- --panel: #f2ece5;
6565
- --border: #d7cec3;
6566
- --text: #2f2419;
6567
- --muted: #6d5d4a;
6568
- --primary: #9a3412;
6569
- --primary-strong: #7c2d12;
6570
- --success: #166534;
6571
- --danger: #b91c1c;
6572
- --sidebar: #efe7dd;
6657
+ --surface-soft: #fafafa;
6658
+ --border: #e5e7eb;
6659
+ --border-strong: #d0d7e2;
6660
+ --text: #111827;
6661
+ --muted: #667085;
6662
+ --primary: #1677ff;
6663
+ --primary-strong: #0958d9;
6664
+ --success: #15803d;
6665
+ --danger: #dc2626;
6666
+ --sidebar: #001529;
6667
+ --sidebar-border: #0f2b46;
6668
+ --sidebar-text: #c7d2e0;
6669
+ --sidebar-active: #1677ff;
6670
+ --code-bg: #0f172a;
6573
6671
  }
6574
6672
 
6575
6673
  * { box-sizing: border-box; }
6576
6674
  html, body { height: 100%; }
6577
6675
  body {
6578
6676
  margin: 0;
6579
- font: 14px/1.5 "SF Pro Text", "PingFang SC", "Microsoft YaHei", sans-serif;
6677
+ font: 14px/1.5 "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
6580
6678
  color: var(--text);
6581
6679
  background: var(--bg);
6582
6680
  }
6583
6681
 
6682
+ button, input, select, textarea {
6683
+ font: inherit;
6684
+ }
6685
+
6584
6686
  .shell {
6585
6687
  min-height: 100vh;
6586
6688
  display: grid;
6587
- grid-template-columns: 248px minmax(0, 1fr);
6689
+ grid-template-columns: 232px minmax(0, 1fr);
6588
6690
  }
6589
6691
 
6590
6692
  .sidebar {
6591
6693
  background: var(--sidebar);
6592
- border-right: 1px solid var(--border);
6593
- padding: 28px 20px;
6694
+ color: var(--sidebar-text);
6695
+ padding: 20px 16px;
6696
+ border-right: 1px solid var(--sidebar-border);
6697
+ }
6698
+
6699
+ .brand {
6700
+ padding: 10px 12px 18px;
6701
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
6702
+ margin-bottom: 18px;
6594
6703
  }
6595
6704
 
6596
- .sidebar-title {
6597
- font-size: 17px;
6705
+ .brand-title {
6706
+ margin: 0;
6707
+ color: #ffffff;
6708
+ font-size: 16px;
6598
6709
  font-weight: 700;
6599
- margin: 0 0 6px;
6600
6710
  }
6601
6711
 
6602
- .sidebar-copy {
6603
- margin: 0 0 24px;
6604
- color: var(--muted);
6712
+ .brand-copy {
6713
+ margin: 6px 0 0;
6714
+ color: var(--sidebar-text);
6715
+ font-size: 13px;
6605
6716
  }
6606
6717
 
6607
6718
  .nav {
6608
6719
  display: grid;
6609
- gap: 8px;
6720
+ gap: 4px;
6610
6721
  }
6611
6722
 
6612
- .nav a {
6613
- color: var(--text);
6614
- text-decoration: none;
6615
- padding: 8px 10px;
6723
+ .nav-link {
6724
+ width: 100%;
6725
+ border: 0;
6726
+ background: transparent;
6727
+ color: var(--sidebar-text);
6728
+ text-align: left;
6729
+ padding: 10px 12px;
6616
6730
  border-radius: 8px;
6731
+ cursor: pointer;
6732
+ }
6733
+
6734
+ .nav-link:hover {
6735
+ background: rgba(255, 255, 255, 0.08);
6736
+ color: #ffffff;
6617
6737
  }
6618
6738
 
6619
- .nav a:hover {
6620
- background: rgba(255, 255, 255, 0.55);
6739
+ .nav-link.active {
6740
+ background: var(--sidebar-active);
6741
+ color: #ffffff;
6621
6742
  }
6622
6743
 
6623
6744
  .main {
6624
- padding: 28px 32px 40px;
6745
+ padding: 28px 32px 36px;
6625
6746
  }
6626
6747
 
6627
- .topbar {
6748
+ .page {
6749
+ display: none;
6750
+ }
6751
+
6752
+ .page.active {
6753
+ display: block;
6754
+ }
6755
+
6756
+ .page-header {
6628
6757
  display: flex;
6629
6758
  justify-content: space-between;
6630
6759
  align-items: flex-start;
6631
- gap: 20px;
6632
- margin-bottom: 24px;
6760
+ gap: 16px;
6761
+ margin-bottom: 20px;
6633
6762
  }
6634
6763
 
6635
- .topbar h1 {
6636
- margin: 0 0 4px;
6764
+ .page-title {
6765
+ margin: 0;
6637
6766
  font-size: 28px;
6638
6767
  line-height: 1.2;
6639
6768
  }
6640
6769
 
6641
- .topbar p {
6642
- margin: 0;
6770
+ .page-copy {
6771
+ margin: 6px 0 0;
6643
6772
  color: var(--muted);
6644
6773
  }
6645
6774
 
6646
6775
  .status-grid {
6647
6776
  display: grid;
6648
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
6649
- gap: 12px;
6650
- margin-bottom: 24px;
6777
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
6778
+ gap: 16px;
6779
+ margin-bottom: 20px;
6651
6780
  }
6652
6781
 
6653
- .status-card, .panel {
6782
+ .status-card,
6783
+ .panel {
6654
6784
  background: var(--surface);
6655
6785
  border: 1px solid var(--border);
6656
6786
  border-radius: 10px;
6657
6787
  }
6658
6788
 
6659
6789
  .status-card {
6660
- padding: 14px 16px;
6790
+ padding: 16px 18px;
6661
6791
  }
6662
6792
 
6663
6793
  .status-card strong {
6664
6794
  display: block;
6665
6795
  font-size: 12px;
6666
6796
  color: var(--muted);
6667
- margin-bottom: 8px;
6668
6797
  font-weight: 600;
6798
+ margin-bottom: 8px;
6669
6799
  }
6670
6800
 
6671
6801
  .status-value {
6672
- font-size: 20px;
6802
+ font-size: 22px;
6803
+ line-height: 1.2;
6673
6804
  font-weight: 700;
6805
+ word-break: break-word;
6674
6806
  }
6675
6807
 
6676
- .stack {
6677
- display: grid;
6678
- gap: 20px;
6808
+ .panel {
6809
+ padding: 20px;
6679
6810
  }
6680
6811
 
6681
6812
  .section-grid {
6682
6813
  display: grid;
6683
6814
  grid-template-columns: repeat(2, minmax(0, 1fr));
6684
- gap: 18px;
6815
+ gap: 20px;
6685
6816
  }
6686
6817
 
6687
- .channel-grid {
6818
+ .overview-grid {
6688
6819
  display: grid;
6689
- grid-template-columns: repeat(2, minmax(0, 1fr));
6690
- gap: 18px;
6820
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.9fr);
6821
+ gap: 20px;
6691
6822
  }
6692
6823
 
6693
- .panel {
6694
- padding: 18px;
6824
+ .panel-header {
6825
+ display: flex;
6826
+ justify-content: space-between;
6827
+ align-items: flex-start;
6828
+ gap: 12px;
6829
+ margin-bottom: 16px;
6695
6830
  }
6696
6831
 
6697
- .panel h2 {
6698
- margin: 0 0 16px;
6832
+ .panel-header h2,
6833
+ .panel-header h3 {
6834
+ margin: 0;
6699
6835
  font-size: 18px;
6700
6836
  }
6701
6837
 
6838
+ .panel-header p {
6839
+ margin: 6px 0 0;
6840
+ color: var(--muted);
6841
+ }
6842
+
6843
+ .toolbar,
6844
+ .actions,
6845
+ .session-actions {
6846
+ display: flex;
6847
+ gap: 10px;
6848
+ flex-wrap: wrap;
6849
+ }
6850
+
6851
+ button {
6852
+ border: 1px solid var(--border-strong);
6853
+ background: #ffffff;
6854
+ color: var(--text);
6855
+ border-radius: 8px;
6856
+ padding: 9px 14px;
6857
+ cursor: pointer;
6858
+ }
6859
+
6860
+ button:hover {
6861
+ border-color: var(--primary);
6862
+ color: var(--primary);
6863
+ }
6864
+
6865
+ button.primary {
6866
+ background: var(--primary);
6867
+ border-color: var(--primary);
6868
+ color: #ffffff;
6869
+ }
6870
+
6871
+ button.primary:hover {
6872
+ background: var(--primary-strong);
6873
+ border-color: var(--primary-strong);
6874
+ color: #ffffff;
6875
+ }
6876
+
6877
+ button[disabled] {
6878
+ border-color: var(--border);
6879
+ color: #9ca3af;
6880
+ background: #f3f4f6;
6881
+ cursor: not-allowed;
6882
+ }
6883
+
6884
+ button[disabled]:hover {
6885
+ border-color: var(--border);
6886
+ color: #9ca3af;
6887
+ }
6888
+
6702
6889
  .fields {
6703
6890
  display: grid;
6704
- gap: 14px;
6891
+ gap: 16px;
6705
6892
  }
6706
6893
 
6707
6894
  .field-row {
6708
6895
  display: grid;
6709
6896
  grid-template-columns: repeat(2, minmax(0, 1fr));
6710
- gap: 14px;
6897
+ gap: 16px;
6898
+ }
6899
+
6900
+ .field-row.triple {
6901
+ grid-template-columns: repeat(3, minmax(0, 1fr));
6711
6902
  }
6712
6903
 
6713
6904
  label {
@@ -6719,16 +6910,15 @@ function renderHtml() {
6719
6910
 
6720
6911
  input, select, textarea {
6721
6912
  width: 100%;
6722
- border: 1px solid var(--border);
6723
- background: #fff;
6913
+ border: 1px solid var(--border-strong);
6914
+ background: #ffffff;
6724
6915
  color: var(--text);
6725
6916
  border-radius: 8px;
6726
6917
  padding: 10px 12px;
6727
- font: inherit;
6728
6918
  }
6729
6919
 
6730
6920
  input:focus, select:focus, textarea:focus {
6731
- outline: 2px solid rgba(154, 52, 18, 0.16);
6921
+ outline: 2px solid rgba(22, 119, 255, 0.14);
6732
6922
  border-color: var(--primary);
6733
6923
  }
6734
6924
 
@@ -6739,7 +6929,7 @@ function renderHtml() {
6739
6929
 
6740
6930
  .checkbox-row {
6741
6931
  display: flex;
6742
- gap: 18px;
6932
+ gap: 16px;
6743
6933
  flex-wrap: wrap;
6744
6934
  }
6745
6935
 
@@ -6748,7 +6938,6 @@ function renderHtml() {
6748
6938
  align-items: center;
6749
6939
  gap: 8px;
6750
6940
  color: var(--text);
6751
- font-weight: 500;
6752
6941
  }
6753
6942
 
6754
6943
  .checkbox input {
@@ -6757,129 +6946,217 @@ function renderHtml() {
6757
6946
  margin: 0;
6758
6947
  }
6759
6948
 
6760
- .actions {
6761
- display: flex;
6949
+ .notice {
6950
+ padding: 12px 14px;
6951
+ background: #f8fafc;
6952
+ border: 1px solid #e2e8f0;
6953
+ border-radius: 8px;
6954
+ color: var(--muted);
6955
+ }
6956
+
6957
+ .message {
6958
+ display: none;
6959
+ margin-top: 14px;
6960
+ padding: 10px 12px;
6961
+ border-radius: 8px;
6962
+ }
6963
+
6964
+ .message.show { display: block; }
6965
+ .message.success { background: rgba(21, 128, 61, 0.10); color: var(--success); }
6966
+ .message.error { background: rgba(220, 38, 38, 0.10); color: var(--danger); }
6967
+
6968
+ .global-message-host {
6969
+ position: fixed;
6970
+ top: 20px;
6971
+ left: 50%;
6972
+ transform: translateX(-50%);
6973
+ display: grid;
6762
6974
  gap: 10px;
6763
- flex-wrap: wrap;
6975
+ z-index: 2000;
6976
+ pointer-events: none;
6764
6977
  }
6765
6978
 
6766
- button {
6979
+ .global-message {
6980
+ min-width: 240px;
6981
+ max-width: min(560px, calc(100vw - 32px));
6982
+ padding: 10px 14px;
6983
+ border-radius: 8px;
6767
6984
  border: 1px solid var(--border);
6768
- background: #fff;
6985
+ background: rgba(255, 255, 255, 0.96);
6986
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
6769
6987
  color: var(--text);
6770
- border-radius: 8px;
6771
- padding: 10px 14px;
6772
- font: inherit;
6773
- cursor: pointer;
6774
6988
  }
6775
6989
 
6776
- button.primary {
6777
- background: var(--primary);
6778
- border-color: var(--primary);
6779
- color: #fff;
6990
+ .global-message.success {
6991
+ border-color: rgba(21, 128, 61, 0.16);
6780
6992
  }
6781
6993
 
6782
- button.primary:hover {
6783
- background: var(--primary-strong);
6994
+ .global-message.error {
6995
+ border-color: rgba(220, 38, 38, 0.16);
6784
6996
  }
6785
6997
 
6786
- button:hover {
6787
- border-color: var(--primary);
6998
+ .info-list {
6999
+ display: grid;
7000
+ gap: 12px;
6788
7001
  }
6789
7002
 
6790
- .notice {
7003
+ .info-item {
6791
7004
  padding: 12px 14px;
7005
+ border: 1px solid var(--border);
6792
7006
  border-radius: 8px;
6793
- background: var(--panel);
7007
+ background: var(--surface-soft);
7008
+ }
7009
+
7010
+ .info-item strong {
7011
+ display: block;
7012
+ margin-bottom: 4px;
7013
+ font-size: 12px;
6794
7014
  color: var(--muted);
6795
7015
  }
6796
7016
 
6797
- .notice strong {
6798
- color: var(--text);
7017
+ .mono,
7018
+ .project-group-path,
7019
+ .session-path,
7020
+ .binding-detail code {
7021
+ font-family: "Cascadia Code", Consolas, "SF Mono", monospace;
6799
7022
  }
6800
7023
 
6801
- .logs {
6802
- white-space: pre-wrap;
6803
- word-break: break-word;
6804
- background: #231b14;
6805
- color: #f3eee7;
7024
+ .session-list {
7025
+ display: grid;
7026
+ gap: 16px;
7027
+ }
7028
+
7029
+ .project-group {
7030
+ border: 1px solid var(--border);
6806
7031
  border-radius: 10px;
6807
- padding: 14px;
6808
- min-height: 260px;
6809
- overflow: auto;
7032
+ background: var(--surface);
7033
+ padding: 16px;
7034
+ display: grid;
7035
+ gap: 14px;
6810
7036
  }
6811
7037
 
6812
- .message {
6813
- margin-top: 12px;
6814
- padding: 10px 12px;
6815
- border-radius: 8px;
6816
- display: none;
7038
+ .project-group-head {
7039
+ display: flex;
7040
+ justify-content: space-between;
7041
+ align-items: flex-start;
7042
+ gap: 16px;
6817
7043
  }
6818
7044
 
6819
- .message.show { display: block; }
6820
- .message.success { background: rgba(22, 101, 52, 0.1); color: var(--success); }
6821
- .message.error { background: rgba(185, 28, 28, 0.1); color: var(--danger); }
7045
+ .project-group-title {
7046
+ font-size: 16px;
7047
+ font-weight: 700;
7048
+ }
6822
7049
 
6823
- .session-list {
7050
+ .project-group-path {
7051
+ color: var(--muted);
7052
+ font-size: 12px;
7053
+ margin-top: 4px;
7054
+ word-break: break-all;
7055
+ }
7056
+
7057
+ .project-group-count {
7058
+ color: var(--muted);
7059
+ font-size: 12px;
7060
+ white-space: nowrap;
7061
+ }
7062
+
7063
+ .project-session-list {
6824
7064
  display: grid;
6825
- gap: 10px;
7065
+ gap: 12px;
6826
7066
  }
6827
7067
 
6828
7068
  .session-card {
6829
7069
  border: 1px solid var(--border);
6830
7070
  border-radius: 10px;
6831
- padding: 12px 14px;
6832
- background: #fcfbf9;
7071
+ padding: 14px;
7072
+ background: var(--surface-soft);
7073
+ }
7074
+
7075
+ .session-card.current-thread {
7076
+ border-color: rgba(22, 119, 255, 0.32);
7077
+ background: rgba(22, 119, 255, 0.03);
6833
7078
  }
6834
7079
 
6835
7080
  .session-head {
6836
- display: flex;
6837
- justify-content: space-between;
6838
- gap: 12px;
7081
+ display: grid;
7082
+ grid-template-columns: minmax(0, 1.9fr) 150px minmax(220px, 1fr) auto;
7083
+ gap: 16px;
6839
7084
  align-items: center;
6840
- margin-bottom: 6px;
7085
+ }
7086
+
7087
+ .session-main {
7088
+ min-width: 0;
7089
+ display: grid;
7090
+ gap: 4px;
6841
7091
  }
6842
7092
 
6843
7093
  .session-title {
6844
7094
  font-weight: 700;
6845
7095
  }
6846
7096
 
6847
- .session-meta {
7097
+ .session-title-row {
6848
7098
  display: flex;
7099
+ align-items: center;
6849
7100
  gap: 8px;
6850
7101
  flex-wrap: wrap;
6851
- margin: 8px 0;
6852
7102
  }
6853
7103
 
6854
- .session-pill {
7104
+ .session-mark {
6855
7105
  display: inline-flex;
6856
7106
  align-items: center;
6857
- padding: 3px 8px;
7107
+ padding: 1px 8px;
6858
7108
  border-radius: 999px;
6859
- border: 1px solid var(--border);
7109
+ background: rgba(22, 119, 255, 0.10);
7110
+ color: var(--primary);
7111
+ font-size: 12px;
7112
+ border: 1px solid rgba(22, 119, 255, 0.16);
7113
+ }
7114
+
7115
+ .session-thread {
6860
7116
  color: var(--muted);
6861
- background: #fff;
6862
7117
  font-size: 12px;
6863
7118
  }
6864
7119
 
6865
- .session-pill.active {
6866
- color: var(--success);
6867
- border-color: rgba(22, 101, 52, 0.26);
6868
- background: rgba(22, 101, 52, 0.08);
7120
+ .session-thread code {
7121
+ word-break: break-all;
6869
7122
  }
6870
7123
 
6871
- .session-path {
6872
- font-family: "SF Mono", "Cascadia Code", Consolas, monospace;
7124
+ .session-inline-action {
7125
+ border: 0;
7126
+ background: transparent;
7127
+ color: var(--primary);
7128
+ padding: 0;
7129
+ margin-left: 8px;
7130
+ font-size: 12px;
7131
+ }
7132
+
7133
+ .session-inline-action:hover {
7134
+ color: var(--primary-strong);
7135
+ text-decoration: underline;
7136
+ }
7137
+
7138
+ .session-cell {
7139
+ min-width: 0;
7140
+ display: grid;
7141
+ gap: 4px;
7142
+ }
7143
+
7144
+ .session-label {
7145
+ color: var(--muted);
6873
7146
  font-size: 12px;
7147
+ }
7148
+
7149
+ .session-value,
7150
+ .session-path {
6874
7151
  color: var(--muted);
7152
+ font-size: 12px;
6875
7153
  word-break: break-all;
6876
7154
  }
6877
7155
 
6878
7156
  .session-actions {
6879
- display: flex;
6880
- gap: 8px;
7157
+ justify-content: flex-end;
7158
+ align-items: center;
6881
7159
  flex-wrap: wrap;
6882
- margin-top: 10px;
6883
7160
  }
6884
7161
 
6885
7162
  .panel-block {
@@ -6894,217 +7171,535 @@ function renderHtml() {
6894
7171
  font-weight: 700;
6895
7172
  }
6896
7173
 
6897
- .binding-list {
6898
- display: grid;
6899
- gap: 10px;
7174
+ .channel-shell {
7175
+ padding: 0;
7176
+ overflow: hidden;
6900
7177
  }
6901
7178
 
6902
- .binding-item {
6903
- border: 1px solid var(--border);
6904
- border-radius: 10px;
6905
- background: #fcfbf9;
6906
- padding: 12px 14px;
7179
+ .channel-tabs {
7180
+ display: flex;
7181
+ align-items: flex-end;
7182
+ gap: 0;
7183
+ padding: 0 20px;
7184
+ border-bottom: 1px solid var(--border);
7185
+ background: #ffffff;
6907
7186
  }
6908
7187
 
6909
- .binding-head {
6910
- display: flex;
6911
- justify-content: space-between;
6912
- align-items: baseline;
6913
- gap: 10px;
6914
- margin-bottom: 6px;
7188
+ .command-sections {
7189
+ display: grid;
7190
+ gap: 14px;
6915
7191
  }
6916
7192
 
6917
- .binding-title {
6918
- font-weight: 700;
7193
+ .command-section {
7194
+ border: 1px solid var(--border);
7195
+ border-radius: 8px;
7196
+ background: var(--surface-soft);
7197
+ overflow: hidden;
6919
7198
  }
6920
7199
 
6921
- .binding-detail {
6922
- color: var(--muted);
6923
- font-size: 12px;
6924
- margin-top: 4px;
6925
- word-break: break-all;
7200
+ .command-section-title {
7201
+ margin: 0;
7202
+ padding: 10px 14px;
7203
+ font-size: 14px;
7204
+ font-weight: 700;
7205
+ border-bottom: 1px solid var(--border);
7206
+ background: #ffffff;
6926
7207
  }
6927
7208
 
6928
- .binding-controls {
7209
+ .command-list {
7210
+ display: grid;
7211
+ }
7212
+
7213
+ .command-item {
7214
+ display: grid;
7215
+ grid-template-columns: 280px minmax(0, 1fr);
7216
+ gap: 16px;
7217
+ padding: 10px 14px;
7218
+ border-top: 1px solid var(--border);
7219
+ }
7220
+
7221
+ .command-item:first-child {
7222
+ border-top: 0;
7223
+ }
7224
+
7225
+ .command-item code {
7226
+ word-break: break-all;
7227
+ }
7228
+
7229
+ .channel-tab {
7230
+ border: 0;
7231
+ border-bottom: 2px solid transparent;
7232
+ border-radius: 0;
7233
+ padding: 14px 18px 12px;
7234
+ background: transparent;
7235
+ color: var(--muted);
7236
+ margin-bottom: -1px;
7237
+ }
7238
+
7239
+ .channel-tab.active {
7240
+ color: var(--primary);
7241
+ border-bottom-color: var(--primary);
7242
+ background: transparent;
7243
+ }
7244
+
7245
+ .channel-view {
7246
+ display: none;
7247
+ padding: 20px;
7248
+ }
7249
+
7250
+ .channel-view.active {
7251
+ display: block;
7252
+ }
7253
+
7254
+ .binding-list {
7255
+ display: grid;
7256
+ gap: 10px;
7257
+ }
7258
+
7259
+ .binding-item {
7260
+ border: 1px solid var(--border);
7261
+ border-radius: 10px;
7262
+ background: var(--surface-soft);
7263
+ padding: 12px 14px;
7264
+ }
7265
+
7266
+ .binding-head {
7267
+ display: flex;
7268
+ justify-content: space-between;
7269
+ align-items: baseline;
7270
+ gap: 10px;
7271
+ margin-bottom: 6px;
7272
+ }
7273
+
7274
+ .binding-title {
7275
+ font-weight: 700;
7276
+ }
7277
+
7278
+ .binding-detail {
7279
+ color: var(--muted);
7280
+ font-size: 12px;
7281
+ margin-top: 4px;
7282
+ word-break: break-all;
7283
+ }
7284
+
7285
+ .binding-controls {
6929
7286
  display: grid;
6930
7287
  grid-template-columns: minmax(0, 1fr) auto;
6931
7288
  gap: 10px;
6932
7289
  margin-top: 12px;
6933
7290
  }
6934
7291
 
6935
- .binding-empty {
6936
- padding: 12px 14px;
6937
- border: 1px dashed var(--border);
7292
+ .binding-table-wrap {
7293
+ margin-top: 12px;
7294
+ border: 1px solid var(--border);
6938
7295
  border-radius: 10px;
7296
+ background: #ffffff;
7297
+ overflow: hidden;
7298
+ }
7299
+
7300
+ .binding-table {
7301
+ width: 100%;
7302
+ border-collapse: collapse;
7303
+ }
7304
+
7305
+ .binding-table th,
7306
+ .binding-table td {
7307
+ padding: 10px 12px;
7308
+ border-top: 1px solid var(--border);
7309
+ text-align: left;
7310
+ vertical-align: top;
7311
+ }
7312
+
7313
+ .binding-table thead th {
7314
+ border-top: 0;
7315
+ background: #f8fafc;
7316
+ color: var(--muted);
7317
+ font-size: 12px;
7318
+ font-weight: 700;
7319
+ }
7320
+
7321
+ .binding-table tbody tr.current {
7322
+ background: rgba(22, 119, 255, 0.04);
7323
+ }
7324
+
7325
+ .binding-table-title {
7326
+ font-weight: 700;
7327
+ word-break: break-word;
7328
+ }
7329
+
7330
+ .binding-table-thread,
7331
+ .binding-table-path {
7332
+ font-size: 12px;
6939
7333
  color: var(--muted);
6940
- background: #fcfbf9;
7334
+ word-break: break-all;
7335
+ }
7336
+
7337
+ .binding-table-mark {
7338
+ display: inline-flex;
7339
+ align-items: center;
7340
+ margin-left: 8px;
7341
+ padding: 1px 8px;
7342
+ border-radius: 999px;
7343
+ background: rgba(22, 119, 255, 0.10);
7344
+ color: var(--primary);
7345
+ font-size: 12px;
7346
+ }
7347
+
7348
+ .binding-target-btn {
7349
+ border: 1px solid var(--border);
7350
+ border-radius: 8px;
7351
+ background: #ffffff;
7352
+ white-space: nowrap;
7353
+ }
7354
+
7355
+ .binding-target-btn.current {
7356
+ border-color: rgba(22, 119, 255, 0.30);
7357
+ background: rgba(22, 119, 255, 0.08);
7358
+ color: var(--primary);
6941
7359
  }
6942
7360
 
6943
- .ghost {
7361
+ .binding-empty {
7362
+ padding: 12px 14px;
7363
+ border: 1px dashed var(--border-strong);
7364
+ border-radius: 8px;
6944
7365
  color: var(--muted);
7366
+ background: var(--surface-soft);
6945
7367
  }
6946
7368
 
7369
+ .logs {
7370
+ white-space: pre-wrap;
7371
+ word-break: break-word;
7372
+ background: var(--code-bg);
7373
+ color: #e2e8f0;
7374
+ border-radius: 10px;
7375
+ padding: 16px;
7376
+ min-height: 420px;
7377
+ overflow: auto;
7378
+ }
7379
+
7380
+ .ghost,
6947
7381
  .small {
6948
7382
  color: var(--muted);
6949
7383
  font-size: 12px;
6950
7384
  }
6951
7385
 
6952
- @media (max-width: 1080px) {
7386
+ @media (max-width: 1180px) {
7387
+ .overview-grid,
7388
+ .section-grid {
7389
+ grid-template-columns: 1fr;
7390
+ }
7391
+ }
7392
+
7393
+ @media (max-width: 980px) {
6953
7394
  .shell { grid-template-columns: 1fr; }
6954
- .sidebar { border-right: 0; border-bottom: 1px solid var(--border); }
6955
- .section-grid { grid-template-columns: 1fr; }
6956
- .channel-grid { grid-template-columns: 1fr; }
6957
- .status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
7395
+ .sidebar { border-right: 0; border-bottom: 1px solid var(--sidebar-border); }
7396
+ .nav { grid-template-columns: repeat(6, minmax(0, 1fr)); }
7397
+ .main { padding: 20px 20px 28px; }
7398
+ .field-row,
7399
+ .field-row.triple,
7400
+ .command-item,
6958
7401
  .binding-controls { grid-template-columns: 1fr; }
7402
+ .status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
7403
+ }
7404
+
7405
+ @media (max-width: 720px) {
7406
+ .nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
7407
+ .status-grid { grid-template-columns: 1fr; }
7408
+ .page-header,
7409
+ .panel-header,
7410
+ .project-group-head,
7411
+ .binding-head { flex-direction: column; align-items: stretch; }
7412
+ .session-head { grid-template-columns: 1fr; }
7413
+ .session-actions { justify-content: flex-start; }
6959
7414
  }
6960
7415
  </style>
6961
7416
  </head>
6962
7417
  <body>
6963
7418
  <div class="shell">
6964
7419
  <aside class="sidebar">
6965
- <p class="sidebar-title">Codex to IM</p>
6966
- <p class="sidebar-copy">\u672C\u5730\u5B89\u88C5\u3001\u914D\u7F6E\u3001\u6D4B\u8BD5\u548C\u540E\u53F0\u63A7\u5236\u90FD\u5728\u8FD9\u91CC\u3002</p>
7420
+ <div class="brand">
7421
+ <p class="brand-title">Codex to IM</p>
7422
+ <p class="brand-copy">\u672C\u5730\u540E\u53F0\u3001\u4F1A\u8BDD\u5171\u4EAB\u3001\u901A\u9053\u7ED1\u5B9A\u548C\u8C03\u8BD5\u90FD\u8D70\u8FD9\u91CC\u3002</p>
7423
+ </div>
6967
7424
  <nav class="nav">
6968
- <a href="#overview">\u6982\u89C8</a>
6969
- <a href="#config">\u914D\u7F6E</a>
6970
- <a href="#feishu">\u98DE\u4E66</a>
6971
- <a href="#wechat">\u5FAE\u4FE1</a>
6972
- <a href="#desktop">\u684C\u9762\u4F1A\u8BDD</a>
6973
- <a href="#ops">\u8FD0\u884C\u63A7\u5236</a>
6974
- <a href="#logs">\u65E5\u5FD7</a>
7425
+ <button type="button" class="nav-link active" data-page="overview">\u6982\u89C8</button>
7426
+ <button type="button" class="nav-link" data-page="sessions">\u4F1A\u8BDD</button>
7427
+ <button type="button" class="nav-link" data-page="config">\u914D\u7F6E</button>
7428
+ <button type="button" class="nav-link" data-page="channels">\u901A\u9053</button>
7429
+ <button type="button" class="nav-link" data-page="logs">\u65E5\u5FD7</button>
7430
+ <button type="button" class="nav-link" data-page="commands">\u547D\u4EE4\u8BF4\u660E</button>
6975
7431
  </nav>
6976
7432
  </aside>
6977
7433
  <main class="main">
6978
- <section class="topbar" id="overview">
6979
- <div>
6980
- <h1>\u672C\u5730\u6865\u63A5\u5DE5\u4F5C\u53F0</h1>
6981
- <p>\u540E\u53F0\u670D\u52A1\u3001\u901A\u9053\u914D\u7F6E\u3001\u684C\u9762\u4F1A\u8BDD\u5171\u4EAB\u548C\u901A\u9053\u6D4B\u8BD5\u90FD\u8D70\u8FD9\u4E00\u9875\u3002</p>
6982
- </div>
6983
- </section>
6984
-
6985
- <section class="status-grid">
6986
- <div class="status-card">
6987
- <strong>Bridge</strong>
6988
- <div class="status-value" id="bridgeStatus">-</div>
6989
- </div>
6990
- <div class="status-card">
6991
- <strong>Codex \u96C6\u6210</strong>
6992
- <div class="status-value" id="integrationStatus">-</div>
6993
- </div>
6994
- <div class="status-card">
6995
- <strong>Runtime</strong>
6996
- <div class="status-value" id="runtimeStatus">-</div>
6997
- </div>
6998
- <div class="status-card">
6999
- <strong>Desktop Sessions</strong>
7000
- <div class="status-value" id="desktopSessionCount">-</div>
7001
- </div>
7002
- <div class="status-card">
7003
- <strong>IM Bindings</strong>
7004
- <div class="status-value" id="bindingCount">-</div>
7005
- </div>
7006
- <div class="status-card">
7007
- <strong>Config Home</strong>
7008
- <div class="status-value" id="homeStatus" style="font-size:14px;">-</div>
7434
+ <section class="page active" data-page="overview">
7435
+ <div class="page-header">
7436
+ <div>
7437
+ <h1 class="page-title">\u6982\u89C8</h1>
7438
+ <p class="page-copy">\u8FD0\u884C\u72B6\u6001\u3001\u540E\u53F0\u63A7\u5236\u548C\u5F53\u524D\u73AF\u5883\u96C6\u4E2D\u5728\u8FD9\u4E00\u9875\u3002</p>
7439
+ </div>
7009
7440
  </div>
7010
- </section>
7011
7441
 
7012
- <div class="stack">
7013
- <section class="panel" id="desktop">
7014
- <h2>\u6700\u8FD1\u684C\u9762\u4F1A\u8BDD</h2>
7015
- <div class="notice">
7016
- \u8FD9\u91CC\u5217\u51FA\u6700\u8FD1\u5728 <code>Codex Windows App</code> \u4E2D\u6D3B\u8DC3\u8FC7\u7684\u672C\u5730\u4F1A\u8BDD\uFF0C\u4F5C\u4E3A\u63A5\u7BA1\u5230\u98DE\u4E66\u6216\u5FAE\u4FE1\u7684\u5019\u9009\u5165\u53E3\u3002
7442
+ <section class="status-grid">
7443
+ <div class="status-card">
7444
+ <strong>Bridge</strong>
7445
+ <div class="status-value" id="bridgeStatus">-</div>
7017
7446
  </div>
7018
- <div class="notice" style="margin-top: 12px;">
7019
- \u6700\u77ED\u8DEF\u5F84\uFF1A\u5728\u8FD9\u91CC\u627E\u5230\u76EE\u6807 thread\uFF0C\u7136\u540E\u628A <code>/thread 019d1da4</code> \u8FD9\u6837\u7684\u547D\u4EE4\u53D1\u7ED9\u98DE\u4E66\u673A\u5668\u4EBA\uFF0C\u6216\u5728\u4E0B\u9762\u7684\u7ED1\u5B9A\u5217\u8868\u91CC\u76F4\u63A5\u5207\u6362\u3002
7447
+ <div class="status-card">
7448
+ <strong>Codex \u96C6\u6210</strong>
7449
+ <div class="status-value" id="integrationStatus">-</div>
7020
7450
  </div>
7021
- <div class="session-actions">
7022
- <button id="refreshDesktopBtn">\u5237\u65B0\u684C\u9762\u4F1A\u8BDD</button>
7451
+ <div class="status-card">
7452
+ <strong>Runtime</strong>
7453
+ <div class="status-value" id="runtimeStatus">-</div>
7454
+ </div>
7455
+ <div class="status-card">
7456
+ <strong>Desktop Sessions</strong>
7457
+ <div class="status-value" id="desktopSessionCount">-</div>
7458
+ </div>
7459
+ <div class="status-card">
7460
+ <strong>IM Bindings</strong>
7461
+ <div class="status-value" id="bindingCount">-</div>
7462
+ </div>
7463
+ <div class="status-card">
7464
+ <strong>Config Home</strong>
7465
+ <div class="status-value" id="homeStatus" style="font-size: 14px;">-</div>
7023
7466
  </div>
7024
- <div class="small" id="desktopSessionMeta" style="margin: 12px 0 14px;">\u6B63\u5728\u52A0\u8F7D\u2026</div>
7025
- <div class="session-list" id="desktopSessionsList"></div>
7026
- <div class="message" id="desktopMessage"></div>
7027
7467
  </section>
7028
7468
 
7029
- <div class="section-grid">
7030
- <section class="panel" id="config">
7031
- <h2>\u57FA\u7840\u914D\u7F6E</h2>
7032
- <div class="fields">
7033
- <div class="field-row">
7034
- <label>
7035
- Runtime
7036
- <select id="runtime">
7037
- <option value="codex">codex</option>
7038
- <option value="auto">auto</option>
7039
- <option value="claude">claude</option>
7040
- </select>
7041
- </label>
7042
- <label>
7043
- \u9ED8\u8BA4\u6A21\u5F0F
7044
- <select id="defaultMode">
7045
- <option value="code">code</option>
7046
- <option value="plan">plan</option>
7047
- <option value="ask">ask</option>
7048
- </select>
7049
- </label>
7050
- <label>
7051
- /history \u8FD4\u56DE\u6761\u6570
7052
- <input id="historyMessageLimit" type="number" min="1" max="20" value="8" />
7053
- </label>
7054
- </div>
7055
- <label>
7056
- \u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55
7057
- <input id="defaultWorkDir" placeholder="D:\\workspace\\project" />
7058
- </label>
7059
- <label>
7060
- \u9ED8\u8BA4\u6A21\u578B
7061
- <input id="defaultModel" placeholder="\u7559\u7A7A\u5219\u4F7F\u7528 runtime \u9ED8\u8BA4\u6A21\u578B" />
7062
- </label>
7063
- <div class="checkbox-row">
7064
- <label class="checkbox"><input id="channelFeishu" type="checkbox" /> \u542F\u7528\u98DE\u4E66</label>
7065
- <label class="checkbox"><input id="channelWeixin" type="checkbox" /> \u542F\u7528\u5FAE\u4FE1</label>
7066
- <label class="checkbox"><input id="autoApprove" type="checkbox" /> \u81EA\u52A8\u6279\u51C6\u5DE5\u5177\u6743\u9650</label>
7067
- </div>
7068
- <div class="checkbox-row">
7069
- <label class="checkbox"><input id="codexSkipGitRepoCheck" type="checkbox" /> \u5141\u8BB8\u5728\u672A\u4FE1\u4EFB Git \u76EE\u5F55\u8FD0\u884C Codex</label>
7469
+ <div class="overview-grid">
7470
+ <section class="panel">
7471
+ <div class="panel-header">
7472
+ <div>
7473
+ <h2>\u8FD0\u884C\u63A7\u5236</h2>
7474
+ <p>\u4FDD\u5B58\u914D\u7F6E\u540E\uFF0C\u53EF\u4EE5\u76F4\u63A5\u5728\u8FD9\u91CC\u542F\u505C bridge\u3001\u6D4B\u8BD5 Codex \u6216\u5237\u65B0\u6574\u4F53\u72B6\u6001\u3002</p>
7070
7475
  </div>
7071
- <div class="small">\u5982\u679C\u65B0\u5EFA\u4F1A\u8BDD\u62A5 \u201CNot inside a trusted directory\u201D\uFF0C\u53EF\u4EE5\u6253\u5F00\u8FD9\u4E2A\u9009\u9879\u3002\u4FEE\u6539\u540E\u9700\u8981\u91CD\u542F Bridge \u624D\u4F1A\u751F\u6548\u3002</div>
7072
7476
  </div>
7073
- </section>
7074
-
7075
- <section class="panel" id="ops">
7076
- <h2>\u8FD0\u884C\u63A7\u5236</h2>
7077
7477
  <div class="actions">
7078
7478
  <button class="primary" id="startBridgeBtn">\u542F\u52A8 Bridge</button>
7079
7479
  <button id="stopBridgeBtn">\u505C\u6B62 Bridge</button>
7480
+ <button id="restartBridgeBtn">\u91CD\u542F Bridge</button>
7080
7481
  <button id="testCodexBtn">\u6D4B\u8BD5 Codex</button>
7081
7482
  <button id="refreshBtn">\u5237\u65B0\u72B6\u6001</button>
7082
7483
  </div>
7484
+
7083
7485
  <div class="panel-block">
7084
7486
  <p class="panel-subtitle">\u5F53\u524D\u80FD\u529B</p>
7085
- <div class="notice">
7086
- \u5DF2\u63A5\u901A\uFF1A\u4FDD\u5B58\u914D\u7F6E\u3001\u540E\u53F0\u542F\u505C\u3001\u98DE\u4E66\u51ED\u636E\u6D4B\u8BD5\u3001\u5FAE\u4FE1\u626B\u7801\u3001Codex \u8FDE\u63A5\u6D4B\u8BD5\u3001\u684C\u9762\u4F1A\u8BDD\u53D1\u73B0\u3001IM \u7ED1\u5B9A\u67E5\u770B\u4E0E\u7F51\u9875\u4FA7\u5207\u6362\u3002
7087
- </div>
7487
+ <div class="notice">\u5DF2\u63A5\u901A\uFF1A\u4FDD\u5B58\u914D\u7F6E\u3001\u540E\u53F0\u542F\u505C\u3001\u98DE\u4E66\u51ED\u636E\u6D4B\u8BD5\u3001\u5FAE\u4FE1\u626B\u7801\u3001Codex \u8FDE\u63A5\u6D4B\u8BD5\u3001\u684C\u9762\u4F1A\u8BDD\u53D1\u73B0\u3001IM \u7ED1\u5B9A\u67E5\u770B\u4E0E\u7F51\u9875\u4FA7\u5207\u6362\u3002</div>
7088
7488
  </div>
7489
+
7089
7490
  <div class="panel-block">
7090
7491
  <p class="panel-subtitle">\u53EF\u9009 Codex \u96C6\u6210</p>
7091
- <div class="notice">
7092
- \u4E3B\u6D41\u7A0B\u4E0D\u4F9D\u8D56\u8FD9\u5C42\u96C6\u6210\u3002\u53EA\u6709\u5F53\u4F60\u60F3\u5728 Codex \u91CC\u76F4\u63A5\u6253\u5F00 <code>codex-to-im</code>\uFF0C\u6216\u8005\u4FDD\u7559\u4E00\u4E2A\u201C\u5171\u4EAB\u5F53\u524D\u4F1A\u8BDD\u5230\u98DE\u4E66\u201D\u7684\u8F7B\u5165\u53E3\u65F6\uFF0C\u624D\u9700\u8981\u5B89\u88C5\u5B83\u3002
7093
- </div>
7492
+ <div class="notice">\u4E3B\u6D41\u7A0B\u4E0D\u4F9D\u8D56\u8FD9\u5C42\u96C6\u6210\u3002\u53EA\u6709\u5F53\u4F60\u60F3\u5728 Codex \u91CC\u76F4\u63A5\u6253\u5F00 codex-to-im\uFF0C\u6216\u8005\u4FDD\u7559\u4E00\u4E2A\u201C\u5171\u4EAB\u5F53\u524D\u4F1A\u8BDD\u5230\u98DE\u4E66\u201D\u7684\u8F7B\u5165\u53E3\u65F6\uFF0C\u624D\u9700\u8981\u5B89\u88C5\u5B83\u3002</div>
7094
7493
  <div class="actions" style="margin-top: 12px;">
7095
7494
  <button id="installIntegrationBtn">\u5B89\u88C5\u53EF\u9009 Codex \u96C6\u6210</button>
7096
7495
  </div>
7097
7496
  </div>
7098
- <div class="small" style="margin-top: 12px;">
7099
- \u5305\u6839\u76EE\u5F55\uFF1A<span id="packageRoot">-</span>
7100
- </div>
7497
+
7101
7498
  <div class="message" id="opsMessage"></div>
7102
7499
  </section>
7500
+
7501
+ <section class="panel">
7502
+ <div class="panel-header">
7503
+ <div>
7504
+ <h2>\u5F53\u524D\u73AF\u5883</h2>
7505
+ <p>\u8FD9\u91CC\u663E\u793A\u672C\u673A\u8FD0\u884C\u65F6\u548C\u5173\u952E\u76EE\u5F55\uFF0C\u4FBF\u4E8E\u6392\u67E5\u90E8\u7F72\u95EE\u9898\u3002</p>
7506
+ </div>
7507
+ </div>
7508
+ <div class="info-list">
7509
+ <div class="info-item">
7510
+ <strong>\u5305\u6839\u76EE\u5F55</strong>
7511
+ <div class="mono" id="packageRoot">-</div>
7512
+ </div>
7513
+ <div class="info-item">
7514
+ <strong>\u914D\u7F6E\u76EE\u5F55</strong>
7515
+ <div class="mono" id="overviewHomeStatus">-</div>
7516
+ </div>
7517
+ <div class="info-item">
7518
+ <strong>\u684C\u9762\u4F1A\u8BDD\u6839\u76EE\u5F55</strong>
7519
+ <div class="mono" id="desktopRootStatus">-</div>
7520
+ </div>
7521
+ <div class="info-item">
7522
+ <strong>\u754C\u9762\u8BF4\u660E</strong>
7523
+ <div>\u5DE6\u4FA7\u5207\u6362\u9875\u9762\uFF1B\u201C\u4F1A\u8BDD\u201D\u7BA1\u7406\u684C\u9762 thread\uFF1B\u201C\u901A\u9053\u201D\u91CC\u67E5\u770B\u98DE\u4E66/\u5FAE\u4FE1\u5F53\u524D\u7ED1\u5B9A\u5E76\u76F4\u63A5\u5207\u6362\u3002</div>
7524
+ </div>
7525
+ </div>
7526
+ </section>
7527
+ </div>
7528
+ </section>
7529
+
7530
+ <section class="page" data-page="sessions">
7531
+ <div class="page-header">
7532
+ <div>
7533
+ <h1 class="page-title">\u4F1A\u8BDD</h1>
7534
+ <p class="page-copy">\u6309\u5DE5\u7A0B\u76EE\u5F55\u67E5\u770B\u6700\u8FD1\u684C\u9762\u4F1A\u8BDD\uFF0C\u5E76\u590D\u5236 thread \u6216\u63A5\u7BA1\u547D\u4EE4\u3002</p>
7535
+ </div>
7536
+ <div class="toolbar">
7537
+ <button id="refreshDesktopBtn">\u5237\u65B0\u684C\u9762\u4F1A\u8BDD</button>
7538
+ </div>
7539
+ </div>
7540
+
7541
+ <section class="panel" id="desktop">
7542
+ <div class="notice">\u8FD9\u91CC\u53EA\u5C55\u793A\u5728 Codex \u684C\u9762\u7D22\u5F15\u91CC\u6709\u540D\u5B57\u7684\u7EBF\u7A0B\uFF0C\u548C Windows App \u5DE6\u4FA7\u5217\u8868\u4FDD\u6301\u4E00\u81F4\u3002</div>
7543
+ <div class="notice" style="margin-top: 12px;">\u6700\u77ED\u8DEF\u5F84\uFF1A\u627E\u5230\u76EE\u6807 thread\uFF0C\u7136\u540E\u628A <code>/thread 019d1da4</code> \u8FD9\u6837\u7684\u547D\u4EE4\u53D1\u7ED9\u98DE\u4E66\u673A\u5668\u4EBA\uFF0C\u6216\u76F4\u63A5\u5230\u201C\u901A\u9053\u201D\u9875\u5207\u6362\u7ED1\u5B9A\u3002</div>
7544
+ <div class="small" id="desktopSessionMeta" style="margin: 14px 0 16px;">\u6B63\u5728\u52A0\u8F7D\u2026</div>
7545
+ <div class="session-list" id="desktopSessionsList"></div>
7546
+ <div class="message" id="desktopMessage"></div>
7547
+ </section>
7548
+ </section>
7549
+
7550
+ <section class="page" data-page="config">
7551
+ <div class="page-header">
7552
+ <div>
7553
+ <h1 class="page-title">\u914D\u7F6E</h1>
7554
+ <p class="page-copy">\u8FD9\u91CC\u7EF4\u62A4\u672C\u5730\u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55\u3001\u8FD0\u884C\u6A21\u5F0F\u548C\u5168\u5C40\u884C\u4E3A\u5F00\u5173\u3002</p>
7555
+ </div>
7556
+ </div>
7557
+
7558
+ <section class="panel" id="config">
7559
+ <div class="panel-header">
7560
+ <div>
7561
+ <h2>\u57FA\u7840\u914D\u7F6E</h2>
7562
+ <p>\u4FDD\u5B58\u540E\u4F1A\u5199\u5165\u672C\u5730\u914D\u7F6E\u76EE\u5F55\uFF1B\u6D89\u53CA\u8FD0\u884C\u65F6\u7684\u9009\u9879\u901A\u5E38\u9700\u8981\u91CD\u542F Bridge \u624D\u751F\u6548\u3002</p>
7563
+ </div>
7564
+ <div class="toolbar">
7565
+ <button class="primary" id="saveConfigBtn">\u4FDD\u5B58\u914D\u7F6E</button>
7566
+ </div>
7567
+ </div>
7568
+
7569
+ <div class="fields">
7570
+ <div class="field-row triple">
7571
+ <label>
7572
+ Runtime
7573
+ <select id="runtime">
7574
+ <option value="codex" selected>codex</option>
7575
+ <option value="auto">auto</option>
7576
+ <option value="claude">claude</option>
7577
+ </select>
7578
+ </label>
7579
+ <label>
7580
+ \u9ED8\u8BA4\u6A21\u5F0F
7581
+ <select id="defaultMode">
7582
+ <option value="code">code</option>
7583
+ <option value="plan">plan</option>
7584
+ <option value="ask">ask</option>
7585
+ </select>
7586
+ </label>
7587
+ <label>
7588
+ /history \u8FD4\u56DE\u6761\u6570
7589
+ <input id="historyMessageLimit" type="number" min="1" max="20" value="8" />
7590
+ </label>
7591
+ </div>
7592
+ <label>
7593
+ \u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55
7594
+ <input id="defaultWorkDir" placeholder="D:\\workspace\\project" />
7595
+ </label>
7596
+ <label>
7597
+ \u9ED8\u8BA4\u6A21\u578B
7598
+ <input id="defaultModel" placeholder="\u7559\u7A7A\u5219\u4F7F\u7528 runtime \u9ED8\u8BA4\u6A21\u578B" />
7599
+ </label>
7600
+ <div class="checkbox-row">
7601
+ <label class="checkbox"><input id="channelFeishu" type="checkbox" checked /> \u542F\u7528\u98DE\u4E66</label>
7602
+ <label class="checkbox"><input id="channelWeixin" type="checkbox" /> \u542F\u7528\u5FAE\u4FE1</label>
7603
+ <label class="checkbox"><input id="autoApprove" type="checkbox" /> \u81EA\u52A8\u6279\u51C6\u5DE5\u5177\u6743\u9650</label>
7604
+ </div>
7605
+ <div class="checkbox-row">
7606
+ <label class="checkbox"><input id="codexSkipGitRepoCheck" type="checkbox" checked /> \u5141\u8BB8\u5728\u672A\u4FE1\u4EFB Git \u76EE\u5F55\u8FD0\u884C Codex</label>
7607
+ </div>
7608
+ <div class="small">\u5982\u679C\u65B0\u5EFA\u4F1A\u8BDD\u62A5 \u201CNot inside a trusted directory\u201D\uFF0C\u53EF\u4EE5\u6253\u5F00\u8FD9\u4E2A\u9009\u9879\u3002\u4FEE\u6539\u540E\u9700\u8981\u91CD\u542F Bridge \u624D\u4F1A\u751F\u6548\u3002</div>
7609
+ </div>
7610
+
7611
+ <div class="message" id="configMessage"></div>
7612
+ </section>
7613
+ </section>
7614
+
7615
+ <section class="page" data-page="commands">
7616
+ <div class="page-header">
7617
+ <div>
7618
+ <h1 class="page-title">\u547D\u4EE4\u8BF4\u660E</h1>
7619
+ <p class="page-copy">\u8FD9\u91CC\u5217\u51FA\u5F53\u524D\u6865\u63A5\u804A\u5929\u91CC\u53EF\u7528\u7684\u547D\u4EE4\uFF0C\u98DE\u4E66\u548C\u5FAE\u4FE1\u5171\u7528\u540C\u4E00\u5957\u8BED\u4E49\u3002</p>
7620
+ </div>
7621
+ </div>
7622
+
7623
+ <section class="panel">
7624
+ <div class="panel-header">
7625
+ <div>
7626
+ <h2>\u547D\u4EE4\u4F7F\u7528\u8BF4\u660E</h2>
7627
+ <p>\u4E0B\u9762\u8FD9\u4E9B\u547D\u4EE4\u9002\u7528\u4E8E\u5F53\u524D\u6865\u63A5\u5230\u7684\u804A\u5929\u901A\u9053\uFF0C\u98DE\u4E66\u548C\u5FAE\u4FE1\u5171\u7528\u540C\u4E00\u5957\u547D\u4EE4\u8BED\u4E49\u3002</p>
7628
+ </div>
7629
+ </div>
7630
+
7631
+ <div class="notice" style="margin-bottom: 16px;">\u6700\u77ED\u4F7F\u7528\u8DEF\u5F84\uFF1A\u5148\u53D1 <code>/threads</code> \u67E5\u770B\u6700\u8FD1\u684C\u9762\u4F1A\u8BDD\uFF0C\u518D\u53D1 <code>/thread 1</code> \u63A5\u7BA1\uFF1B\u4E4B\u540E\u76F4\u63A5\u53D1\u9001\u6587\u672C\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002</div>
7632
+
7633
+ <div class="command-sections">
7634
+ <section class="command-section">
7635
+ <h3 class="command-section-title">\u6700\u5E38\u7528</h3>
7636
+ <div class="command-list">
7637
+ <div class="command-item"><code>/help</code><div>\u67E5\u770B\u5B8C\u6574\u547D\u4EE4\u8BF4\u660E\u3002</div></div>
7638
+ <div class="command-item"><code>/threads</code><div>\u5217\u51FA\u6700\u8FD1\u684C\u9762\u4F1A\u8BDD\u3002</div></div>
7639
+ <div class="command-item"><code>/thread &lt;thread-id | \u5E8F\u53F7&gt;</code><div>\u7ED1\u5B9A\u684C\u9762 thread\uFF0C\u63A5\u7BA1\u684C\u9762\u4F1A\u8BDD\u3002</div></div>
7640
+ <div class="command-item"><code>\u76F4\u63A5\u53D1\u9001\u6587\u672C</code><div>\u7EE7\u7EED\u5F53\u524D\u5DF2\u7ED1\u5B9A\u4F1A\u8BDD\u3002</div></div>
7641
+ <div class="command-item"><code>/status</code><div>\u67E5\u770B\u5F53\u524D\u804A\u5929\u7ED1\u5B9A\u5230\u4E86\u54EA\u6761\u4F1A\u8BDD\u3001thread\u3001\u76EE\u5F55\u548C\u6A21\u5F0F\u3002</div></div>
7642
+ <div class="command-item"><code>/history</code><div>\u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u6700\u8FD1 N \u6761\u6D88\u606F\u3002</div></div>
7643
+ </div>
7644
+ </section>
7645
+
7646
+ <section class="command-section">
7647
+ <h3 class="command-section-title">\u5207\u6362\u4E0E\u7ED1\u5B9A</h3>
7648
+ <div class="command-list">
7649
+ <div class="command-item"><code>/sessions</code><div>\u5217\u51FA\u5185\u90E8\u4F1A\u8BDD\u3002</div></div>
7650
+ <div class="command-item"><code>/use &lt;session-id | \u5E8F\u53F7&gt;</code><div>\u5207\u6362\u5230\u5DF2\u6709\u5185\u90E8\u4F1A\u8BDD\u3002</div></div>
7651
+ <div class="command-item"><code>/bind &lt;session-id | thread-id | \u5E8F\u53F7&gt;</code><div>\u667A\u80FD\u7ED1\u5B9A\uFF0C\u517C\u5BB9\u65E7\u7528\u6CD5\uFF1B\u53EF\u7ED1\u5B9A\u5185\u90E8\u4F1A\u8BDD\u6216\u684C\u9762 thread\u3002</div></div>
7652
+ </div>
7653
+ </section>
7654
+
7655
+ <section class="command-section">
7656
+ <h3 class="command-section-title">\u4F1A\u8BDD\u8BBE\u7F6E</h3>
7657
+ <div class="command-list">
7658
+ <div class="command-item"><code>/new [\u7EDD\u5BF9\u8DEF\u5F84]</code><div>\u65B0\u5EFA\u4F1A\u8BDD\uFF1B\u53EF\u9009\u5730\u6307\u5B9A\u5DE5\u4F5C\u76EE\u5F55\u3002</div></div>
7659
+ <div class="command-item"><code>/cwd /path/to/project</code><div>\u4FEE\u6539\u5F53\u524D\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u3002</div></div>
7660
+ <div class="command-item"><code>/mode plan|code|ask</code><div>\u4FEE\u6539\u5F53\u524D\u4F1A\u8BDD\u6A21\u5F0F\u3002</div></div>
7661
+ <div class="command-item"><code>/stop</code><div>\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002</div></div>
7662
+ </div>
7663
+ </section>
7664
+
7665
+ <section class="command-section">
7666
+ <h3 class="command-section-title">\u6743\u9650</h3>
7667
+ <div class="command-list">
7668
+ <div class="command-item"><code>/perm allow|allow_session|deny &lt;id&gt;</code><div>\u6587\u672C\u65B9\u5F0F\u5904\u7406\u4E00\u4E2A\u5F85\u6279\u51C6\u6743\u9650\u3002</div></div>
7669
+ <div class="command-item"><code>1 / 2 / 3</code><div>\u5FEB\u901F\u5904\u7406\u5355\u4E2A\u5F85\u6279\u51C6\u6743\u9650\u3002</div></div>
7670
+ </div>
7671
+ </section>
7672
+ </div>
7673
+ </section>
7674
+ </section>
7675
+
7676
+ <section class="page" data-page="channels">
7677
+ <div class="page-header">
7678
+ <div>
7679
+ <h1 class="page-title">\u901A\u9053</h1>
7680
+ <p class="page-copy">\u6BCF\u4E2A\u901A\u9053\u90FD\u663E\u793A\u5F53\u524D\u7ED1\u5B9A\u7684\u4F1A\u8BDD\uFF0C\u5E76\u652F\u6301\u5728\u7F51\u9875\u91CC\u76F4\u63A5\u6539\u7ED1\u3002</p>
7681
+ </div>
7103
7682
  </div>
7104
7683
 
7105
- <div class="channel-grid">
7106
- <section class="panel" id="feishu">
7107
- <h2>\u98DE\u4E66 / Lark</h2>
7684
+ <section class="panel channel-shell">
7685
+ <div class="channel-tabs" role="tablist" aria-label="\u901A\u9053\u914D\u7F6E">
7686
+ <button type="button" class="channel-tab active" data-channel="feishu" role="tab" aria-selected="true">\u98DE\u4E66</button>
7687
+ <button type="button" class="channel-tab" data-channel="weixin" role="tab" aria-selected="false">\u5FAE\u4FE1</button>
7688
+ </div>
7689
+
7690
+ <div class="channel-view active" data-channel="feishu" role="tabpanel">
7691
+ <section id="feishu">
7692
+ <div class="panel-header">
7693
+ <div>
7694
+ <h2>\u98DE\u4E66 / Lark</h2>
7695
+ <p>\u586B\u5199\u51ED\u636E\u3001\u6D4B\u8BD5\u53EF\u7528\u6027\uFF0C\u5E76\u67E5\u770B\u5F53\u524D\u98DE\u4E66\u804A\u5929\u7ED1\u5B9A\u5230\u54EA\u6761\u4F1A\u8BDD\u3002</p>
7696
+ </div>
7697
+ <div class="toolbar">
7698
+ <button id="saveFeishuChannelBtn">\u4FDD\u5B58\u901A\u9053\u914D\u7F6E</button>
7699
+ <button id="testFeishuBtn">\u6D4B\u8BD5\u98DE\u4E66\u51ED\u636E</button>
7700
+ </div>
7701
+ </div>
7702
+
7108
7703
  <div class="fields">
7109
7704
  <div class="field-row">
7110
7705
  <label>
@@ -7130,28 +7725,48 @@ function renderHtml() {
7130
7725
  <label class="checkbox"><input id="feishuStreamingEnabled" type="checkbox" checked /> \u542F\u7528\u98DE\u4E66\u6D41\u5F0F\u54CD\u5E94\u5361\u7247</label>
7131
7726
  </div>
7132
7727
  <div class="small">\u9700\u8981\u98DE\u4E66\u4FA7\u5DF2\u5F00\u901A\u53EF\u66F4\u65B0\u5361\u7247\u7684\u76F8\u5173\u80FD\u529B\uFF1B\u5982\u679C\u6743\u9650\u4E0D\u8DB3\uFF0C\u4F1A\u81EA\u52A8\u56DE\u9000\u4E3A\u6700\u7EC8\u7ED3\u679C\u6D88\u606F\u3002</div>
7133
- <div class="actions">
7134
- <button class="primary" id="saveConfigBtn">\u4FDD\u5B58\u914D\u7F6E</button>
7135
- <button id="testFeishuBtn">\u6D4B\u8BD5\u98DE\u4E66\u51ED\u636E</button>
7136
- </div>
7728
+ </div>
7729
+
7730
+ <div class="panel-block">
7731
+ <p class="panel-subtitle">\u901A\u9053\u72B6\u6001</p>
7732
+ <div class="small" id="feishuRuntimeMeta">\u6B63\u5728\u52A0\u8F7D\u2026</div>
7137
7733
  </div>
7138
7734
  <div class="panel-block">
7139
7735
  <p class="panel-subtitle">\u5F53\u524D\u98DE\u4E66\u7ED1\u5B9A</p>
7140
7736
  <div class="small" id="feishuBindingMeta">\u6B63\u5728\u52A0\u8F7D\u2026</div>
7141
7737
  <div class="binding-list" id="feishuBindings" style="margin-top: 12px;"></div>
7142
7738
  </div>
7739
+
7143
7740
  <div class="message" id="feishuMessage"></div>
7144
- </section>
7741
+ </section>
7742
+ </div>
7743
+
7744
+ <div class="channel-view" data-channel="weixin" role="tabpanel">
7745
+ <section id="wechat">
7746
+ <div class="panel-header">
7747
+ <div>
7748
+ <h2>\u5FAE\u4FE1</h2>
7749
+ <p>\u626B\u7801\u767B\u5F55\u5FAE\u4FE1\u5E76\u67E5\u770B\u5F53\u524D\u804A\u5929\u7ED1\u5B9A\u7684\u4F1A\u8BDD\u3002</p>
7750
+ </div>
7751
+ <div class="toolbar">
7752
+ <button id="saveWeixinChannelBtn">\u4FDD\u5B58\u901A\u9053\u914D\u7F6E</button>
7753
+ <button id="weixinLoginBtn">\u5F00\u59CB\u5FAE\u4FE1\u626B\u7801</button>
7754
+ </div>
7755
+ </div>
7145
7756
 
7146
- <section class="panel" id="wechat">
7147
- <h2>\u5FAE\u4FE1</h2>
7148
7757
  <div class="fields">
7149
7758
  <div class="checkbox-row">
7150
7759
  <label class="checkbox"><input id="weixinMediaEnabled" type="checkbox" /> \u542F\u7528\u56FE\u7247 / \u6587\u4EF6 / \u89C6\u9891\u5165\u7AD9\u4E0B\u8F7D</label>
7151
7760
  </div>
7152
- <div class="actions">
7153
- <button id="weixinLoginBtn">\u5F00\u59CB\u5FAE\u4FE1\u626B\u7801</button>
7154
- </div>
7761
+ </div>
7762
+ <div class="panel-block">
7763
+ <p class="panel-subtitle">\u901A\u9053\u72B6\u6001</p>
7764
+ <div class="small" id="weixinRuntimeMeta">\u6B63\u5728\u52A0\u8F7D\u2026</div>
7765
+ </div>
7766
+ <div class="panel-block">
7767
+ <p class="panel-subtitle">\u5DF2\u767B\u5F55\u5FAE\u4FE1\u8D26\u53F7</p>
7768
+ <div class="small" id="weixinAccountMeta">\u6B63\u5728\u52A0\u8F7D\u2026</div>
7769
+ <div class="binding-list" id="weixinAccounts" style="margin-top: 12px;"></div>
7155
7770
  </div>
7156
7771
  <div class="panel-block">
7157
7772
  <p class="panel-subtitle">\u5F53\u524D\u5FAE\u4FE1\u7ED1\u5B9A</p>
@@ -7159,23 +7774,41 @@ function renderHtml() {
7159
7774
  <div class="binding-list" id="weixinBindings" style="margin-top: 12px;"></div>
7160
7775
  </div>
7161
7776
  <div class="message" id="weixinMessage"></div>
7162
- </section>
7777
+ </section>
7778
+ </div>
7779
+ </section>
7780
+ </section>
7781
+
7782
+ <section class="page" data-page="logs">
7783
+ <div class="page-header">
7784
+ <div>
7785
+ <h1 class="page-title">\u65E5\u5FD7</h1>
7786
+ <p class="page-copy">\u65E5\u5FD7\u9875\u53EA\u8D1F\u8D23\u67E5\u770B bridge \u65E5\u5FD7\uFF0C\u4FBF\u4E8E\u6392\u67E5\u8FD0\u884C\u548C\u901A\u9053\u95EE\u9898\u3002</p>
7787
+ </div>
7788
+ <div class="toolbar">
7789
+ <button id="refreshLogsBtn">\u5237\u65B0\u65E5\u5FD7</button>
7790
+ </div>
7163
7791
  </div>
7164
7792
 
7165
7793
  <section class="panel" id="logs">
7166
- <h2>\u65E5\u5FD7</h2>
7167
7794
  <div class="logs" id="logsOutput">\u7B49\u5F85\u52A0\u8F7D\u65E5\u5FD7\u2026</div>
7168
7795
  </section>
7169
- </div>
7796
+ </section>
7170
7797
  </main>
7171
7798
  </div>
7799
+ <div id="globalMessageHost" class="global-message-host" aria-live="polite"></div>
7172
7800
 
7173
7801
  <script>
7174
7802
  const state = {
7175
7803
  config: null,
7804
+ bridgeStatus: null,
7176
7805
  desktopSessions: [],
7177
7806
  bindings: [],
7178
7807
  bindingOptions: [],
7808
+ weixinAccounts: [],
7809
+ desktopRoot: '',
7810
+ activePage: 'overview',
7811
+ activeChannel: 'feishu',
7179
7812
  };
7180
7813
 
7181
7814
  function escapeHtml(value) {
@@ -7196,6 +7829,16 @@ function renderHtml() {
7196
7829
  return '/thread ' + value.slice(0, 12);
7197
7830
  }
7198
7831
 
7832
+ function pathSegments(value) {
7833
+ const normalized = String(value || '').split('/').join('\\\\');
7834
+ return normalized.split('\\\\').filter(Boolean);
7835
+ }
7836
+
7837
+ function baseName(value) {
7838
+ const segments = pathSegments(value);
7839
+ return segments[segments.length - 1] || value || '(no cwd)';
7840
+ }
7841
+
7199
7842
  function formatTime(value) {
7200
7843
  if (!value) return '-';
7201
7844
  const date = new Date(value);
@@ -7207,6 +7850,33 @@ function renderHtml() {
7207
7850
  return option.label + ' \xB7 ' + option.description;
7208
7851
  }
7209
7852
 
7853
+ function renderBindingTable(binding) {
7854
+ const sessions = state.desktopSessions || [];
7855
+ if (!sessions.length) {
7856
+ return '<div class="binding-empty">\u5F53\u524D\u8FD8\u6CA1\u6709\u548C\u4F1A\u8BDD\u9875\u4E00\u81F4\u7684\u547D\u540D\u684C\u9762\u7EBF\u7A0B\u3002</div>';
7857
+ }
7858
+
7859
+ return ''
7860
+ + '<div class="binding-table-wrap">'
7861
+ + '<table class="binding-table">'
7862
+ + '<thead><tr><th>\u6807\u9898</th><th>Thread</th><th>\u76EE\u5F55</th><th>\u64CD\u4F5C</th></tr></thead>'
7863
+ + '<tbody>'
7864
+ + sessions.map((session) => {
7865
+ const targetKey = 'desktop:' + session.threadId;
7866
+ const active = targetKey === binding.currentTargetKey;
7867
+ return ''
7868
+ + '<tr class="' + (active ? 'current' : '') + '">'
7869
+ + '<td><div class="binding-table-title">' + escapeHtml(session.title || 'Untitled Session') + (active ? '<span class="binding-table-mark">\u5F53\u524D</span>' : '') + '</div></td>'
7870
+ + '<td><div class="binding-table-thread"><code>' + escapeHtml(session.threadId) + '</code></div></td>'
7871
+ + '<td><div class="binding-table-path">' + escapeHtml(session.cwd || '(no cwd)') + '</div></td>'
7872
+ + '<td><button type="button" class="binding-target-btn' + (active ? ' current' : '') + '" data-action="switch-binding-target" data-binding-id="' + escapeHtml(binding.id) + '" data-target-key="' + escapeHtml(targetKey) + '"' + (active ? ' disabled' : '') + '>' + (active ? '\u5F53\u524D\u4F1A\u8BDD' : '\u5207\u6362\u5230\u5F53\u524D\u4F1A\u8BDD') + '</button></td>'
7873
+ + '</tr>';
7874
+ }).join('')
7875
+ + '</tbody>'
7876
+ + '</table>'
7877
+ + '</div>';
7878
+ }
7879
+
7210
7880
  function enabledChannelsFromForm() {
7211
7881
  const channels = [];
7212
7882
  if (document.getElementById('channelFeishu').checked) channels.push('feishu');
@@ -7235,53 +7905,316 @@ function renderHtml() {
7235
7905
 
7236
7906
  function showMessage(id, type, message) {
7237
7907
  const node = document.getElementById(id);
7908
+ if (!node) return;
7238
7909
  node.className = 'message show ' + type;
7239
7910
  node.textContent = message;
7240
7911
  }
7241
7912
 
7913
+ function showGlobalMessage(type, message) {
7914
+ const host = document.getElementById('globalMessageHost');
7915
+ if (!host) return;
7916
+
7917
+ const item = document.createElement('div');
7918
+ item.className = 'global-message ' + (type || 'success');
7919
+ item.textContent = message;
7920
+ host.appendChild(item);
7921
+
7922
+ window.setTimeout(() => {
7923
+ item.remove();
7924
+ }, 2200);
7925
+ }
7926
+
7927
+ function setActivePage(page, syncHash) {
7928
+ const nextPage = ['overview', 'sessions', 'config', 'commands', 'channels', 'logs'].includes(page) ? page : 'overview';
7929
+ state.activePage = nextPage;
7930
+
7931
+ document.querySelectorAll('.nav-link').forEach((element) => {
7932
+ const node = element;
7933
+ node.classList.toggle('active', node.dataset.page === nextPage);
7934
+ });
7935
+
7936
+ document.querySelectorAll('.page').forEach((element) => {
7937
+ const node = element;
7938
+ node.classList.toggle('active', node.dataset.page === nextPage);
7939
+ });
7940
+
7941
+ if (syncHash !== false) {
7942
+ const hash = nextPage === 'channels'
7943
+ ? '#channels/' + state.activeChannel
7944
+ : '#' + nextPage;
7945
+ if (window.location.hash !== hash) {
7946
+ history.replaceState(null, '', hash);
7947
+ }
7948
+ }
7949
+ }
7950
+
7951
+ function setActiveChannel(channel, syncHash) {
7952
+ const nextChannel = channel === 'weixin' ? 'weixin' : 'feishu';
7953
+ state.activeChannel = nextChannel;
7954
+
7955
+ document.querySelectorAll('.channel-tab').forEach((element) => {
7956
+ const node = element;
7957
+ const active = node.dataset.channel === nextChannel;
7958
+ node.classList.toggle('active', active);
7959
+ node.setAttribute('aria-selected', active ? 'true' : 'false');
7960
+ });
7961
+
7962
+ document.querySelectorAll('.channel-view').forEach((element) => {
7963
+ const node = element;
7964
+ const active = node.dataset.channel === nextChannel;
7965
+ node.classList.toggle('active', active);
7966
+ node.hidden = !active;
7967
+ });
7968
+
7969
+ if (syncHash !== false && state.activePage === 'channels') {
7970
+ const hash = '#channels/' + nextChannel;
7971
+ if (window.location.hash !== hash) {
7972
+ history.replaceState(null, '', hash);
7973
+ }
7974
+ }
7975
+ }
7976
+
7977
+ function syncPageFromHash() {
7978
+ const raw = String(window.location.hash || '').replace(/^#/, '');
7979
+ if (!raw) {
7980
+ setActivePage('overview', false);
7981
+ setActiveChannel('feishu', false);
7982
+ return;
7983
+ }
7984
+
7985
+ if (raw.startsWith('channels/')) {
7986
+ setActivePage('channels', false);
7987
+ setActiveChannel(raw.split('/')[1] || 'feishu', false);
7988
+ return;
7989
+ }
7990
+
7991
+ setActivePage(raw, false);
7992
+ }
7993
+
7242
7994
  async function copyText(value, successMessage) {
7243
7995
  if (!navigator.clipboard || !value) {
7244
7996
  throw new Error('\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u590D\u5236\uFF0C\u6216\u6CA1\u6709\u53EF\u590D\u5236\u7684\u5185\u5BB9\u3002');
7245
7997
  }
7246
7998
  await navigator.clipboard.writeText(value);
7247
- showMessage('desktopMessage', 'success', successMessage);
7999
+ showGlobalMessage('success', successMessage);
8000
+ }
8001
+
8002
+ function groupDesktopSessions(sessions) {
8003
+ const groups = new Map();
8004
+ for (const session of sessions || []) {
8005
+ const key = session.cwd || '(no cwd)';
8006
+ if (!groups.has(key)) {
8007
+ groups.set(key, {
8008
+ key,
8009
+ name: baseName(key),
8010
+ cwd: key,
8011
+ latest: session.lastEventAt || '',
8012
+ sessions: [],
8013
+ });
8014
+ }
8015
+ const group = groups.get(key);
8016
+ group.sessions.push(session);
8017
+ if ((session.lastEventAt || '') > group.latest) {
8018
+ group.latest = session.lastEventAt || '';
8019
+ }
8020
+ }
8021
+
8022
+ return Array.from(groups.values())
8023
+ .map((group) => ({
8024
+ ...group,
8025
+ sessions: group.sessions.sort((left, right) => (right.lastEventAt || '').localeCompare(left.lastEventAt || '')),
8026
+ }))
8027
+ .sort((left, right) => {
8028
+ const timeOrder = (right.latest || '').localeCompare(left.latest || '');
8029
+ if (timeOrder !== 0) return timeOrder;
8030
+ return left.name.localeCompare(right.name, 'zh-CN');
8031
+ });
8032
+ }
8033
+
8034
+ function isChannelEnabled(channelType) {
8035
+ return Boolean(state.config && (state.config.enabledChannels || []).includes(channelType));
8036
+ }
8037
+
8038
+ function runningChannels() {
8039
+ return state.bridgeStatus && Array.isArray(state.bridgeStatus.channels) ? state.bridgeStatus.channels : [];
8040
+ }
8041
+
8042
+ function isChannelRunning(channelType) {
8043
+ return Boolean(state.bridgeStatus && state.bridgeStatus.running && runningChannels().includes(channelType));
8044
+ }
8045
+
8046
+ function bridgeNeedsRestart() {
8047
+ if (!state.config || !state.bridgeStatus || !state.bridgeStatus.running) return false;
8048
+ const configured = (state.config.enabledChannels || []).slice().sort().join(',');
8049
+ const running = runningChannels().slice().sort().join(',');
8050
+ return configured !== running;
8051
+ }
8052
+
8053
+ function channelLabel(channelType) {
8054
+ return channelType === 'weixin' ? '\u5FAE\u4FE1' : '\u98DE\u4E66';
8055
+ }
8056
+
8057
+ function channelRuntimeText(channelType) {
8058
+ const label = channelLabel(channelType);
8059
+ if (!isChannelEnabled(channelType)) {
8060
+ return label + '\u5728\u914D\u7F6E\u4E2D\u672A\u542F\u7528\u3002';
8061
+ }
8062
+ if (!state.bridgeStatus || !state.bridgeStatus.running) {
8063
+ return label + '\u5DF2\u542F\u7528\uFF0C\u4F46 Bridge \u8FD8\u6CA1\u542F\u52A8\u3002\u542F\u52A8 Bridge \u540E\u624D\u4F1A\u771F\u6B63\u63A5\u901A\u3002';
8064
+ }
8065
+ if (!isChannelRunning(channelType)) {
8066
+ return label + '\u5DF2\u5199\u5165\u914D\u7F6E\uFF0C\u4F46\u5F53\u524D\u8FD0\u884C\u4E2D\u7684 Bridge \u8FD8\u6CA1\u52A0\u8F7D\u8FD9\u4E2A\u901A\u9053\u3002\u70B9\u51FB\u201C\u91CD\u542F Bridge\u201D\u540E\u751F\u6548\u3002';
8067
+ }
8068
+ return label + '\u5DF2\u63A5\u901A\u5230\u5F53\u524D\u8FD0\u884C\u4E2D\u7684 Bridge\u3002';
8069
+ }
8070
+
8071
+ function emptyBindingText(channelType) {
8072
+ const label = channelLabel(channelType);
8073
+ if (!isChannelEnabled(channelType)) {
8074
+ return label + '\u672A\u542F\u7528\u3002\u5148\u5728\u201C\u914D\u7F6E\u201D\u91CC\u52FE\u9009\u540E\u4FDD\u5B58\u3002';
8075
+ }
8076
+ if (!state.bridgeStatus || !state.bridgeStatus.running) {
8077
+ return label + '\u5DF2\u542F\u7528\uFF0C\u4F46 Bridge \u8FD8\u6CA1\u542F\u52A8\u3002\u542F\u52A8\u540E\u624D\u4F1A\u521B\u5EFA\u7ED1\u5B9A\u3002';
8078
+ }
8079
+ if (!isChannelRunning(channelType)) {
8080
+ return label + '\u5DF2\u542F\u7528\uFF0C\u4F46\u5F53\u524D\u8FD0\u884C\u4E2D\u7684 Bridge \u8FD8\u6CA1\u52A0\u8F7D\u8FD9\u4E2A\u901A\u9053\u3002\u70B9\u51FB\u201C\u91CD\u542F Bridge\u201D\u540E\u751F\u6548\u3002';
8081
+ }
8082
+ return label + '\u5F53\u524D\u8FD8\u6CA1\u6709\u804A\u5929\u63A5\u5165\u3002\u5148\u4ECE' + label + '\u53D1\u4E00\u6761\u6D88\u606F\uFF0Cbridge \u624D\u4F1A\u521B\u5EFA\u7ED1\u5B9A\u3002';
8083
+ }
8084
+
8085
+ function quickSwitchState(channelType) {
8086
+ const label = channelLabel(channelType);
8087
+ if (!isChannelEnabled(channelType)) {
8088
+ return { disabled: true, title: label + '\u672A\u542F\u7528\u3002' };
8089
+ }
8090
+ if (!state.bridgeStatus || !state.bridgeStatus.running) {
8091
+ return { disabled: true, title: 'Bridge \u8FD8\u6CA1\u542F\u52A8\u3002\u542F\u52A8\u540E\u518D\u5207\u6362' + label + '\u4F1A\u8BDD\u3002' };
8092
+ }
8093
+ if (!isChannelRunning(channelType)) {
8094
+ return { disabled: true, title: label + '\u5DF2\u5199\u5165\u914D\u7F6E\uFF0C\u4F46\u5F53\u524D\u8FD0\u884C\u4E2D\u7684 Bridge \u8FD8\u6CA1\u52A0\u8F7D\u3002\u8BF7\u5148\u91CD\u542F Bridge\u3002' };
8095
+ }
8096
+
8097
+ const bindings = state.bindings.filter((item) => item.channelType === channelType);
8098
+ if (bindings.length === 0) {
8099
+ return { disabled: true, title: '\u5F53\u524D\u8FD8\u6CA1\u6709' + label + '\u804A\u5929\u7ED1\u5B9A\u3002\u5148\u8BA9' + label + '\u53D1\u6765\u4E00\u6761\u6D88\u606F\u3002' };
8100
+ }
8101
+ if (bindings.length > 1) {
8102
+ return { disabled: true, title: label + '\u6709\u591A\u4E2A\u7ED1\u5B9A\uFF0C\u8BF7\u5230\u901A\u9053\u9875\u5207\u6362\u3002' };
8103
+ }
8104
+
8105
+ return {
8106
+ disabled: false,
8107
+ title: '\u5207\u6362' + label + '\u5230\u5F53\u524D\u4F1A\u8BDD',
8108
+ bindingId: bindings[0].id,
8109
+ };
8110
+ }
8111
+
8112
+ function currentThreadMarks(threadId) {
8113
+ const marks = [];
8114
+ const currentTargetKey = 'desktop:' + threadId;
8115
+ const counts = new Map();
8116
+
8117
+ for (const binding of state.bindings || []) {
8118
+ const matchesThread = binding.currentThreadId === threadId || binding.currentTargetKey === currentTargetKey;
8119
+ if (!matchesThread) continue;
8120
+ counts.set(binding.channelType, (counts.get(binding.channelType) || 0) + 1);
8121
+ }
8122
+
8123
+ for (const [channelType, count] of counts.entries()) {
8124
+ const label = channelType === 'weixin' ? '\u5FAE\u4FE1\u5F53\u524D' : '\u98DE\u4E66\u5F53\u524D';
8125
+ marks.push(count > 1 ? label + ' x' + count : label);
8126
+ }
8127
+
8128
+ return marks;
8129
+ }
8130
+
8131
+ function renderDesktopSessionCard(session) {
8132
+ const feishuSwitch = quickSwitchState('feishu');
8133
+ const weixinSwitch = quickSwitchState('weixin');
8134
+ const targetKey = 'desktop:' + session.threadId;
8135
+ const originator = session.originator || 'Codex Desktop';
8136
+ const marks = currentThreadMarks(session.threadId);
8137
+ const markHtml = marks.map((mark) => '<span class="session-mark">' + escapeHtml(mark) + '</span>').join('');
8138
+
8139
+ return ''
8140
+ + '<article class="session-card' + (marks.length ? ' current-thread' : '') + '">'
8141
+ + '<div class="session-head">'
8142
+ + '<div class="session-main">'
8143
+ + '<div class="session-title-row"><div class="session-title">' + escapeHtml(session.title || 'Untitled Session') + '</div>' + markHtml + '</div>'
8144
+ + '<div class="session-thread">Thread: <code>' + escapeHtml(session.threadId) + '</code><button type="button" class="session-inline-action" data-action="copy-thread" data-thread-id="' + escapeHtml(session.threadId) + '">\u590D\u5236</button></div>'
8145
+ + '</div>'
8146
+ + '<div class="session-cell">'
8147
+ + '<div class="session-label">\u6765\u6E90</div>'
8148
+ + '<div class="session-value">' + escapeHtml(originator) + '</div>'
8149
+ + '</div>'
8150
+ + '<div class="session-cell">'
8151
+ + '<div class="session-label">\u76EE\u5F55</div>'
8152
+ + '<div class="session-path">' + escapeHtml(session.cwd || '(no cwd)') + '</div>'
8153
+ + '</div>'
8154
+ + '<div class="session-actions">'
8155
+ + '<button type="button" data-action="bind-channel" data-channel="feishu" data-binding-id="' + escapeHtml(feishuSwitch.bindingId || '') + '" data-target-key="' + escapeHtml(targetKey) + '" title="' + escapeHtml(feishuSwitch.title) + '"' + (feishuSwitch.disabled ? ' disabled' : '') + '>\u98DE\u4E66\u5207\u5230\u6B64\u4F1A\u8BDD</button>'
8156
+ + '<button type="button" data-action="bind-channel" data-channel="weixin" data-binding-id="' + escapeHtml(weixinSwitch.bindingId || '') + '" data-target-key="' + escapeHtml(targetKey) + '" title="' + escapeHtml(weixinSwitch.title) + '"' + (weixinSwitch.disabled ? ' disabled' : '') + '>\u5FAE\u4FE1\u5207\u5230\u6B64\u4F1A\u8BDD</button>'
8157
+ + '<button type="button" data-action="copy-bind-command" data-thread-id="' + escapeHtml(session.threadId) + '">\u590D\u5236\u547D\u4EE4</button>'
8158
+ + '</div>'
8159
+ + '</div>'
8160
+ + '</article>';
7248
8161
  }
7249
8162
 
7250
8163
  function renderDesktopSessions(result) {
7251
8164
  state.desktopSessions = result.sessions || [];
8165
+ state.desktopRoot = result.root || '-';
8166
+ const groups = groupDesktopSessions(state.desktopSessions);
7252
8167
  document.getElementById('desktopSessionCount').textContent = String(state.desktopSessions.length);
7253
8168
  document.getElementById('desktopSessionMeta').textContent =
7254
- '\u626B\u63CF\u76EE\u5F55\uFF1A' + (result.root || '-') + ' \xB7 \u6700\u8FD1 ' + state.desktopSessions.length + ' \u6761\u684C\u9762\u4F1A\u8BDD';
8169
+ '\u626B\u63CF\u76EE\u5F55\uFF1A' + state.desktopRoot + ' \xB7 ' + groups.length + ' \u4E2A\u5DE5\u7A0B \xB7 ' + state.desktopSessions.length + ' \u6761\u684C\u9762\u4F1A\u8BDD';
8170
+ document.getElementById('desktopRootStatus').textContent = state.desktopRoot;
7255
8171
 
7256
8172
  const list = document.getElementById('desktopSessionsList');
7257
8173
  if (state.desktopSessions.length === 0) {
7258
8174
  list.innerHTML = '<div class="notice ghost">\u5F53\u524D\u6CA1\u6709\u53D1\u73B0\u684C\u9762\u7AEF\u4F1A\u8BDD\u3002\u5148\u5728 Codex Windows App \u4E2D\u6253\u5F00\u6216\u8FD0\u884C\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u518D\u56DE\u5230\u8FD9\u91CC\u5237\u65B0\u3002</div>';
8175
+ rerenderBindingPanels();
7259
8176
  return;
7260
8177
  }
7261
8178
 
7262
- list.innerHTML = state.desktopSessions.map((session) => {
7263
- const tags = [
7264
- '<span class="session-pill ' + (session.activeEstimate ? 'active' : '') + '">' + (session.activeEstimate ? '\u6700\u8FD1\u6D3B\u8DC3' : '\u5386\u53F2\u4F1A\u8BDD') + '</span>',
7265
- '<span class="session-pill">' + escapeHtml(session.originator || 'Codex Desktop') + '</span>',
7266
- session.source ? '<span class="session-pill">' + escapeHtml(session.source) + '</span>' : '',
7267
- ].filter(Boolean).join('');
8179
+ list.innerHTML = groups.map((group) => ''
8180
+ + '<section class="project-group">'
8181
+ + '<div class="project-group-head">'
8182
+ + '<div>'
8183
+ + '<div class="project-group-title">' + escapeHtml(group.name) + '</div>'
8184
+ + '<div class="project-group-path">' + escapeHtml(group.cwd || '(no cwd)') + '</div>'
8185
+ + '</div>'
8186
+ + '<div class="project-group-count">' + group.sessions.length + ' \u4E2A\u7EBF\u7A0B</div>'
8187
+ + '</div>'
8188
+ + '<div class="project-session-list">'
8189
+ + group.sessions.map((session) => renderDesktopSessionCard(session)).join('')
8190
+ + '</div>'
8191
+ + '</section>'
8192
+ ).join('');
8193
+
8194
+ rerenderBindingPanels();
8195
+ }
8196
+
8197
+ function rerenderDesktopSessions() {
8198
+ if (!state.desktopSessions.length && !state.desktopRoot) return;
8199
+ renderDesktopSessions({
8200
+ root: state.desktopRoot,
8201
+ sessions: state.desktopSessions,
8202
+ });
8203
+ }
7268
8204
 
7269
- return ''
7270
- + '<article class="session-card">'
7271
- + '<div class="session-head">'
7272
- + '<div class="session-title">' + escapeHtml(session.title || 'Untitled Session') + '</div>'
7273
- + '<div class="small">' + escapeHtml(formatTime(session.lastEventAt)) + '</div>'
7274
- + '</div>'
7275
- + '<div class="small">Thread: <code>' + escapeHtml(shortId(session.threadId)) + '</code></div>'
7276
- + '<div class="session-meta">' + tags + '</div>'
7277
- + '<div class="session-path">' + escapeHtml(session.cwd || '(no cwd)') + '</div>'
7278
- + '<div class="session-actions">'
7279
- + '<button type="button" data-action="copy-thread" data-thread-id="' + escapeHtml(session.threadId) + '">\u590D\u5236 Thread ID</button>'
7280
- + '<button type="button" data-action="copy-bind-command" data-thread-id="' + escapeHtml(session.threadId) + '">\u590D\u5236\u98DE\u4E66\u63A5\u7BA1\u547D\u4EE4</button>'
7281
- + '<button type="button" data-action="copy-cwd" data-cwd="' + escapeHtml(session.cwd || '') + '">\u590D\u5236\u5DE5\u4F5C\u76EE\u5F55</button>'
7282
- + '</div>'
7283
- + '</article>';
7284
- }).join('');
8205
+ function rerenderBindingPanels() {
8206
+ renderChannelBindings(
8207
+ 'feishu',
8208
+ 'feishuBindings',
8209
+ 'feishuBindingMeta',
8210
+ emptyBindingText('feishu')
8211
+ );
8212
+ renderChannelBindings(
8213
+ 'weixin',
8214
+ 'weixinBindings',
8215
+ 'weixinBindingMeta',
8216
+ emptyBindingText('weixin')
8217
+ );
7285
8218
  }
7286
8219
 
7287
8220
  function renderChannelBindings(channelType, listId, metaId, emptyText) {
@@ -7289,7 +8222,7 @@ function renderHtml() {
7289
8222
  const list = document.getElementById(listId);
7290
8223
  const meta = document.getElementById(metaId);
7291
8224
  meta.textContent = bindings.length > 0
7292
- ? '\u5F53\u524D\u5DF2\u53D1\u73B0 ' + bindings.length + ' \u4E2A\u804A\u5929\u7ED1\u5B9A\uFF0C\u53EF\u4EE5\u76F4\u63A5\u5728\u7F51\u9875\u5207\u6362\u76EE\u6807\u4F1A\u8BDD\u3002'
8225
+ ? '\u5F53\u524D\u5DF2\u53D1\u73B0 ' + bindings.length + ' \u4E2A\u804A\u5929\u7ED1\u5B9A\u3002\u8FD9\u91CC\u53EA\u663E\u793A\u548C\u4F1A\u8BDD\u9875\u4E00\u81F4\u7684\u547D\u540D\u684C\u9762\u7EBF\u7A0B\u3002'
7293
8226
  : emptyText;
7294
8227
 
7295
8228
  if (bindings.length === 0) {
@@ -7298,11 +8231,6 @@ function renderHtml() {
7298
8231
  }
7299
8232
 
7300
8233
  list.innerHTML = bindings.map((binding) => {
7301
- const options = state.bindingOptions.map((option) => {
7302
- const selected = option.key === binding.currentTargetKey ? ' selected' : '';
7303
- return '<option value="' + escapeHtml(option.key) + '"' + selected + '>' + escapeHtml(optionLabel(option)) + '</option>';
7304
- }).join('');
7305
-
7306
8234
  return ''
7307
8235
  + '<article class="binding-item" data-binding-id="' + escapeHtml(binding.id) + '">'
7308
8236
  + '<div class="binding-head">'
@@ -7310,16 +8238,40 @@ function renderHtml() {
7310
8238
  + '<div class="small">' + escapeHtml(binding.mode) + '</div>'
7311
8239
  + '</div>'
7312
8240
  + '<div class="binding-detail">\u5F53\u524D\u4F1A\u8BDD\uFF1A<code>' + escapeHtml(binding.currentSessionId.slice(0, 8)) + '...</code> \xB7 ' + escapeHtml(binding.currentSessionName) + '</div>'
8241
+ + '<div class="binding-detail">\u5F53\u524D\u76EE\u6807\uFF1A' + escapeHtml(binding.currentTargetLabel || '\u672A\u7ED1\u5B9A') + '</div>'
7313
8242
  + '<div class="binding-detail">\u5F53\u524D thread\uFF1A<code>' + escapeHtml(binding.currentThreadId || 'not-shared') + '</code></div>'
7314
8243
  + '<div class="binding-detail">\u76EE\u5F55\uFF1A' + escapeHtml(binding.workingDirectory || '~') + '</div>'
7315
- + '<div class="binding-controls">'
7316
- + '<select data-role="target">' + options + '</select>'
7317
- + '<button type="button" data-action="save-binding">\u5207\u6362\u7ED1\u5B9A</button>'
7318
- + '</div>'
8244
+ + renderBindingTable(binding)
7319
8245
  + '</article>';
7320
8246
  }).join('');
7321
8247
  }
7322
8248
 
8249
+ function renderWeixinAccounts() {
8250
+ const meta = document.getElementById('weixinAccountMeta');
8251
+ const list = document.getElementById('weixinAccounts');
8252
+ const accounts = state.weixinAccounts || [];
8253
+
8254
+ if (accounts.length === 0) {
8255
+ meta.textContent = '\u5F53\u524D\u8FD8\u6CA1\u6709\u5DF2\u4FDD\u5B58\u7684\u5FAE\u4FE1\u8D26\u53F7\u3002\u5148\u70B9\u51FB\u201C\u5F00\u59CB\u5FAE\u4FE1\u626B\u7801\u201D\uFF0C\u7136\u540E\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u3002';
8256
+ list.innerHTML = '<div class="binding-empty">\u626B\u7801\u6210\u529F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u5F53\u524D\u5DF2\u4FDD\u5B58\u7684\u5FAE\u4FE1\u8D26\u53F7\u3002</div>';
8257
+ return;
8258
+ }
8259
+
8260
+ meta.textContent = '\u5F53\u524D\u5DF2\u4FDD\u5B58 ' + accounts.length + ' \u4E2A\u5FAE\u4FE1\u8D26\u53F7\u3002\u5FAE\u4FE1\u6865\u63A5\u662F\u5355\u8D26\u53F7\u6A21\u5F0F\uFF0C\u6700\u65B0\u542F\u7528\u7684\u8D26\u53F7\u4F1A\u751F\u6548\u3002';
8261
+ list.innerHTML = accounts.map((account) => ''
8262
+ + '<article class="binding-item">'
8263
+ + '<div class="binding-head">'
8264
+ + '<div class="binding-title">' + escapeHtml(account.name || account.accountId) + '</div>'
8265
+ + '<div class="small">' + (account.enabled ? '\u5DF2\u542F\u7528' : '\u5DF2\u505C\u7528') + '</div>'
8266
+ + '</div>'
8267
+ + '<div class="binding-detail">\u8D26\u53F7 ID\uFF1A<code>' + escapeHtml(account.accountId) + '</code></div>'
8268
+ + '<div class="binding-detail">\u7528\u6237 ID\uFF1A<code>' + escapeHtml(account.userId || '-') + '</code></div>'
8269
+ + '<div class="binding-detail">Base URL\uFF1A' + escapeHtml(account.baseUrl || '-') + '</div>'
8270
+ + '<div class="binding-detail">\u6700\u8FD1\u767B\u5F55\uFF1A' + escapeHtml(formatTime(account.lastLoginAt || account.updatedAt)) + '</div>'
8271
+ + '</article>'
8272
+ ).join('');
8273
+ }
8274
+
7323
8275
  function renderBindings(result) {
7324
8276
  state.bindings = result.bindings || [];
7325
8277
  state.bindingOptions = result.options || [];
@@ -7328,14 +8280,15 @@ function renderHtml() {
7328
8280
  'feishu',
7329
8281
  'feishuBindings',
7330
8282
  'feishuBindingMeta',
7331
- '\u5F53\u524D\u8FD8\u6CA1\u6709\u98DE\u4E66\u804A\u5929\u63A5\u5165\u3002\u5148\u5728\u98DE\u4E66\u673A\u5668\u4EBA\u91CC\u53D1\u4E00\u6761\u6D88\u606F\uFF0Cbridge \u624D\u4F1A\u521B\u5EFA\u7ED1\u5B9A\u3002'
8283
+ emptyBindingText('feishu')
7332
8284
  );
7333
8285
  renderChannelBindings(
7334
8286
  'weixin',
7335
8287
  'weixinBindings',
7336
8288
  'weixinBindingMeta',
7337
- '\u5F53\u524D\u8FD8\u6CA1\u6709\u5FAE\u4FE1\u804A\u5929\u63A5\u5165\u3002\u5148\u8BA9\u5FAE\u4FE1\u8D26\u53F7\u53D1\u4E00\u6761\u6D88\u606F\uFF0Cbridge \u624D\u4F1A\u521B\u5EFA\u7ED1\u5B9A\u3002'
8289
+ emptyBindingText('weixin')
7338
8290
  );
8291
+ rerenderDesktopSessions();
7339
8292
  }
7340
8293
 
7341
8294
  function fillForm(config) {
@@ -7355,6 +8308,7 @@ function renderHtml() {
7355
8308
  document.getElementById('feishuAllowedUsers').value = config.feishuAllowedUsers || '';
7356
8309
  document.getElementById('feishuStreamingEnabled').checked = config.feishuStreamingEnabled !== false;
7357
8310
  document.getElementById('weixinMediaEnabled').checked = config.weixinMediaEnabled === true;
8311
+ rerenderDesktopSessions();
7358
8312
  }
7359
8313
 
7360
8314
  async function api(path, options) {
@@ -7372,12 +8326,23 @@ function renderHtml() {
7372
8326
  async function loadStatus() {
7373
8327
  const status = await api('/api/status');
7374
8328
  const config = await api('/api/config');
8329
+ state.bridgeStatus = status.bridge || null;
8330
+ state.weixinAccounts = status.weixin && Array.isArray(status.weixin.linkedAccounts) ? status.weixin.linkedAccounts : [];
7375
8331
  fillForm(config);
7376
- document.getElementById('bridgeStatus').textContent = status.bridge.running ? 'Running' : 'Stopped';
8332
+ const runningChannelText = runningChannels().length ? ' \xB7 ' + runningChannels().join(', ') : '';
8333
+ document.getElementById('bridgeStatus').textContent = status.bridge.running ? 'Running' + runningChannelText : 'Stopped';
7377
8334
  document.getElementById('integrationStatus').textContent = status.codexIntegrationInstalled ? '\u5DF2\u5B89\u88C5' : '\u672A\u5B89\u88C5';
7378
8335
  document.getElementById('runtimeStatus').textContent = config.runtime || 'codex';
7379
8336
  document.getElementById('homeStatus').textContent = status.home;
8337
+ document.getElementById('overviewHomeStatus').textContent = status.home;
7380
8338
  document.getElementById('packageRoot').textContent = status.packageRoot;
8339
+ document.getElementById('feishuRuntimeMeta').textContent = channelRuntimeText('feishu');
8340
+ document.getElementById('weixinRuntimeMeta').textContent = channelRuntimeText('weixin');
8341
+ renderWeixinAccounts();
8342
+ renderBindings({
8343
+ bindings: state.bindings,
8344
+ options: state.bindingOptions,
8345
+ });
7381
8346
  }
7382
8347
 
7383
8348
  async function loadLogs() {
@@ -7386,7 +8351,7 @@ function renderHtml() {
7386
8351
  }
7387
8352
 
7388
8353
  async function loadDesktopSessions() {
7389
- const result = await api('/api/desktop-sessions?limit=10');
8354
+ const result = await api('/api/desktop-sessions?limit=36');
7390
8355
  renderDesktopSessions(result);
7391
8356
  }
7392
8357
 
@@ -7401,14 +8366,53 @@ function renderHtml() {
7401
8366
  body: JSON.stringify(formPayload()),
7402
8367
  });
7403
8368
  fillForm(saved.config);
7404
- showMessage('feishuMessage', 'success', '\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002');
8369
+ showMessage(
8370
+ 'configMessage',
8371
+ 'success',
8372
+ bridgeNeedsRestart()
8373
+ ? '\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u5F53\u524D Bridge \u8FD8\u5728\u4F7F\u7528\u65E7\u901A\u9053\u914D\u7F6E\uFF0C\u70B9\u51FB\u201C\u91CD\u542F Bridge\u201D\u540E\u751F\u6548\u3002'
8374
+ : '\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002'
8375
+ );
7405
8376
  return saved;
7406
8377
  }
7407
8378
 
8379
+ document.querySelectorAll('.nav-link').forEach((element) => {
8380
+ element.addEventListener('click', () => {
8381
+ setActivePage(element.dataset.page || 'overview', true);
8382
+ });
8383
+ });
8384
+
8385
+ document.querySelectorAll('.channel-tab').forEach((element) => {
8386
+ element.addEventListener('click', () => {
8387
+ setActivePage('channels', false);
8388
+ setActiveChannel(element.dataset.channel || 'feishu', true);
8389
+ });
8390
+ });
8391
+
8392
+ window.addEventListener('hashchange', syncPageFromHash);
8393
+
7408
8394
  document.getElementById('saveConfigBtn').addEventListener('click', async () => {
7409
8395
  try {
7410
8396
  await saveConfig();
7411
8397
  await loadStatus();
8398
+ await loadBindings();
8399
+ } catch (error) {
8400
+ showMessage('configMessage', 'error', error.message);
8401
+ }
8402
+ });
8403
+
8404
+ document.getElementById('saveFeishuChannelBtn').addEventListener('click', async () => {
8405
+ try {
8406
+ await saveConfig();
8407
+ await loadStatus();
8408
+ await loadBindings();
8409
+ showMessage(
8410
+ 'feishuMessage',
8411
+ 'success',
8412
+ bridgeNeedsRestart()
8413
+ ? '\u98DE\u4E66\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u5F53\u524D Bridge \u8FD8\u5728\u4F7F\u7528\u65E7\u901A\u9053\u914D\u7F6E\uFF0C\u70B9\u51FB\u201C\u91CD\u542F Bridge\u201D\u540E\u751F\u6548\u3002'
8414
+ : '\u98DE\u4E66\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002'
8415
+ );
7412
8416
  } catch (error) {
7413
8417
  showMessage('feishuMessage', 'error', error.message);
7414
8418
  }
@@ -7417,6 +8421,8 @@ function renderHtml() {
7417
8421
  document.getElementById('testFeishuBtn').addEventListener('click', async () => {
7418
8422
  try {
7419
8423
  await saveConfig();
8424
+ await loadStatus();
8425
+ await loadBindings();
7420
8426
  const result = await api('/api/test/feishu', { method: 'POST' });
7421
8427
  showMessage('feishuMessage', result.ok ? 'success' : 'error', result.message);
7422
8428
  } catch (error) {
@@ -7424,12 +8430,34 @@ function renderHtml() {
7424
8430
  }
7425
8431
  });
7426
8432
 
8433
+ document.getElementById('saveWeixinChannelBtn').addEventListener('click', async () => {
8434
+ try {
8435
+ await saveConfig();
8436
+ await loadStatus();
8437
+ await loadBindings();
8438
+ showMessage(
8439
+ 'weixinMessage',
8440
+ 'success',
8441
+ bridgeNeedsRestart()
8442
+ ? '\u5FAE\u4FE1\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u5F53\u524D Bridge \u8FD8\u5728\u4F7F\u7528\u65E7\u901A\u9053\u914D\u7F6E\uFF0C\u70B9\u51FB\u201C\u91CD\u542F Bridge\u201D\u540E\u751F\u6548\u3002'
8443
+ : '\u5FAE\u4FE1\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002'
8444
+ );
8445
+ } catch (error) {
8446
+ showMessage('weixinMessage', 'error', error.message);
8447
+ }
8448
+ });
8449
+
7427
8450
  document.getElementById('weixinLoginBtn').addEventListener('click', async () => {
7428
8451
  try {
7429
8452
  await saveConfig();
7430
8453
  showMessage('weixinMessage', 'success', '\u5FAE\u4FE1\u626B\u7801\u6D41\u7A0B\u5DF2\u542F\u52A8\uFF0C\u6D4F\u89C8\u5668\u4F1A\u6253\u5F00\u4E8C\u7EF4\u7801\u9875\u9762\u3002');
7431
8454
  const result = await api('/api/test/weixin', { method: 'POST' });
7432
- showMessage('weixinMessage', result.ok ? 'success' : 'error', result.message);
8455
+ await loadStatus();
8456
+ await loadBindings();
8457
+ const followup = isChannelRunning('weixin')
8458
+ ? '\u5FAE\u4FE1\u8D26\u53F7\u5DF2\u4FDD\u5B58\u3002\u5F53\u524D Bridge \u5DF2\u52A0\u8F7D\u5FAE\u4FE1\u901A\u9053\uFF0C\u51E0\u79D2\u540E\u4F1A\u81EA\u52A8\u63A5\u5165\u65B0\u8D26\u53F7\u3002'
8459
+ : '\u5FAE\u4FE1\u8D26\u53F7\u5DF2\u4FDD\u5B58\u3002\u5F53\u524D\u8FD0\u884C\u4E2D\u7684 Bridge \u8FD8\u6CA1\u52A0\u8F7D\u5FAE\u4FE1\u901A\u9053\uFF0C\u70B9\u51FB\u201C\u91CD\u542F Bridge\u201D\u540E\u751F\u6548\u3002';
8460
+ showMessage('weixinMessage', result.ok ? 'success' : 'error', followup);
7433
8461
  } catch (error) {
7434
8462
  showMessage('weixinMessage', 'error', error.message);
7435
8463
  }
@@ -7460,6 +8488,20 @@ function renderHtml() {
7460
8488
  }
7461
8489
  });
7462
8490
 
8491
+ document.getElementById('restartBridgeBtn').addEventListener('click', async () => {
8492
+ try {
8493
+ await saveConfig();
8494
+ const result = await api('/api/bridge/restart', { method: 'POST' });
8495
+ showMessage('opsMessage', 'success', 'Bridge \u5DF2\u91CD\u542F\u3002PID: ' + (result.status.pid || '-'));
8496
+ await loadStatus();
8497
+ await loadBindings();
8498
+ await loadLogs();
8499
+ } catch (error) {
8500
+ showMessage('opsMessage', 'error', error.message);
8501
+ await loadLogs();
8502
+ }
8503
+ });
8504
+
7463
8505
  document.getElementById('installIntegrationBtn').addEventListener('click', async () => {
7464
8506
  try {
7465
8507
  const result = await api('/api/install-codex-integration', { method: 'POST' });
@@ -7492,12 +8534,20 @@ function renderHtml() {
7492
8534
  }
7493
8535
  });
7494
8536
 
8537
+ document.getElementById('refreshLogsBtn').addEventListener('click', async () => {
8538
+ try {
8539
+ await loadLogs();
8540
+ } catch (error) {
8541
+ showMessage('opsMessage', 'error', error.message);
8542
+ }
8543
+ });
8544
+
7495
8545
  document.getElementById('refreshDesktopBtn').addEventListener('click', async () => {
7496
8546
  try {
7497
8547
  await loadDesktopSessions();
7498
8548
  showMessage('desktopMessage', 'success', '\u684C\u9762\u4F1A\u8BDD\u5217\u8868\u5DF2\u5237\u65B0\u3002');
7499
8549
  } catch (error) {
7500
- showMessage('desktopMessage', 'error', error.message);
8550
+ showGlobalMessage('error', error.message);
7501
8551
  }
7502
8552
  });
7503
8553
 
@@ -7507,6 +8557,28 @@ function renderHtml() {
7507
8557
  if (!target) return;
7508
8558
 
7509
8559
  try {
8560
+ if (target.dataset.action === 'bind-channel') {
8561
+ const bindingId = target.dataset.bindingId || '';
8562
+ const targetKey = target.dataset.targetKey || '';
8563
+ const channelType = target.dataset.channel || '';
8564
+ if (!bindingId || !targetKey) {
8565
+ throw new Error('\u5F53\u524D\u901A\u9053\u6CA1\u6709\u53EF\u5207\u6362\u7684\u7ED1\u5B9A\uFF0C\u8BF7\u5148\u5728\u901A\u9053\u9875\u5B8C\u6210\u63A5\u5165\u3002');
8566
+ }
8567
+ const result = await api('/api/bindings/update', {
8568
+ method: 'POST',
8569
+ body: JSON.stringify({
8570
+ bindingId,
8571
+ targetKey,
8572
+ }),
8573
+ });
8574
+ renderBindings(result);
8575
+ showMessage(
8576
+ 'desktopMessage',
8577
+ 'success',
8578
+ (channelType === 'weixin' ? '\u5FAE\u4FE1' : '\u98DE\u4E66') + '\u5DF2\u5207\u6362\u5230\u5F53\u524D\u4F1A\u8BDD\u3002'
8579
+ );
8580
+ return;
8581
+ }
7510
8582
  if (target.dataset.action === 'copy-thread') {
7511
8583
  await copyText(target.dataset.threadId || '', 'Thread ID \u5DF2\u590D\u5236\u3002');
7512
8584
  return;
@@ -7525,20 +8597,15 @@ function renderHtml() {
7525
8597
 
7526
8598
  async function handleBindingAction(event, channelType, messageId) {
7527
8599
  const source = event.target instanceof Element ? event.target : null;
7528
- const target = source ? source.closest('button[data-action="save-binding"]') : null;
8600
+ const target = source ? source.closest('button[data-action="switch-binding-target"]') : null;
7529
8601
  if (!target) return;
7530
8602
 
7531
- const item = target.closest('[data-binding-id]');
7532
- if (!item) return;
7533
- const select = item.querySelector('select[data-role="target"]');
7534
- if (!select) return;
7535
-
7536
8603
  try {
7537
8604
  const result = await api('/api/bindings/update', {
7538
8605
  method: 'POST',
7539
8606
  body: JSON.stringify({
7540
- bindingId: item.dataset.bindingId,
7541
- targetKey: select.value,
8607
+ bindingId: target.dataset.bindingId,
8608
+ targetKey: target.dataset.targetKey,
7542
8609
  }),
7543
8610
  });
7544
8611
  renderBindings(result);
@@ -7556,6 +8623,8 @@ function renderHtml() {
7556
8623
  handleBindingAction(event, 'weixin', 'weixinMessage');
7557
8624
  });
7558
8625
 
8626
+ syncPageFromHash();
8627
+
7559
8628
  Promise.all([loadStatus(), loadBindings(), loadDesktopSessions(), loadLogs()]).catch((error) => {
7560
8629
  showMessage('opsMessage', 'error', error.message);
7561
8630
  });
@@ -7581,6 +8650,9 @@ var server = http.createServer(async (request, response) => {
7581
8650
  home: CTI_HOME,
7582
8651
  packageRoot: getPackageRoot(),
7583
8652
  codexIntegrationInstalled: isCodexIntegrationInstalled(),
8653
+ weixin: {
8654
+ linkedAccounts: getWeixinAccountsPayload()
8655
+ },
7584
8656
  startedAt: serverStartTime
7585
8657
  });
7586
8658
  return;
@@ -7645,6 +8717,11 @@ var server = http.createServer(async (request, response) => {
7645
8717
  json(response, 200, { ok: true, status });
7646
8718
  return;
7647
8719
  }
8720
+ if (request.method === "POST" && url.pathname === "/api/bridge/restart") {
8721
+ const status = await restartBridge();
8722
+ json(response, 200, { ok: true, status });
8723
+ return;
8724
+ }
7648
8725
  if (request.method === "POST" && url.pathname === "/api/bindings/update") {
7649
8726
  const payload = await readJsonBody(request);
7650
8727
  const bindingId = asString(payload.bindingId);