codex-to-im 1.0.28 → 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
 
@@ -5646,6 +5646,30 @@ function isProcessAlive(pid) {
5646
5646
  return false;
5647
5647
  }
5648
5648
  }
5649
+ function collectTrackedBridgePids(bridgePid, statusPid) {
5650
+ const unique = /* @__PURE__ */ new Set();
5651
+ for (const pid of [bridgePid, statusPid]) {
5652
+ if (Number.isFinite(pid) && pid > 0) {
5653
+ unique.add(pid);
5654
+ }
5655
+ }
5656
+ return [...unique];
5657
+ }
5658
+ function resolveTrackedBridgePid(bridgePid, statusPid, isAlive = isProcessAlive) {
5659
+ if (isAlive(bridgePid)) return bridgePid;
5660
+ if (isAlive(statusPid)) return statusPid;
5661
+ return bridgePid ?? statusPid;
5662
+ }
5663
+ function getTrackedBridgePids(status) {
5664
+ const resolvedStatus = status ?? readJsonFile(bridgeStatusFile, { running: false });
5665
+ return collectTrackedBridgePids(readPid(bridgePidFile), resolvedStatus.pid);
5666
+ }
5667
+ function clearBridgePidFile() {
5668
+ try {
5669
+ fs3.unlinkSync(bridgePidFile);
5670
+ } catch {
5671
+ }
5672
+ }
5649
5673
  function sleep(ms) {
5650
5674
  return new Promise((resolve) => setTimeout(resolve, ms));
5651
5675
  }
@@ -5700,7 +5724,7 @@ function getUiServerUrl(port2 = uiPort) {
5700
5724
  }
