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.
- package/dist/cli.mjs +261 -21
- package/dist/daemon.mjs +247 -57
- package/dist/ui-server.mjs +763 -105
- package/package.json +1 -1
- package/references/setup-guides.md +1 -1
- package/scripts/doctor.sh +9 -9
package/dist/ui-server.mjs
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
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
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
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
|
-
|
|
5797
|
-
|
|
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
|
|
6514
|
-
|
|
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
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
)
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
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
|
|
6541
|
-
|
|
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())
|
|
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()
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
6796
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
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
|
-
|
|
9025
|
-
|
|
9026
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
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);
|