codex-to-im 1.0.29 → 1.0.30

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.
@@ -4566,7 +4566,7 @@ var require_lib = __commonJS({
4566
4566
 
4567
4567
  // src/ui-server.ts
4568
4568
  import http from "node:http";
4569
- import crypto4 from "node:crypto";
4569
+ import crypto5 from "node:crypto";
4570
4570
  import net from "node:net";
4571
4571
  import os5 from "node:os";
4572
4572
 
@@ -5762,6 +5762,27 @@ function buildDaemonEnv() {
5762
5762
  delete env.CLAUDECODE;
5763
5763
  return env;
5764
5764
  }
5765
+ function describeBridgeStartupPreflightFailure(channels) {
5766
+ const configured = Array.isArray(channels) ? channels : [];
5767
+ if (configured.length === 0) {
5768
+ return "\u672A\u914D\u7F6E\u4EFB\u4F55\u901A\u9053\u5B9E\u4F8B\u3002\u8BF7\u5148\u5728 Web \u63A7\u5236\u53F0\u521B\u5EFA\u5E76\u4FDD\u5B58\u81F3\u5C11\u4E00\u4E2A\u98DE\u4E66\u6216\u5FAE\u4FE1\u901A\u9053\uFF0C\u7136\u540E\u518D\u542F\u52A8\u6865\u63A5\u670D\u52A1\u3002";
5769
+ }
5770
+ const enabled = configured.filter((channel) => channel.enabled !== false);
5771
+ if (enabled.length === 0) {
5772
+ return "\u5F53\u524D\u6240\u6709\u901A\u9053\u5B9E\u4F8B\u90FD\u5DF2\u7981\u7528\u3002\u8BF7\u5148\u542F\u7528\u81F3\u5C11\u4E00\u4E2A\u901A\u9053\u5B9E\u4F8B\uFF0C\u7136\u540E\u518D\u542F\u52A8\u6865\u63A5\u670D\u52A1\u3002";
5773
+ }
5774
+ return null;
5775
+ }
5776
+ function describeBridgeActivationFailure(status, channels) {
5777
+ const statusReason = status.lastExitReason?.trim();
5778
+ if (statusReason) return statusReason;
5779
+ const preflightFailure = describeBridgeStartupPreflightFailure(channels);
5780
+ if (preflightFailure) return preflightFailure;
5781
+ const enabled = (channels || []).filter((channel) => channel.enabled !== false);
5782
+ if (enabled.length === 0) return null;
5783
+ const labels = enabled.map((channel) => channel.alias?.trim() || channel.id).join("\u3001");
5784
+ return `\u6CA1\u6709\u4EFB\u4F55\u901A\u9053\u9002\u914D\u5668\u542F\u52A8\u6210\u529F\u3002\u8BF7\u68C0\u67E5\u901A\u9053\u914D\u7F6E\u3001\u51ED\u636E\u548C\u65E5\u5FD7\u3002\u5F53\u524D\u5DF2\u542F\u7528\u901A\u9053\uFF1A${labels}`;
5785
+ }
5765
5786
  async function waitForBridgeRunning(timeoutMs = 2e4) {
5766
5787
  const startedAt = Date.now();
5767
5788
  while (Date.now() - startedAt < timeoutMs) {
@@ -5779,6 +5800,11 @@ async function startBridge() {
5779
5800
  if (current.running && extraAlivePids.length > 0) {
5780
5801
  await stopBridge();
5781
5802
  }
5803
+ const config = loadConfig();
5804
+ const preflightFailure = describeBridgeStartupPreflightFailure(config.channels);
5805
+ if (preflightFailure) {
5806
+ throw new Error(preflightFailure);
5807
+ }
5782
5808
  const daemonEntry = path4.join(packageRoot, "dist", "daemon.mjs");
5783
5809
  if (!fs3.existsSync(daemonEntry)) {
5784
5810
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
@@ -5795,7 +5821,9 @@ async function startBridge() {
5795
5821
  child.unref();
5796
5822
  const status = await waitForBridgeRunning();
5797
5823
  if (!status.running) {
5798
- throw new Error(status.lastExitReason || "Bridge failed to report running=true.");
5824
+ throw new Error(
5825
+ describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
5826
+ );
5799
5827
  }
5800
5828
  return status;
5801
5829
  }
@@ -6464,6 +6492,7 @@ var JsonFileStore = class {
6464
6492
  var import_qrcode = __toESM(require_lib(), 1);
6465
6493
  import fs6 from "node:fs";
6466
6494
  import path7 from "node:path";
6495
+ import crypto4 from "node:crypto";
6467
6496
  import { spawn as spawn2 } from "node:child_process";
6468
6497
 
6469
6498
  // src/adapters/weixin/weixin-api.ts
@@ -6545,11 +6574,8 @@ function now2() {
6545
6574
  function getAccountRecency(account) {
6546
6575
  return account.lastLoginAt ?? account.updatedAt ?? account.createdAt;
6547
6576
  }
6548
- function normalizeAccounts(accounts) {
6549
- if (accounts.length <= 1) {
6550
- return { accounts, removedAccountIds: [] };
6551
- }
6552
- const sorted = [...accounts].sort((a, b) => {
6577
+ function sortAccountsByRecency(accounts) {
6578
+ return [...accounts].sort((a, b) => {
6553
6579
  const recencyDiff = getAccountRecency(b).localeCompare(getAccountRecency(a));
6554
6580
  if (recencyDiff !== 0) return recencyDiff;
6555
6581
  const updatedDiff = b.updatedAt.localeCompare(a.updatedAt);
@@ -6558,25 +6584,20 @@ function normalizeAccounts(accounts) {
6558
6584
  if (createdDiff !== 0) return createdDiff;
6559
6585
  return 0;
6560
6586
  });
6561
- const kept = sorted[0];
6562
- const removedAccountIds = [
6563
- ...new Set(
6564
- sorted.slice(1).map((account) => account.accountId).filter((accountId) => accountId !== kept.accountId)
6565
- )
6566
- ];
6567
- return {
6568
- accounts: [kept],
6569
- removedAccountIds
6570
- };
6587
+ }
6588
+ function normalizeAccounts(accounts) {
6589
+ const deduped = /* @__PURE__ */ new Map();
6590
+ for (const account of sortAccountsByRecency(accounts)) {
6591
+ if (!account?.accountId || deduped.has(account.accountId)) continue;
6592
+ deduped.set(account.accountId, account);
6593
+ }
6594
+ return Array.from(deduped.values());
6571
6595
  }
6572
6596
  function persistAccounts(accounts) {
6573
6597
  ensureDir2(DATA_DIR2);
6574
6598
  const normalized = normalizeAccounts(accounts);
6575
- atomicWrite2(ACCOUNTS_PATH, JSON.stringify(normalized.accounts, null, 2));
6576
- for (const accountId of normalized.removedAccountIds) {
6577
- deleteWeixinContextTokensByAccount(accountId);
6578
- }
6579
- return normalized.accounts;
6599
+ atomicWrite2(ACCOUNTS_PATH, JSON.stringify(normalized, null, 2));
6600
+ return normalized;
6580
6601
  }
6581
6602
  function readStoredAccounts() {
6582
6603
  ensureDir2(DATA_DIR2);
@@ -6584,21 +6605,13 @@ function readStoredAccounts() {
6584
6605
  return Array.isArray(raw) ? raw : [];
6585
6606
  }
6586
6607
  function readAccounts() {
6587
- return normalizeAccounts(readStoredAccounts()).accounts;
6608
+ return normalizeAccounts(readStoredAccounts());
6588
6609
  }
6589
6610
  function writeAccounts(accounts) {
6590
6611
  persistAccounts(accounts);
6591
6612
  }
6592
- function readContextTokens() {
6593
- ensureDir2(DATA_DIR2);
6594
- return readJson2(CONTEXT_TOKENS_PATH, {});
6595
- }
6596
- function writeContextTokens(tokens) {
6597
- ensureDir2(DATA_DIR2);
6598
- atomicWrite2(CONTEXT_TOKENS_PATH, JSON.stringify(tokens, null, 2));
6599
- }
6600
6613
  function listWeixinAccounts() {
6601
- return readAccounts().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
6614
+ return sortAccountsByRecency(readAccounts());
6602
6615
  }
6603
6616
  function upsertWeixinAccount(params) {
6604
6617
  const accounts = readAccounts();
@@ -6633,20 +6646,15 @@ function upsertWeixinAccount(params) {
6633
6646
  writeAccounts(nextAccounts);
6634
6647
  return account;
6635
6648
  }
6636
- function deleteWeixinContextTokensByAccount(accountId) {
6637
- const tokens = readContextTokens();
6638
- const nextTokens = Object.fromEntries(
6639
- Object.entries(tokens).filter(([key]) => !key.startsWith(`${accountId}::`))
6640
- );
6641
- writeContextTokens(nextTokens);
6642
- }
6643
6649
 
6644
6650
  // src/weixin-login.ts
6645
6651
  var MAX_REFRESHES = 3;
6646
6652
  var QR_TTL_MS = 5 * 6e4;
6647
6653
  var POLL_INTERVAL_MS = 3e3;
6654
+ var WEB_SESSION_TTL_MS = 15 * 6e4;
6648
6655
  var RUNTIME_DIR = path7.join(CTI_HOME, "runtime");
6649
6656
  var HTML_PATH = path7.join(RUNTIME_DIR, "weixin-login.html");
6657
+ var webLoginSessions = /* @__PURE__ */ new Map();
6650
6658
  function ensureRuntimeDir() {
6651
6659
  fs6.mkdirSync(RUNTIME_DIR, { recursive: true });
6652
6660
  }
@@ -6740,14 +6748,227 @@ function buildQrHtml(session, qrSvg) {
6740
6748
  </html>
6741
6749
  `;
6742
6750
  }
6751
+ function buildWeixinLoginPopupHtml(sessionId) {
6752
+ const escapedSessionId = escapeHtml(sessionId);
6753
+ return `<!doctype html>
6754
+ <html lang="zh-CN">
6755
+ <head>
6756
+ <meta charset="utf-8" />
6757
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6758
+ <title>\u5FAE\u4FE1\u626B\u7801\u767B\u5F55</title>
6759
+ <style>
6760
+ :root { color-scheme: light; }
6761
+ body {
6762
+ margin: 0;
6763
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
6764
+ background: linear-gradient(180deg, #f6fbf8 0%, #eef5ff 100%);
6765
+ color: #14213d;
6766
+ }
6767
+ .wrap {
6768
+ min-height: 100vh;
6769
+ display: flex;
6770
+ align-items: center;
6771
+ justify-content: center;
6772
+ padding: 28px 18px;
6773
+ }
6774
+ .card {
6775
+ width: min(100%, 420px);
6776
+ background: rgba(255,255,255,0.95);
6777
+ border: 1px solid rgba(20,33,61,0.08);
6778
+ border-radius: 24px;
6779
+ box-shadow: 0 20px 50px rgba(36, 82, 167, 0.14);
6780
+ padding: 24px;
6781
+ }
6782
+ h1 {
6783
+ margin: 0 0 8px;
6784
+ font-size: 24px;
6785
+ }
6786
+ p {
6787
+ margin: 0;
6788
+ color: #5b6b86;
6789
+ line-height: 1.6;
6790
+ }
6791
+ .status {
6792
+ margin-top: 16px;
6793
+ padding: 10px 12px;
6794
+ border-radius: 12px;
6795
+ background: #f8fafc;
6796
+ border: 1px solid rgba(20,33,61,0.08);
6797
+ font-size: 14px;
6798
+ color: #1f2937;
6799
+ }
6800
+ .status.success {
6801
+ color: #166534;
6802
+ background: #f0fdf4;
6803
+ border-color: rgba(22, 101, 52, 0.16);
6804
+ }
6805
+ .status.error {
6806
+ color: #b91c1c;
6807
+ background: #fef2f2;
6808
+ border-color: rgba(185, 28, 28, 0.16);
6809
+ }
6810
+ .qr {
6811
+ display: flex;
6812
+ justify-content: center;
6813
+ margin: 22px 0;
6814
+ min-height: 332px;
6815
+ align-items: center;
6816
+ }
6817
+ .qr svg {
6818
+ width: 300px;
6819
+ height: 300px;
6820
+ border-radius: 18px;
6821
+ background: white;
6822
+ border: 1px solid rgba(20,33,61,0.08);
6823
+ padding: 16px;
6824
+ }
6825
+ .qr-placeholder {
6826
+ width: 300px;
6827
+ height: 300px;
6828
+ display: flex;
6829
+ align-items: center;
6830
+ justify-content: center;
6831
+ border-radius: 18px;
6832
+ border: 1px dashed rgba(20,33,61,0.18);
6833
+ color: #64748b;
6834
+ background: rgba(248, 250, 252, 0.88);
6835
+ text-align: center;
6836
+ padding: 20px;
6837
+ box-sizing: border-box;
6838
+ }
6839
+ ol {
6840
+ margin: 0;
6841
+ padding-left: 22px;
6842
+ color: #334155;
6843
+ line-height: 1.75;
6844
+ }
6845
+ .meta {
6846
+ margin-top: 16px;
6847
+ font-size: 12px;
6848
+ color: #64748b;
6849
+ }
6850
+ .actions {
6851
+ margin-top: 18px;
6852
+ display: flex;
6853
+ justify-content: flex-end;
6854
+ }
6855
+ button {
6856
+ border: 1px solid rgba(20,33,61,0.12);
6857
+ border-radius: 10px;
6858
+ background: white;
6859
+ color: #14213d;
6860
+ padding: 9px 14px;
6861
+ font: inherit;
6862
+ cursor: pointer;
6863
+ }
6864
+ button:hover {
6865
+ border-color: #2452a7;
6866
+ color: #2452a7;
6867
+ }
6868
+ </style>
6869
+ </head>
6870
+ <body>
6871
+ <div class="wrap">
6872
+ <div class="card">
6873
+ <h1>\u5FAE\u4FE1\u626B\u7801\u767B\u5F55</h1>
6874
+ <p>\u8BF7\u4F7F\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u63CF\u4E8C\u7EF4\u7801\uFF0C\u5E76\u5728\u624B\u673A\u4E0A\u5B8C\u6210\u767B\u5F55\u786E\u8BA4\u3002</p>
6875
+ <div id="status" class="status">\u6B63\u5728\u52A0\u8F7D\u4E8C\u7EF4\u7801\u2026</div>
6876
+ <div class="qr" id="qrHost">
6877
+ <div class="qr-placeholder">\u6B63\u5728\u51C6\u5907\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u2026</div>
6878
+ </div>
6879
+ <ol>
6880
+ <li>\u6253\u5F00\u624B\u673A\u5FAE\u4FE1\u626B\u4E00\u626B</li>
6881
+ <li>\u626B\u63CF\u4E8C\u7EF4\u7801</li>
6882
+ <li>\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u6388\u6743</li>
6883
+ <li>\u770B\u5230\u201C\u767B\u5F55\u6210\u529F\u201D\u540E\u5373\u53EF\u5173\u95ED\u5F53\u524D\u7A97\u53E3</li>
6884
+ </ol>
6885
+ <div class="meta" id="meta"></div>
6886
+ <div class="actions">
6887
+ <button type="button" id="closeBtn">\u5173\u95ED\u7A97\u53E3</button>
6888
+ </div>
6889
+ </div>
6890
+ </div>
6891
+ <script>
6892
+ const sessionId = ${JSON.stringify(sessionId)};
6893
+ const statusNode = document.getElementById('status');
6894
+ const qrHost = document.getElementById('qrHost');
6895
+ const meta = document.getElementById('meta');
6896
+ const closeBtn = document.getElementById('closeBtn');
6897
+ let timer = null;
6898
+
6899
+ function renderStatus(payload) {
6900
+ const session = payload && payload.session ? payload.session : null;
6901
+ if (!session) {
6902
+ statusNode.className = 'status error';
6903
+ statusNode.textContent = '\u626B\u7801\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\u3002';
6904
+ qrHost.innerHTML = '<div class="qr-placeholder">\u5F53\u524D\u4E8C\u7EF4\u7801\u5DF2\u5931\u6548\uFF0C\u8BF7\u56DE\u5230\u5DE5\u4F5C\u53F0\u91CD\u65B0\u53D1\u8D77\u626B\u7801\u3002</div>';
6905
+ meta.textContent = '\u4F1A\u8BDD\uFF1A${escapedSessionId}';
6906
+ return true;
6907
+ }
6908
+
6909
+ statusNode.className = 'status';
6910
+ if (session.status === 'confirmed') statusNode.classList.add('success');
6911
+ if (session.status === 'failed') statusNode.classList.add('error');
6912
+ statusNode.textContent = session.message || '\u7B49\u5F85\u626B\u7801\u2026';
6913
+
6914
+ if (session.qrSvg && session.status !== 'confirmed') {
6915
+ qrHost.innerHTML = session.qrSvg;
6916
+ } else if (session.status === 'confirmed') {
6917
+ qrHost.innerHTML = '<div class="qr-placeholder">\u5F53\u524D\u8D26\u53F7\u5DF2\u7ED1\u5B9A\u6210\u529F\uFF0C\u53EF\u4EE5\u5173\u95ED\u5F53\u524D\u7A97\u53E3\u3002</div>';
6918
+ } else {
6919
+ qrHost.innerHTML = '<div class="qr-placeholder">\u4E8C\u7EF4\u7801\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002</div>';
6920
+ }
6921
+
6922
+ meta.textContent = '\u4F1A\u8BDD\uFF1A' + session.id + ' \xB7 \u5DF2\u5237\u65B0 ' + session.refreshCount + ' \u6B21';
6923
+
6924
+ if (window.opener && window.location.origin) {
6925
+ try {
6926
+ window.opener.postMessage({
6927
+ source: 'codex-to-im-weixin-login',
6928
+ sessionId: session.id,
6929
+ status: session.status,
6930
+ accountId: session.accountId || '',
6931
+ }, window.location.origin);
6932
+ } catch {}
6933
+ }
6934
+
6935
+ return session.status === 'confirmed' || session.status === 'failed';
6936
+ }
6937
+
6938
+ async function tick() {
6939
+ try {
6940
+ const response = await fetch('/api/channels/weixin-login/' + encodeURIComponent(sessionId), {
6941
+ headers: { 'Content-Type': 'application/json' },
6942
+ });
6943
+ const data = await response.json();
6944
+ if (!response.ok) {
6945
+ throw new Error(data.error || '\u52A0\u8F7D\u5FAE\u4FE1\u626B\u7801\u72B6\u6001\u5931\u8D25');
6946
+ }
6947
+ if (renderStatus(data) && timer) {
6948
+ clearInterval(timer);
6949
+ timer = null;
6950
+ }
6951
+ } catch (error) {
6952
+ statusNode.className = 'status error';
6953
+ statusNode.textContent = error && error.message ? error.message : '\u52A0\u8F7D\u5FAE\u4FE1\u626B\u7801\u72B6\u6001\u5931\u8D25';
6954
+ qrHost.innerHTML = '<div class="qr-placeholder">\u65E0\u6CD5\u8BFB\u53D6\u626B\u7801\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002</div>';
6955
+ if (timer) {
6956
+ clearInterval(timer);
6957
+ timer = null;
6958
+ }
6959
+ }
6960
+ }
6961
+
6962
+ closeBtn.addEventListener('click', () => window.close());
6963
+ tick();
6964
+ timer = window.setInterval(tick, 2000);
6965
+ </script>
6966
+ </body>
6967
+ </html>`;
6968
+ }
6743
6969
  async function writeQrHtml(session) {
6744
6970
  ensureRuntimeDir();
6745
- const qrSvg = await import_qrcode.default.toString(session.qrImageUrl, {
6746
- type: "svg",
6747
- errorCorrectionLevel: "M",
6748
- margin: 0,
6749
- width: 300
6750
- });
6971
+ const qrSvg = await buildQrSvg(session.qrImageUrl);
6751
6972
  fs6.writeFileSync(HTML_PATH, buildQrHtml(session, qrSvg), "utf-8");
6752
6973
  }
6753
6974
  function openQrHtml() {
@@ -6775,6 +6996,43 @@ function normalizeAccountId(rawAccountId) {
6775
6996
  function sleep2(ms) {
6776
6997
  return new Promise((resolve) => setTimeout(resolve, ms));
6777
6998
  }
6999
+ async function buildQrSvg(qrImageUrl) {
7000
+ return await import_qrcode.default.toString(qrImageUrl, {
7001
+ type: "svg",
7002
+ errorCorrectionLevel: "M",
7003
+ margin: 0,
7004
+ width: 300
7005
+ });
7006
+ }
7007
+ function pruneWebLoginSessions() {
7008
+ const now3 = Date.now();
7009
+ for (const [sessionId, session] of webLoginSessions) {
7010
+ if (now3 - session.updatedAt > WEB_SESSION_TTL_MS) {
7011
+ webLoginSessions.delete(sessionId);
7012
+ }
7013
+ }
7014
+ }
7015
+ function toWebSessionState(session) {
7016
+ return {
7017
+ id: session.id,
7018
+ channelId: session.channelId,
7019
+ status: session.status,
7020
+ startedAt: session.startedAt,
7021
+ refreshCount: session.refreshCount,
7022
+ updatedAt: session.updatedAt,
7023
+ qrSvg: session.qrSvg,
7024
+ message: session.message,
7025
+ accountId: session.accountId
7026
+ };
7027
+ }
7028
+ function updateWebSession(sessionId, updater) {
7029
+ const current = webLoginSessions.get(sessionId);
7030
+ if (!current) return null;
7031
+ const next = updater(current);
7032
+ next.updatedAt = Date.now();
7033
+ webLoginSessions.set(sessionId, next);
7034
+ return next;
7035
+ }
6778
7036
  async function createSession(refreshCount, baseUrl) {
6779
7037
  const response = await startLoginQr(baseUrl);
6780
7038
  if (!response.qrcode || !response.qrcode_img_content) {
@@ -6788,16 +7046,137 @@ async function createSession(refreshCount, baseUrl) {
6788
7046
  refreshCount
6789
7047
  };
6790
7048
  }
6791
- async function refreshSession(previous, baseUrl) {
7049
+ async function createRefreshedSession(previous, baseUrl) {
6792
7050
  if (previous.refreshCount >= MAX_REFRESHES) {
6793
7051
  throw new Error("QR code expired too many times. Please run the login helper again.");
6794
7052
  }
6795
- const next = await createSession(previous.refreshCount + 1, baseUrl);
7053
+ return await createSession(previous.refreshCount + 1, baseUrl);
7054
+ }
7055
+ async function refreshCliSession(previous, baseUrl) {
7056
+ const next = await createRefreshedSession(previous, baseUrl);
6796
7057
  await writeQrHtml(next);
6797
7058
  openQrHtml();
6798
7059
  console.log(`[weixin-login] QR code refreshed (${next.refreshCount}/${MAX_REFRESHES})`);
6799
7060
  return next;
6800
7061
  }
7062
+ function persistConfirmedLogin(response, config = {}) {
7063
+ if (!response.bot_token || !response.ilink_bot_id) {
7064
+ throw new Error("QR login confirmed, but WeChat did not return bot credentials.");
7065
+ }
7066
+ const accountId = normalizeAccountId(response.ilink_bot_id);
7067
+ upsertWeixinAccount({
7068
+ accountId,
7069
+ userId: response.ilink_user_id || "",
7070
+ baseUrl: config.baseUrl || response.baseurl || DEFAULT_BASE_URL,
7071
+ cdnBaseUrl: config.cdnBaseUrl || DEFAULT_CDN_BASE_URL,
7072
+ token: response.bot_token,
7073
+ name: accountId,
7074
+ enabled: true
7075
+ });
7076
+ return { accountId };
7077
+ }
7078
+ async function runWeixinLoginWebSession(sessionId, config, onConfirmed) {
7079
+ let lastStatus = "waiting";
7080
+ try {
7081
+ while (true) {
7082
+ const active = webLoginSessions.get(sessionId);
7083
+ if (!active) return;
7084
+ if (Date.now() - active.startedAt > QR_TTL_MS) {
7085
+ const next = await createRefreshedSession(active, config.baseUrl);
7086
+ const qrSvg = await buildQrSvg(next.qrImageUrl);
7087
+ updateWebSession(sessionId, (current) => ({
7088
+ ...current,
7089
+ ...next,
7090
+ qrSvg,
7091
+ message: `\u4E8C\u7EF4\u7801\u5DF2\u5237\u65B0\uFF08${next.refreshCount}/${MAX_REFRESHES}\uFF09\uFF0C\u8BF7\u4F7F\u7528\u65B0\u7684\u4E8C\u7EF4\u7801\u626B\u7801\u3002`
7092
+ }));
7093
+ lastStatus = "waiting";
7094
+ continue;
7095
+ }
7096
+ const response = await pollLoginQrStatus(active.qrcode, config.baseUrl);
7097
+ switch (response.status) {
7098
+ case "wait":
7099
+ if (lastStatus !== "waiting") {
7100
+ updateWebSession(sessionId, (current) => ({
7101
+ ...current,
7102
+ status: "waiting",
7103
+ message: "\u7B49\u5F85\u626B\u7801\u2026"
7104
+ }));
7105
+ lastStatus = "waiting";
7106
+ }
7107
+ break;
7108
+ case "scaned":
7109
+ if (lastStatus !== "scanned") {
7110
+ updateWebSession(sessionId, (current) => ({
7111
+ ...current,
7112
+ status: "scanned",
7113
+ message: "\u4E8C\u7EF4\u7801\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u767B\u5F55\u3002"
7114
+ }));
7115
+ lastStatus = "scanned";
7116
+ }
7117
+ break;
7118
+ case "confirmed": {
7119
+ const persisted = persistConfirmedLogin(response, config);
7120
+ if (onConfirmed) {
7121
+ await onConfirmed(persisted.accountId);
7122
+ }
7123
+ updateWebSession(sessionId, (current) => ({
7124
+ ...current,
7125
+ status: "confirmed",
7126
+ accountId: persisted.accountId,
7127
+ message: `\u5FAE\u4FE1\u626B\u7801\u6210\u529F\uFF0C\u8D26\u53F7 ${persisted.accountId} \u5DF2\u4FDD\u5B58\u3002`
7128
+ }));
7129
+ return;
7130
+ }
7131
+ case "expired": {
7132
+ const next = await createRefreshedSession(active, config.baseUrl);
7133
+ const qrSvg = await buildQrSvg(next.qrImageUrl);
7134
+ updateWebSession(sessionId, (current) => ({
7135
+ ...current,
7136
+ ...next,
7137
+ qrSvg,
7138
+ message: `\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u5DF2\u81EA\u52A8\u5237\u65B0\uFF08${next.refreshCount}/${MAX_REFRESHES}\uFF09\u3002`
7139
+ }));
7140
+ lastStatus = "waiting";
7141
+ continue;
7142
+ }
7143
+ default:
7144
+ break;
7145
+ }
7146
+ await sleep2(POLL_INTERVAL_MS);
7147
+ }
7148
+ } catch (error) {
7149
+ updateWebSession(sessionId, (current) => ({
7150
+ ...current,
7151
+ status: "failed",
7152
+ message: error instanceof Error ? error.message : String(error)
7153
+ }));
7154
+ }
7155
+ }
7156
+ async function startWeixinLoginWebSession(options = {}) {
7157
+ pruneWebLoginSessions();
7158
+ ensureRuntimeDir();
7159
+ const config = options.config || {};
7160
+ const seed = await createSession(0, config.baseUrl);
7161
+ const sessionId = crypto4.randomUUID();
7162
+ const qrSvg = await buildQrSvg(seed.qrImageUrl);
7163
+ const session = {
7164
+ id: sessionId,
7165
+ channelId: options.channelId,
7166
+ ...seed,
7167
+ qrSvg,
7168
+ updatedAt: Date.now(),
7169
+ message: "\u7B49\u5F85\u626B\u7801\u2026"
7170
+ };
7171
+ webLoginSessions.set(sessionId, session);
7172
+ void runWeixinLoginWebSession(sessionId, config, options.onConfirmed);
7173
+ return toWebSessionState(session);
7174
+ }
7175
+ function getWeixinLoginWebSession(sessionId) {
7176
+ pruneWebLoginSessions();
7177
+ const session = webLoginSessions.get(sessionId);
7178
+ return session ? toWebSessionState(session) : void 0;
7179
+ }
6801
7180
  async function runWeixinLogin(config = {}) {
6802
7181
  ensureRuntimeDir();
6803
7182
  let session = await createSession(0, config.baseUrl);
@@ -6811,7 +7190,7 @@ async function runWeixinLogin(config = {}) {
6811
7190
  let lastStatus = session.status;
6812
7191
  while (true) {
6813
7192
  if (Date.now() - session.startedAt > QR_TTL_MS) {
6814
- session = await refreshSession(session, config.baseUrl);
7193
+ session = await refreshCliSession(session, config.baseUrl);
6815
7194
  lastStatus = session.status;
6816
7195
  }
6817
7196
  const response = await pollLoginQrStatus(session.qrcode, config.baseUrl);
@@ -6823,30 +7202,15 @@ async function runWeixinLogin(config = {}) {
6823
7202
  session.status = "scanned";
6824
7203
  break;
6825
7204
  case "confirmed": {
6826
- if (!response.bot_token || !response.ilink_bot_id) {
6827
- throw new Error("QR login confirmed, but WeChat did not return bot credentials.");
6828
- }
6829
7205
  session.status = "confirmed";
6830
- const accountId = normalizeAccountId(response.ilink_bot_id);
6831
- const previousAccount = listWeixinAccounts()[0];
6832
- upsertWeixinAccount({
6833
- accountId,
6834
- userId: response.ilink_user_id || "",
6835
- baseUrl: config.baseUrl || response.baseurl || DEFAULT_BASE_URL,
6836
- cdnBaseUrl: config.cdnBaseUrl || DEFAULT_CDN_BASE_URL,
6837
- token: response.bot_token,
6838
- name: accountId,
6839
- enabled: true
6840
- });
7206
+ const persisted = persistConfirmedLogin(response, config);
7207
+ const accountId = persisted.accountId;
6841
7208
  console.log(`[weixin-login] Login successful. Saved linked account ${accountId}`);
6842
- if (previousAccount && previousAccount.accountId !== accountId) {
6843
- console.log(`[weixin-login] Replaced previous local account ${previousAccount.accountId}`);
6844
- }
6845
7209
  console.log("[weixin-login] You can now enable the `weixin` channel and start the bridge.");
6846
7210
  return { accountId, htmlPath: HTML_PATH };
6847
7211
  }
6848
7212
  case "expired":
6849
- session = await refreshSession(session, config.baseUrl);
7213
+ session = await refreshCliSession(session, config.baseUrl);
6850
7214
  lastStatus = session.status;
6851
7215
  continue;
6852
7216
  default:
@@ -7002,6 +7366,11 @@ function asString(value) {
7002
7366
  const trimmed = value.trim();
7003
7367
  return trimmed ? trimmed : void 0;
7004
7368
  }
7369
+ function getPathSuffix(pathname, prefix) {
7370
+ if (!pathname.startsWith(prefix)) return void 0;
7371
+ const suffix = pathname.slice(prefix.length);
7372
+ return suffix ? decodeURIComponent(suffix) : void 0;
7373
+ }
7005
7374
  function parseCsv(value) {
7006
7375
  const text2 = asString(value);
7007
7376
  if (!text2) return void 0;
@@ -7045,6 +7414,15 @@ function parseChannelProvider(value) {
7045
7414
  if (value === "feishu" || value === "weixin") return value;
7046
7415
  return void 0;
7047
7416
  }
7417
+ function getWeixinAccountConflict(config, accountId, currentChannelId) {
7418
+ return (config.channels || []).find((channel) => channel.provider === "weixin" && channel.id !== currentChannelId && channel.config.accountId === accountId);
7419
+ }
7420
+ function assertWeixinAccountAvailable(config, accountId, currentChannelId) {
7421
+ if (!accountId) return;
7422
+ const conflict = getWeixinAccountConflict(config, accountId, currentChannelId);
7423
+ if (!conflict) return;
7424
+ throw new Error(`\u5FAE\u4FE1\u8D26\u53F7 ${accountId} \u5DF2\u88AB\u901A\u9053 ${getChannelLabel(conflict)} \u4F7F\u7528\uFF0C\u8BF7\u5148\u89E3\u7ED1\u6216\u6539\u7528\u5176\u4ED6\u8D26\u53F7\u3002`);
7425
+ }
7048
7426
  function createUiStore() {
7049
7427
  return new JsonFileStore(configToSettings(loadConfig()));
7050
7428
  }
@@ -7066,14 +7444,14 @@ function channelToPayload(channel) {
7066
7444
  };
7067
7445
  }
7068
7446
  function generateAccessToken() {
7069
- return crypto4.randomBytes(18).toString("base64url");
7447
+ return crypto5.randomBytes(18).toString("base64url");
7070
7448
  }
7071
7449
  function timingSafeMatch(left, right) {
7072
7450
  if (!left || !right) return false;
7073
7451
  const leftBuffer = Buffer.from(left);
7074
7452
  const rightBuffer = Buffer.from(right);
7075
7453
  if (leftBuffer.length !== rightBuffer.length) return false;
7076
- return crypto4.timingSafeEqual(leftBuffer, rightBuffer);
7454
+ return crypto5.timingSafeEqual(leftBuffer, rightBuffer);
7077
7455
  }
7078
7456
  function parseCookies(request) {
7079
7457
  const header = request.headers.cookie;
@@ -7197,8 +7575,10 @@ function mergeChannelInstance(payload, current) {
7197
7575
  feedbackMarkdownEnabled: payload.feedbackMarkdownEnabled !== false
7198
7576
  };
7199
7577
  } else {
7578
+ const accountId = asString(payload.accountId);
7579
+ assertWeixinAccountAvailable(current, accountId, existing?.id);
7200
7580
  nextConfig = {
7201
- accountId: asString(payload.accountId),
7581
+ accountId,
7202
7582
  baseUrl: asString(payload.baseUrl),
7203
7583
  cdnBaseUrl: asString(payload.cdnBaseUrl),
7204
7584
  mediaEnabled: payload.mediaEnabled === true,
@@ -8274,6 +8654,87 @@ function renderHtml() {
8274
8654
  overflow: hidden;
8275
8655
  }
8276
8656
 
8657
+ .channel-workspace .panel-header {
8658
+ margin-bottom: 20px;
8659
+ padding-bottom: 18px;
8660
+ border-bottom: 1px solid var(--border);
8661
+ gap: 18px;
8662
+ align-items: stretch;
8663
+ }
8664
+
8665
+ .channel-header-copy {
8666
+ max-width: 620px;
8667
+ }
8668
+
8669
+ .channel-header-actions {
8670
+ display: grid;
8671
+ grid-template-columns: repeat(2, minmax(0, max-content));
8672
+ align-items: stretch;
8673
+ justify-content: flex-end;
8674
+ gap: 12px;
8675
+ }
8676
+
8677
+ .channel-action-group {
8678
+ min-width: 220px;
8679
+ padding: 14px 16px;
8680
+ border: 1px solid var(--border);
8681
+ border-radius: 12px;
8682
+ background: linear-gradient(180deg, #ffffff 0%, var(--surface-soft) 100%);
8683
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
8684
+ display: grid;
8685
+ gap: 8px;
8686
+ }
8687
+
8688
+ .channel-action-label {
8689
+ font-size: 12px;
8690
+ font-weight: 600;
8691
+ color: var(--muted);
8692
+ }
8693
+
8694
+ .channel-action-row {
8695
+ display: flex;
8696
+ align-items: center;
8697
+ gap: 10px;
8698
+ }
8699
+
8700
+ .channel-action-hint {
8701
+ font-size: 12px;
8702
+ color: var(--muted);
8703
+ line-height: 1.5;
8704
+ }
8705
+
8706
+ .channel-action-row select {
8707
+ min-width: 110px;
8708
+ height: 40px;
8709
+ padding: 0 12px;
8710
+ border: 1px solid var(--border-strong);
8711
+ border-radius: 8px;
8712
+ background: #ffffff;
8713
+ color: var(--text);
8714
+ }
8715
+
8716
+ .channel-create-button,
8717
+ .channel-refresh-button {
8718
+ min-height: 40px;
8719
+ font-weight: 600;
8720
+ white-space: nowrap;
8721
+ margin-top: 0;
8722
+ }
8723
+
8724
+ .channel-create-button {
8725
+ min-width: 128px;
8726
+ }
8727
+
8728
+ .channel-refresh-button {
8729
+ width: auto;
8730
+ padding-inline: 16px;
8731
+ background: var(--surface);
8732
+ }
8733
+
8734
+ .channel-refresh-button:hover {
8735
+ background: #f8fafc;
8736
+ }
8737
+
8277
8738
  .channel-layout {
8278
8739
  display: grid;
8279
8740
  grid-template-columns: 280px minmax(0, 1fr);
@@ -8687,6 +9148,11 @@ function renderHtml() {
8687
9148
  .main { padding: 20px 20px 28px; }
8688
9149
  .channel-layout { grid-template-columns: 1fr; }
8689
9150
  .channel-sidebar { border-right: 0; padding-right: 0; }
9151
+ .channel-header-actions {
9152
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
9153
+ justify-content: stretch;
9154
+ }
9155
+ .channel-action-group { min-width: 0; }
8690
9156
  .channel-editor-summary { grid-template-columns: 1fr; }
8691
9157
  .field-row,
8692
9158
  .field-row.triple,
@@ -8704,6 +9170,13 @@ function renderHtml() {
8704
9170
  .project-group-head,
8705
9171
  .binding-head,
8706
9172
  .session-section-head { flex-direction: column; align-items: stretch; }
9173
+ .channel-header-actions,
9174
+ .channel-action-row { width: 100%; }
9175
+ .channel-action-group,
9176
+ .channel-refresh-button,
9177
+ .channel-create-button { width: 100%; }
9178
+ .channel-action-row { display: grid; grid-template-columns: 1fr; }
9179
+ .channel-action-row select { width: 100%; }
8707
9180
  .session-head { grid-template-columns: 1fr; }
8708
9181
  .session-simple-item { grid-template-columns: 1fr; }
8709
9182
  .session-actions { justify-content: flex-start; }
@@ -9043,24 +9516,33 @@ function renderHtml() {
9043
9516
  </div>
9044
9517
  </div>
9045
9518
 
9046
- <section class="panel channel-workspace">
9047
- <div class="panel-header">
9048
- <div>
9049
- <h2>\u901A\u9053\u5B9E\u4F8B</h2>
9050
- <p>\u8FD9\u91CC\u7BA1\u7406\u591A\u4E2A\u98DE\u4E66\u6216\u5FAE\u4FE1\u673A\u5668\u4EBA\u5B9E\u4F8B\u3002\u5B9E\u4F8B\u53EA\u662F\u4E0D\u540C\u804A\u5929\u5165\u53E3\uFF0C\u4E0D\u4F1A\u6539\u53D8 Codex \u7684\u4F1A\u8BDD\u8BED\u4E49\u3002</p>
9051
- </div>
9052
- <div class="toolbar">
9053
- <label class="inline-select">
9054
- \u65B0\u901A\u9053
9519
+ <section class="panel channel-workspace">
9520
+ <div class="panel-header">
9521
+ <div class="channel-header-copy">
9522
+ <h2>\u901A\u9053\u5B9E\u4F8B</h2>
9523
+ <p>\u8FD9\u91CC\u7BA1\u7406\u591A\u4E2A\u98DE\u4E66\u6216\u5FAE\u4FE1\u673A\u5668\u4EBA\u5B9E\u4F8B\u3002\u5B9E\u4F8B\u53EA\u662F\u4E0D\u540C\u804A\u5929\u5165\u53E3\uFF0C\u4E0D\u4F1A\u6539\u53D8 Codex \u7684\u4F1A\u8BDD\u8BED\u4E49\u3002</p>
9524
+ </div>
9525
+ <div class="channel-header-actions">
9526
+ <div class="channel-action-group">
9527
+ <div class="channel-action-label">\u65B0\u901A\u9053</div>
9528
+ <div class="channel-action-row">
9055
9529
  <select id="newChannelProvider">
9056
9530
  <option value="feishu">\u98DE\u4E66</option>
9057
9531
  <option value="weixin">\u5FAE\u4FE1</option>
9058
9532
  </select>
9059
- </label>
9060
- <button id="createChannelBtn">\u65B0\u589E\u901A\u9053</button>
9061
- <button id="refreshChannelsBtn">\u5237\u65B0\u72B6\u6001</button>
9533
+ <button class="primary channel-create-button" id="createChannelBtn">\u65B0\u589E\u901A\u9053</button>
9534
+ </div>
9535
+ <div class="channel-action-hint">\u5148\u9009\u62E9\u901A\u9053\u7C7B\u578B\uFF0C\u518D\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684\u673A\u5668\u4EBA\u5B9E\u4F8B\u3002</div>
9536
+ </div>
9537
+ <div class="channel-action-group">
9538
+ <div class="channel-action-label">\u72B6\u6001\u540C\u6B65</div>
9539
+ <div class="channel-action-row">
9540
+ <button class="channel-refresh-button" id="refreshChannelsBtn">\u5237\u65B0\u72B6\u6001</button>
9541
+ </div>
9542
+ <div class="channel-action-hint">\u624B\u52A8\u62C9\u53D6\u6700\u65B0\u901A\u9053\u72B6\u6001\u548C\u5F53\u524D\u7ED1\u5B9A\u4FE1\u606F\u3002</div>
9062
9543
  </div>
9063
9544
  </div>
9545
+ </div>
9064
9546
 
9065
9547
  <div class="channel-layout">
9066
9548
  <aside class="channel-sidebar">
@@ -9110,6 +9592,7 @@ function renderHtml() {
9110
9592
  activePage: 'overview',
9111
9593
  activeChannelId: '',
9112
9594
  channelDraft: null,
9595
+ weixinLoginPollers: {},
9113
9596
  };
9114
9597
 
9115
9598
  function escapeHtml(value) {
@@ -10246,7 +10729,63 @@ function renderHtml() {
10246
10729
  showMessage('channelMessage', result.ok ? 'success' : 'error', result.message);
10247
10730
  }
10248
10731
 
10732
+ function buildWeixinLoginPopupUrl(sessionId) {
10733
+ return '/weixin-login/' + encodeURIComponent(sessionId);
10734
+ }
10735
+
10736
+ function stopWeixinLoginWatcher(sessionId) {
10737
+ const timer = state.weixinLoginPollers[sessionId];
10738
+ if (!timer) return;
10739
+ window.clearInterval(timer);
10740
+ delete state.weixinLoginPollers[sessionId];
10741
+ }
10742
+
10743
+ async function watchWeixinLoginSession(sessionId) {
10744
+ stopWeixinLoginWatcher(sessionId);
10745
+
10746
+ const tick = async () => {
10747
+ try {
10748
+ const result = await api('/api/channels/weixin-login/' + encodeURIComponent(sessionId));
10749
+ const session = result.session || null;
10750
+ if (!session) {
10751
+ stopWeixinLoginWatcher(sessionId);
10752
+ showMessage('channelMessage', 'error', '\u5FAE\u4FE1\u626B\u7801\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u626B\u7801\u3002');
10753
+ return;
10754
+ }
10755
+
10756
+ if (session.status === 'confirmed') {
10757
+ stopWeixinLoginWatcher(sessionId);
10758
+ if (result.config) {
10759
+ fillForm(result.config);
10760
+ } else {
10761
+ await loadStatus();
10762
+ }
10763
+ showMessage('channelMessage', 'success', session.message || ('\u5FAE\u4FE1\u626B\u7801\u6210\u529F\uFF0C\u8D26\u53F7 ' + (session.accountId || '') + ' \u5DF2\u4FDD\u5B58\u3002'));
10764
+ return;
10765
+ }
10766
+
10767
+ if (session.status === 'failed') {
10768
+ stopWeixinLoginWatcher(sessionId);
10769
+ showMessage('channelMessage', 'error', session.message || '\u5FAE\u4FE1\u626B\u7801\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u626B\u7801\u3002');
10770
+ }
10771
+ } catch (error) {
10772
+ stopWeixinLoginWatcher(sessionId);
10773
+ showMessage('channelMessage', 'error', error.message);
10774
+ }
10775
+ };
10776
+
10777
+ await tick();
10778
+ if (!state.weixinLoginPollers[sessionId]) {
10779
+ state.weixinLoginPollers[sessionId] = window.setInterval(tick, 2000);
10780
+ }
10781
+ }
10782
+
10249
10783
  async function loginWeixinForChannel(channel) {
10784
+ let popup = null;
10785
+ try {
10786
+ popup = window.open('about:blank', '_blank', 'popup=yes,width=520,height=760');
10787
+ } catch {}
10788
+
10250
10789
  if (String(channel.id || '').startsWith('__draft__:')) {
10251
10790
  const saved = await saveChannel(channel);
10252
10791
  channel = getChannelById(saved.channel.id);
@@ -10254,13 +10793,25 @@ function renderHtml() {
10254
10793
  await saveChannel(channel);
10255
10794
  channel = getChannelById(channel.id);
10256
10795
  }
10257
- const result = await api('/api/channels/weixin-login', {
10796
+
10797
+ const result = await api('/api/channels/weixin-login/start', {
10258
10798
  method: 'POST',
10259
10799
  body: JSON.stringify({ channelId: channel.id }),
10260
10800
  });
10261
- fillForm(result.config || state.config);
10262
- await loadStatus();
10263
- showMessage('channelMessage', result.ok ? 'success' : 'error', result.message);
10801
+
10802
+ const popupUrl = result.popupUrl || buildWeixinLoginPopupUrl(result.sessionId);
10803
+ if (popup && !popup.closed) {
10804
+ popup.location.replace(popupUrl);
10805
+ } else {
10806
+ const fallback = window.open(popupUrl, '_blank', 'popup=yes,width=520,height=760');
10807
+ if (!fallback) {
10808
+ showMessage('channelMessage', 'error', '\u6D4F\u89C8\u5668\u963B\u6B62\u4E86\u626B\u7801\u5F39\u7A97\uFF0C\u8BF7\u5141\u8BB8\u5F53\u524D\u7AD9\u70B9\u6253\u5F00\u5F39\u7A97\u540E\u91CD\u8BD5\u3002');
10809
+ return;
10810
+ }
10811
+ }
10812
+
10813
+ showMessage('channelMessage', 'success', result.message || '\u5FAE\u4FE1\u626B\u7801\u7A97\u53E3\u5DF2\u6253\u5F00\uFF0C\u8BF7\u5728\u5F39\u7A97\u4E2D\u5B8C\u6210\u626B\u7801\u3002');
10814
+ void watchWeixinLoginSession(result.sessionId);
10264
10815
  }
10265
10816
 
10266
10817
  async function handleChannelEditorAction(event) {
@@ -10561,6 +11112,21 @@ var server = http.createServer(async (request, response) => {
10561
11112
  html(response, renderLoginHtml());
10562
11113
  return;
10563
11114
  }
11115
+ const weixinLoginPageSessionId = request.method === "GET" ? getPathSuffix(url.pathname, "/weixin-login/") : void 0;
11116
+ if (request.method === "GET" && weixinLoginPageSessionId) {
11117
+ if (!localRequest) {
11118
+ if (config.uiAllowLan !== true) {
11119
+ html(response, renderAccessDeniedHtml());
11120
+ return;
11121
+ }
11122
+ if (!isRemoteAuthenticated(request, config)) {
11123
+ html(response, renderLoginHtml());
11124
+ return;
11125
+ }
11126
+ }
11127
+ html(response, buildWeixinLoginPopupHtml(weixinLoginPageSessionId));
11128
+ return;
11129
+ }
10564
11130
  if (request.method === "POST" && url.pathname === "/api/auth/login") {
10565
11131
  if (config.uiAllowLan !== true) {
10566
11132
  json(response, 403, { error: "\u5F53\u524D\u672A\u5F00\u542F\u5C40\u57DF\u7F51\u8BBF\u95EE\u3002" });
@@ -10746,6 +11312,63 @@ var server = http.createServer(async (request, response) => {
10746
11312
  });
10747
11313
  return;
10748
11314
  }
11315
+ if (request.method === "POST" && url.pathname === "/api/channels/weixin-login/start") {
11316
+ const payload = await readJsonBody(request);
11317
+ const channelId = asString(payload.channelId);
11318
+ if (!channelId) {
11319
+ json(response, 400, { error: "channelId \u4E0D\u80FD\u4E3A\u7A7A\u3002" });
11320
+ return;
11321
+ }
11322
+ const current = loadConfig();
11323
+ const channel = findChannelInstance(channelId, current);
11324
+ if (!channel || channel.provider !== "weixin") {
11325
+ json(response, 404, { error: "\u6307\u5B9A\u7684\u5FAE\u4FE1\u901A\u9053\u4E0D\u5B58\u5728\u3002" });
11326
+ return;
11327
+ }
11328
+ const loginConfig = channel.config;
11329
+ const session = await startWeixinLoginWebSession({
11330
+ channelId: channel.id,
11331
+ config: loginConfig,
11332
+ onConfirmed: async (accountId) => {
11333
+ const latest = loadConfig();
11334
+ const latestChannel = findChannelInstance(channel.id, latest);
11335
+ if (!latestChannel || latestChannel.provider !== "weixin") return;
11336
+ const merged = mergeChannelInstance({
11337
+ id: latestChannel.id,
11338
+ provider: latestChannel.provider,
11339
+ alias: latestChannel.alias,
11340
+ enabled: latestChannel.enabled,
11341
+ accountId,
11342
+ baseUrl: latestChannel.config.baseUrl,
11343
+ cdnBaseUrl: latestChannel.config.cdnBaseUrl,
11344
+ mediaEnabled: latestChannel.config.mediaEnabled === true,
11345
+ feedbackMarkdownEnabled: latestChannel.config.feedbackMarkdownEnabled === true
11346
+ }, latest);
11347
+ saveConfig(merged.config);
11348
+ }
11349
+ });
11350
+ json(response, 200, {
11351
+ ok: true,
11352
+ sessionId: session.id,
11353
+ popupUrl: `/weixin-login/${encodeURIComponent(session.id)}`,
11354
+ message: "\u5FAE\u4FE1\u626B\u7801\u7A97\u53E3\u5DF2\u6253\u5F00\uFF0C\u8BF7\u5728\u5F39\u7A97\u4E2D\u5B8C\u6210\u626B\u7801\u3002"
11355
+ });
11356
+ return;
11357
+ }
11358
+ const weixinLoginStatusSessionId = request.method === "GET" ? getPathSuffix(url.pathname, "/api/channels/weixin-login/") : void 0;
11359
+ if (request.method === "GET" && weixinLoginStatusSessionId) {
11360
+ const session = getWeixinLoginWebSession(weixinLoginStatusSessionId);
11361
+ if (!session) {
11362
+ json(response, 404, { error: "\u5FAE\u4FE1\u626B\u7801\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\u3002" });
11363
+ return;
11364
+ }
11365
+ json(response, 200, {
11366
+ ok: true,
11367
+ session,
11368
+ config: session.status === "confirmed" ? configToPayload(loadConfig()) : void 0
11369
+ });
11370
+ return;
11371
+ }
10749
11372
  if (request.method === "POST" && url.pathname === "/api/install-codex-integration") {
10750
11373
  const result = await installCodexIntegration();
10751
11374
  json(response, 200, result);