5701
5725
  function getBridgeStatus() {
5702
5726
  const status = readJsonFile(bridgeStatusFile, { running: false });
5703
- const pid = readPid(bridgePidFile) ?? status.pid;
5727
+ const pid = resolveTrackedBridgePid(readPid(bridgePidFile), status.pid);
5704
5728
  if (!isProcessAlive(pid)) {
5705
5729
  return {
5706
5730
  ...status,
@@ -5738,6 +5762,27 @@ function buildDaemonEnv() {
5738
5762
  delete env.CLAUDECODE;
5739
5763
  return env;
5740
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
+ }
5741
5786
  async function waitForBridgeRunning(timeoutMs = 2e4) {
5742
5787
  const startedAt = Date.now();
5743
5788
  while (Date.now() - startedAt < timeoutMs) {
@@ -5750,7 +5795,16 @@ async function waitForBridgeRunning(timeoutMs = 2e4) {
5750
5795
  async function startBridge() {
5751
5796
  ensureDirs();
5752
5797
  const current = getBridgeStatus();
5753
- if (current.running) return current;
5798
+ const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive(pid));
5799
+ if (current.running && extraAlivePids.length === 0) return current;
5800
+ if (current.running && extraAlivePids.length > 0) {
5801
+ await stopBridge();
5802
+ }
5803
+ const config = loadConfig();
5804
+ const preflightFailure = describeBridgeStartupPreflightFailure(config.channels);
5805
+ if (preflightFailure) {
5806
+ throw new Error(preflightFailure);
5807
+ }
5754
5808
  const daemonEntry = path4.join(packageRoot, "dist", "daemon.mjs");
5755
5809
  if (!fs3.existsSync(daemonEntry)) {
5756
5810
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
@@ -5767,36 +5821,45 @@ async function startBridge() {
5767
5821
  child.unref();
5768
5822
  const status = await waitForBridgeRunning();
5769
5823
  if (!status.running) {
5770
- 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
+ );
5771
5827
  }
5772
5828
  return status;
5773
5829
  }
5774
5830
  async function stopBridge() {
5775
- const status = getBridgeStatus();
5776
- if (!status.pid || !isProcessAlive(status.pid)) {
5777
- return { ...status, running: false };
5778
- }
5779
- if (process.platform === "win32") {
5780
- await new Promise((resolve) => {
5781
- const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(status.pid), "/T", "/F"], {
5782
- stdio: "ignore",
5783
- ...WINDOWS_HIDE
5831
+ const status = readJsonFile(bridgeStatusFile, { running: false });
5832
+ const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive(pid));
5833
+ if (pids.length === 0) {
5834
+ clearBridgePidFile();
5835
+ return { ...getBridgeStatus(), running: false };
5836
+ }
5837
+ for (const pid of pids) {
5838
+ if (process.platform === "win32") {
5839
+ await new Promise((resolve) => {
5840
+ const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], {
5841
+ stdio: "ignore",
5842
+ ...WINDOWS_HIDE
5843
+ });
5844
+ killer.on("exit", () => resolve());
5845
+ killer.on("error", () => resolve());
5784
5846
  });
5785
- killer.on("exit", () => resolve());
5786
- killer.on("error", () => resolve());
5787
- });
5788
- } else {
5789
- try {
5790
- process.kill(status.pid, "SIGTERM");
5791
- } catch {
5847
+ } else {
5848
+ try {
5849
+ process.kill(pid, "SIGTERM");
5850
+ } catch {
5851
+ }
5792
5852
  }
5793
5853
  }
5794
5854
  const startedAt = Date.now();
5795
5855
  while (Date.now() - startedAt < 1e4) {
5796
- const next = getBridgeStatus();
5797
- if (!next.running) return next;
5856
+ if (pids.every((pid) => !isProcessAlive(pid))) {
5857
+ clearBridgePidFile();
5858
+ return getBridgeStatus();
5859
+ }
5798
5860
  await sleep(300);
5799
5861
  }
5862
+ clearBridgePidFile();
5800
5863
  return getBridgeStatus();
5801
5864
  }
5802
5865
  async function restartBridge() {
@@ -6429,6 +6492,7 @@ var JsonFileStore = class {
6429
6492
  var import_qrcode = __toESM(require_lib(), 1);
6430
6493
  import fs6 from "node:fs";
6431
6494
  import path7 from "node:path";
6495
+ import crypto4 from "node:crypto";
6432
6496
  import { spawn as spawn2 } from "node:child_process";
6433
6497
 
6434
6498
  // src/adapters/weixin/weixin-api.ts
@@ -6510,11 +6574,8 @@ function now2() {
6510
6574
  function getAccountRecency(account) {
6511
6575
  return account.lastLoginAt ?? account.updatedAt ?? account.createdAt;
6512
6576
  }
6513
- function normalizeAccounts(accounts) {
6514
- if (accounts.length <= 1) {
6515
- return { accounts, removedAccountIds: [] };
6516
- }
6517
- const sorted = [...accounts].sort((a, b) => {
6577
+ function sortAccountsByRecency(accounts) {
6578
+ return [...accounts].sort((a, b) => {
6518
6579
  const recencyDiff = getAccountRecency(b).localeCompare(getAccountRecency(a));
6519
6580
  if (recencyDiff !== 0) return recencyDiff;
6520
6581
  const updatedDiff = b.updatedAt.localeCompare(a.updatedAt);
@@ -6523,25 +6584,20 @@ function normalizeAccounts(accounts) {
6523
6584
  if (createdDiff !== 0) return createdDiff;
6524
6585
  return 0;
6525
6586
  });
6526
- const kept = sorted[0];
6527
- const removedAccountIds = [
6528
- ...new Set(
6529
- sorted.slice(1).map((account) => account.accountId).filter((accountId) => accountId !== kept.accountId)
6530
- )
6531
- ];
6532
- return {
6533
- accounts: [kept],
6534
- removedAccountIds
6535
- };
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());
6536
6595
  }
6537
6596
  function persistAccounts(accounts) {
6538
6597
  ensureDir2(DATA_DIR2);
6539
6598
  const normalized = normalizeAccounts(accounts);
6540
- atomicWrite2(ACCOUNTS_PATH, JSON.stringify(normalized.accounts, null, 2));
6541
- for (const accountId of normalized.removedAccountIds) {
6542
- deleteWeixinContextTokensByAccount(accountId);
6543
- }
6544
- return normalized.accounts;
6599
+ atomicWrite2(ACCOUNTS_PATH, JSON.stringify(normalized, null, 2));
6600
+ return normalized;
6545
6601
  }
6546
6602
  function readStoredAccounts() {
6547
6603
  ensureDir2(DATA_DIR2);
@@ -6549,21 +6605,13 @@ function readStoredAccounts() {
6549
6605
  return Array.isArray(raw) ? raw : [];
6550
6606
  }
6551
6607
  function readAccounts() {
6552
- return normalizeAccounts(readStoredAccounts()).accounts;
6608
+ return normalizeAccounts(readStoredAccounts());
6553
6609
  }
6554
6610
  function writeAccounts(accounts) {
6555
6611
  persistAccounts(accounts);
6556
6612
  }
6557
- function readContextTokens() {
6558
- ensureDir2(DATA_DIR2);
6559
- return readJson2(CONTEXT_TOKENS_PATH, {});
6560
- }
6561
- function writeContextTokens(tokens) {
6562
- ensureDir2(DATA_DIR2);
6563
- atomicWrite2(CONTEXT_TOKENS_PATH, JSON.stringify(tokens, null, 2));
6564
- }
6565
6613
  function listWeixinAccounts() {
6566
- return readAccounts().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
6614
+ return sortAccountsByRecency(readAccounts());
6567
6615
  }
6568
6616
  function upsertWeixinAccount(params) {
6569
6617
  const accounts = readAccounts();
@@ -6598,20 +6646,15 @@ function upsertWeixinAccount(params) {
6598
6646
  writeAccounts(nextAccounts);
6599
6647
  return account;
6600
6648
  }
6601
- function deleteWeixinContextTokensByAccount(accountId) {
6602
- const tokens = readContextTokens();
6603
- const nextTokens = Object.fromEntries(
6604
- Object.entries(tokens).filter(([key]) => !key.startsWith(`${accountId}::`))
6605
- );
6606
- writeContextTokens(nextTokens);
6607
- }
6608
6649
 
6609
6650
  // src/weixin-login.ts
6610
6651
  var MAX_REFRESHES = 3;
6611
6652
  var QR_TTL_MS = 5 * 6e4;
6612
6653
  var POLL_INTERVAL_MS = 3e3;
6654
+ var WEB_SESSION_TTL_MS = 15 * 6e4;
6613
6655
  var RUNTIME_DIR = path7.join(CTI_HOME, "runtime");
6614
6656
  var HTML_PATH = path7.join(RUNTIME_DIR, "weixin-login.html");
6657
+ var webLoginSessions = /* @__PURE__ */ new Map();
6615
6658
  function ensureRuntimeDir() {
6616
6659
  fs6.mkdirSync(RUNTIME_DIR, { recursive: true });
6617
6660
  }
@@ -6705,14 +6748,227 @@ function buildQrHtml(session, qrSvg) {
6705
6748
  </html>
6706
6749
  `;
6707
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
+ }
6708
6969
  async function writeQrHtml(session) {
6709
6970
  ensureRuntimeDir();
6710
- const qrSvg = await import_qrcode.default.toString(session.qrImageUrl, {
6711
- type: "svg",
6712
- errorCorrectionLevel: "M",
6713
- margin: 0,
6714
- width: 300
6715
- });
6971
+ const qrSvg = await buildQrSvg(session.qrImageUrl);
6716
6972
  fs6.writeFileSync(HTML_PATH, buildQrHtml(session, qrSvg), "utf-8");
6717
6973
  }
6718
6974
  function openQrHtml() {
@@ -6740,6 +6996,43 @@ function normalizeAccountId(rawAccountId) {
6740
6996
  function sleep2(ms) {
6741
6997
  return new Promise((resolve) => setTimeout(resolve, ms));
6742
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
+ }
6743
7036
  async function createSession(refreshCount, baseUrl) {
6744
7037
  const response = await startLoginQr(baseUrl);
6745
7038
  if (!response.qrcode || !response.qrcode_img_content) {
@@ -6753,16 +7046,137 @@ async function createSession(refreshCount, baseUrl) {
6753
7046
  refreshCount
6754
7047
  };
6755
7048
  }
6756
- async function refreshSession(previous, baseUrl) {
7049
+ async function createRefreshedSession(previous, baseUrl) {
6757
7050
  if (previous.refreshCount >= MAX_REFRESHES) {
6758
7051
  throw new Error("QR code expired too many times. Please run the login helper again.");
6759
7052
  }
6760
- 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);
6761
7057
  await writeQrHtml(next);
6762
7058
  openQrHtml();
6763
7059
  console.log(`[weixin-login] QR code refreshed (${next.refreshCount}/${MAX_REFRESHES})`);
6764
7060
  return next;
6765
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
+ }
6766
7180
  async function runWeixinLogin(config = {}) {
6767
7181
  ensureRuntimeDir();
6768
7182
  let session = await createSession(0, config.baseUrl);
@@ -6776,7 +7190,7 @@ async function runWeixinLogin(config = {}) {
6776
7190
  let lastStatus = session.status;
6777
7191
  while (true) {
6778
7192
  if (Date.now() - session.startedAt > QR_TTL_MS) {
6779
- session = await refreshSession(session, config.baseUrl);
7193
+ session = await refreshCliSession(session, config.baseUrl);
6780
7194
  lastStatus = session.status;
6781
7195
  }
6782
7196
  const response = await pollLoginQrStatus(session.qrcode, config.baseUrl);
@@ -6788,30 +7202,15 @@ async function runWeixinLogin(config = {}) {
6788
7202
  session.status = "scanned";
6789
7203
  break;
6790
7204
  case "confirmed": {
6791
- if (!response.bot_token || !response.ilink_bot_id) {
6792
- throw new Error("QR login confirmed, but WeChat did not return bot credentials.");
6793
- }
6794
7205
  session.status = "confirmed";
6795
- const accountId = normalizeAccountId(response.ilink_bot_id);
6796
- const previousAccount = listWeixinAccounts()[0];
6797
- upsertWeixinAccount({
6798
- accountId,
6799
- userId: response.ilink_user_id || "",
6800
- baseUrl: config.baseUrl || response.baseurl || DEFAULT_BASE_URL,
6801
- cdnBaseUrl: config.cdnBaseUrl || DEFAULT_CDN_BASE_URL,
6802
- token: response.bot_token,
6803
- name: accountId,
6804
- enabled: true
6805
- });
7206
+ const persisted = persistConfirmedLogin(response, config);
7207
+ const accountId = persisted.accountId;
6806
7208
  console.log(`[weixin-login] Login successful. Saved linked account ${accountId}`);
6807
- if (previousAccount && previousAccount.accountId !== accountId) {
6808
- console.log(`[weixin-login] Replaced previous local account ${previousAccount.accountId}`);
6809
- }
6810
7209
  console.log("[weixin-login] You can now enable the `weixin` channel and start the bridge.");
6811
7210
  return { accountId, htmlPath: HTML_PATH };
6812
7211
  }
6813
7212
  case "expired":
6814
- session = await refreshSession(session, config.baseUrl);
7213
+ session = await refreshCliSession(session, config.baseUrl);
6815
7214
  lastStatus = session.status;
6816
7215
  continue;
6817
7216
  default:
@@ -6967,6 +7366,11 @@ function asString(value) {
6967
7366
  const trimmed = value.trim();
6968
7367
  return trimmed ? trimmed : void 0;
6969
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
+ }
6970
7374
  function parseCsv(value) {
6971
7375
  const text2 = asString(value);
6972
7376
  if (!text2) return void 0;
@@ -7010,6 +7414,15 @@ function parseChannelProvider(value) {
7010
7414
  if (value === "feishu" || value === "weixin") return value;
7011
7415
  return void 0;
7012
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
+ }
7013
7426
  function createUiStore() {
7014
7427
  return new JsonFileStore(configToSettings(loadConfig()));
7015
7428
  }
@@ -7031,14 +7444,14 @@ function channelToPayload(channel) {
7031
7444
  };
7032
7445
  }
7033
7446
  function generateAccessToken() {
7034
- return crypto4.randomBytes(18).toString("base64url");
7447
+ return crypto5.randomBytes(18).toString("base64url");
7035
7448
  }
7036
7449
  function timingSafeMatch(left, right) {
7037
7450
  if (!left || !right) return false;
7038
7451
  const leftBuffer = Buffer.from(left);
7039
7452
  const rightBuffer = Buffer.from(right);
7040
7453
  if (leftBuffer.length !== rightBuffer.length) return false;
7041
- return crypto4.timingSafeEqual(leftBuffer, rightBuffer);
7454
+ return crypto5.timingSafeEqual(leftBuffer, rightBuffer);
7042
7455
  }
7043
7456
  function parseCookies(request) {
7044
7457
  const header = request.headers.cookie;
@@ -7162,8 +7575,10 @@ function mergeChannelInstance(payload, current) {
7162
7575
  feedbackMarkdownEnabled: payload.feedbackMarkdownEnabled !== false
7163
7576
  };
7164
7577
  } else {
7578
+ const accountId = asString(payload.accountId);
7579
+ assertWeixinAccountAvailable(current, accountId, existing?.id);
7165
7580
  nextConfig = {
7166
- accountId: asString(payload.accountId),
7581
+ accountId,
7167
7582
  baseUrl: asString(payload.baseUrl),
7168
7583
  cdnBaseUrl: asString(payload.cdnBaseUrl),
7169
7584
  mediaEnabled: payload.mediaEnabled === true,
@@ -8239,6 +8654,87 @@ function renderHtml() {
8239
8654
  overflow: hidden;
8240
8655
  }
8241
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
+
8242
8738
  .channel-layout {
8243
8739
  display: grid;
8244
8740
  grid-template-columns: 280px minmax(0, 1fr);
@@ -8652,6 +9148,11 @@ function renderHtml() {
8652
9148
  .main { padding: 20px 20px 28px; }
8653
9149
  .channel-layout { grid-template-columns: 1fr; }
8654
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; }
8655
9156
  .channel-editor-summary { grid-template-columns: 1fr; }
8656
9157
  .field-row,
8657
9158
  .field-row.triple,
@@ -8669,6 +9170,13 @@ function renderHtml() {
8669
9170
  .project-group-head,
8670
9171
  .binding-head,
8671
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%; }
8672
9180
  .session-head { grid-template-columns: 1fr; }
8673
9181
  .session-simple-item { grid-template-columns: 1fr; }
8674
9182
  .session-actions { justify-content: flex-start; }
@@ -9008,24 +9516,33 @@ function renderHtml() {
9008
9516
  </div>
9009
9517
  </div>
9010
9518
 
9011
- <section class="panel channel-workspace">
9012
- <div class="panel-header">
9013
- <div>
9014
- <h2>\u901A\u9053\u5B9E\u4F8B</h2>
9015
- <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>
9016
- </div>
9017
- <div class="toolbar">
9018
- <label class="inline-select">
9019
- \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">
9020
9529
  <select id="newChannelProvider">
9021
9530
  <option value="feishu">\u98DE\u4E66</option>
9022
9531
  <option value="weixin">\u5FAE\u4FE1</option>
9023
9532
  </select>
9024
- </label>
9025
- <button id="createChannelBtn">\u65B0\u589E\u901A\u9053</button>
9026
- <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>
9027
9543
  </div>
9028
9544
  </div>
9545
+ </div>
9029
9546
 
9030
9547
  <div class="channel-layout">
9031
9548
  <aside class="channel-sidebar">
@@ -9075,6 +9592,7 @@ function renderHtml() {
9075
9592
  activePage: 'overview',
9076
9593
  activeChannelId: '',
9077
9594
  channelDraft: null,
9595
+ weixinLoginPollers: {},
9078
9596
  };
9079
9597
 
9080
9598
  function escapeHtml(value) {
@@ -10211,7 +10729,63 @@ function renderHtml() {
10211
10729
  showMessage('channelMessage', result.ok ? 'success' : 'error', result.message);
10212
10730
  }
10213
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
+
10214
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
+
10215
10789
  if (String(channel.id || '').startsWith('__draft__:')) {
10216
10790
  const saved = await saveChannel(channel);
10217
10791
  channel = getChannelById(saved.channel.id);
@@ -10219,13 +10793,25 @@ function renderHtml() {
10219
10793
  await saveChannel(channel);
10220
10794
  channel = getChannelById(channel.id);
10221
10795
  }
10222
- const result = await api('/api/channels/weixin-login', {
10796
+
10797
+ const result = await api('/api/channels/weixin-login/start', {
10223
10798
  method: 'POST',
10224
10799
  body: JSON.stringify({ channelId: channel.id }),
10225
10800
  });
10226
- fillForm(result.config || state.config);
10227
- await loadStatus();
10228
- 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);
10229
10815
  }
10230
10816
 
10231
10817
  async function handleChannelEditorAction(event) {
@@ -10526,6 +11112,21 @@ var server = http.createServer(async (request, response) => {
10526
11112
  html(response, renderLoginHtml());
10527
11113
  return;
10528
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
+ }
10529
11130
  if (request.method === "POST" && url.pathname === "/api/auth/login") {
10530
11131
  if (config.uiAllowLan !== true) {
10531
11132
  json(response, 403, { error: "\u5F53\u524D\u672A\u5F00\u542F\u5C40\u57DF\u7F51\u8BBF\u95EE\u3002" });
@@ -10711,6 +11312,63 @@ var server = http.createServer(async (request, response) => {
10711
11312
  });
10712
11313
  return;
10713
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
+ }
10714
11372
  if (request.method === "POST" && url.pathname === "/api/install-codex-integration") {
10715
11373
  const result = await installCodexIntegration();
10716
11374
  json(response, 200, result);