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