codex-weixin 0.2.6 → 0.2.8
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/CHANGELOG.md +29 -0
- package/README.en.md +5 -2
- package/README.md +7 -2
- package/dist/bridge/service.d.ts +1 -0
- package/dist/bridge/service.js +44 -4
- package/dist/bridge/service.js.map +1 -1
- package/dist/codex/app-server-runner.d.ts +8 -0
- package/dist/codex/app-server-runner.js +91 -5
- package/dist/codex/app-server-runner.js.map +1 -1
- package/dist/codex/exec-runner.d.ts +2 -0
- package/dist/codex/exec-runner.js.map +1 -1
- package/dist/codex/runner.js +7 -2
- package/dist/codex/runner.js.map +1 -1
- package/dist/server/account-manager.d.ts +2 -1
- package/dist/server/account-manager.js +9 -2
- package/dist/server/account-manager.js.map +1 -1
- package/dist/server/http-server.js +55 -5
- package/dist/server/http-server.js.map +1 -1
- package/dist/state/config.d.ts +1 -0
- package/dist/state/config.js +2 -0
- package/dist/state/config.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.js +20 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/web/app.js +198 -17
- package/dist/web/index.html +31 -3
- package/dist/web/styles.css +32 -6
- package/package.json +1 -1
package/dist/web/app.js
CHANGED
|
@@ -15,6 +15,7 @@ const state = {
|
|
|
15
15
|
sendingMessage: false,
|
|
16
16
|
savingSessionRuntime: false,
|
|
17
17
|
updateInfo: null,
|
|
18
|
+
updateChecking: false,
|
|
18
19
|
updateInstalling: false,
|
|
19
20
|
selectedAccountId: "",
|
|
20
21
|
chatFiles: []
|
|
@@ -25,6 +26,7 @@ const MAX_CHAT_FILE_BYTES = 50 * 1024 * 1024;
|
|
|
25
26
|
const DISMISSED_UPDATE_KEY = "codex-weixin.dismissed-update";
|
|
26
27
|
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
27
28
|
const UPDATE_RECONNECT_TIMEOUT_MS = 90 * 1000;
|
|
29
|
+
let streamingRenderFrame = 0;
|
|
28
30
|
|
|
29
31
|
const els = {};
|
|
30
32
|
|
|
@@ -48,6 +50,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
48
50
|
sessionRuntimeToolbar: document.querySelector("#sessionRuntimeToolbar"),
|
|
49
51
|
sessionModelInput: document.querySelector("#sessionModelInput"),
|
|
50
52
|
sessionEffortInput: document.querySelector("#sessionEffortInput"),
|
|
53
|
+
sessionStreamInput: document.querySelector("#sessionStreamInput"),
|
|
51
54
|
runningAccountMetric: document.querySelector("#runningAccountMetric"),
|
|
52
55
|
sessionMetric: document.querySelector("#sessionMetric"),
|
|
53
56
|
workspaceMetric: document.querySelector("#workspaceMetric"),
|
|
@@ -62,6 +65,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
62
65
|
updateProgressDetail: document.querySelector("#updateProgressDetail"),
|
|
63
66
|
updateLaterButton: document.querySelector("#updateLaterButton"),
|
|
64
67
|
updateNowButton: document.querySelector("#updateNowButton"),
|
|
68
|
+
updateCheckButton: document.querySelector("#updateCheckButton"),
|
|
69
|
+
settingsVersionValue: document.querySelector("#settingsVersionValue"),
|
|
65
70
|
accountDialog: document.querySelector("#accountDialog"),
|
|
66
71
|
sessionDialog: document.querySelector("#sessionDialog")
|
|
67
72
|
});
|
|
@@ -80,6 +85,7 @@ function bindEvents() {
|
|
|
80
85
|
document.querySelector("#modelInput").addEventListener("change", () => renderEffortOptions(""));
|
|
81
86
|
els.sessionModelInput.addEventListener("change", () => void handleSessionModelChange());
|
|
82
87
|
els.sessionEffortInput.addEventListener("change", () => void saveSessionRuntimeSettings());
|
|
88
|
+
els.sessionStreamInput.addEventListener("change", () => void saveSessionRuntimeSettings());
|
|
83
89
|
document.querySelector("#accountForm").addEventListener("submit", (event) => void saveAccountRemark(event));
|
|
84
90
|
document.querySelector("#sessionForm").addEventListener("submit", (event) => void saveSession(event));
|
|
85
91
|
document.querySelector("#sessionSenderInput").addEventListener("change", updateNewSessionDefaultTitle);
|
|
@@ -96,6 +102,7 @@ function bindEvents() {
|
|
|
96
102
|
els.qrDialog.addEventListener("close", stopLoginPoll);
|
|
97
103
|
els.updateLaterButton.addEventListener("click", dismissUpdate);
|
|
98
104
|
els.updateNowButton.addEventListener("click", () => void installUpdate());
|
|
105
|
+
els.updateCheckButton.addEventListener("click", () => void checkForUpdateNow());
|
|
99
106
|
els.updateDialog.addEventListener("cancel", (event) => {
|
|
100
107
|
event.preventDefault();
|
|
101
108
|
if (!state.updateInstalling) dismissUpdate();
|
|
@@ -126,23 +133,54 @@ async function bootstrap() {
|
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
async function checkForUpdate() {
|
|
129
|
-
if (state.updateInstalling) return;
|
|
136
|
+
if (state.updateInstalling || state.updateChecking) return;
|
|
130
137
|
try {
|
|
131
138
|
const info = await api("/api/update", { token: false });
|
|
132
139
|
if (!info.updateAvailable || !info.latestVersion || dismissedUpdateVersion() === info.latestVersion) {
|
|
133
140
|
return;
|
|
134
141
|
}
|
|
135
|
-
|
|
136
|
-
els.updateCurrentVersion.textContent = `v${String(info.currentVersion).replace(/^v/i, "")}`;
|
|
137
|
-
els.updateLatestVersion.textContent = `v${String(info.latestVersion).replace(/^v/i, "")}`;
|
|
138
|
-
resetUpdateDialog();
|
|
139
|
-
if (!els.updateDialog.open) els.updateDialog.showModal();
|
|
140
|
-
drawIcons();
|
|
142
|
+
showAvailableUpdate(info);
|
|
141
143
|
} catch {
|
|
142
144
|
// Update checks are best-effort and must not interrupt the local management page.
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
async function checkForUpdateNow() {
|
|
149
|
+
if (state.updateChecking || state.updateInstalling) return;
|
|
150
|
+
const label = els.updateCheckButton.querySelector("span");
|
|
151
|
+
state.updateChecking = true;
|
|
152
|
+
els.updateCheckButton.disabled = true;
|
|
153
|
+
els.updateCheckButton.classList.add("is-loading");
|
|
154
|
+
label.textContent = "检查中";
|
|
155
|
+
try {
|
|
156
|
+
const info = await api("/api/update?force=1");
|
|
157
|
+
if (info.error) throw new Error(info.error);
|
|
158
|
+
if (info.updateAvailable && info.latestVersion) {
|
|
159
|
+
showAvailableUpdate(info);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const current = String(info.currentVersion || state.version || "").replace(/^v/i, "");
|
|
163
|
+
toast(current ? `已是最新版本 v${current}` : "已是最新版本");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
toast(error.message || "无法检查新版本", true);
|
|
166
|
+
} finally {
|
|
167
|
+
state.updateChecking = false;
|
|
168
|
+
els.updateCheckButton.disabled = false;
|
|
169
|
+
els.updateCheckButton.classList.remove("is-loading");
|
|
170
|
+
label.textContent = "检查更新";
|
|
171
|
+
drawIcons();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function showAvailableUpdate(info) {
|
|
176
|
+
state.updateInfo = info;
|
|
177
|
+
els.updateCurrentVersion.textContent = `v${String(info.currentVersion).replace(/^v/i, "")}`;
|
|
178
|
+
els.updateLatestVersion.textContent = `v${String(info.latestVersion).replace(/^v/i, "")}`;
|
|
179
|
+
resetUpdateDialog();
|
|
180
|
+
if (!els.updateDialog.open) els.updateDialog.showModal();
|
|
181
|
+
drawIcons();
|
|
182
|
+
}
|
|
183
|
+
|
|
146
184
|
function dismissUpdate() {
|
|
147
185
|
if (state.updateInstalling) return;
|
|
148
186
|
const version = state.updateInfo?.latestVersion;
|
|
@@ -290,6 +328,7 @@ function renderProductVersion() {
|
|
|
290
328
|
const version = state.version.trim();
|
|
291
329
|
els.productVersion.hidden = !version;
|
|
292
330
|
els.productVersion.textContent = version ? `v${version.replace(/^v/i, "")}` : "";
|
|
331
|
+
els.settingsVersionValue.textContent = version ? `v${version.replace(/^v/i, "")}` : "--";
|
|
293
332
|
}
|
|
294
333
|
|
|
295
334
|
function renderMetrics() {
|
|
@@ -441,6 +480,7 @@ function renderSettings() {
|
|
|
441
480
|
document.querySelector("#allowedWorkspacesInput").value = (state.config.allowedWorkspaces || []).join("\n");
|
|
442
481
|
document.querySelector("#backendInput").value = state.config.codexBackend || "auto";
|
|
443
482
|
document.querySelector("#sandboxInput").value = state.config.codexExecSandbox || "";
|
|
483
|
+
document.querySelector("#streamRepliesInput").checked = Boolean(state.config.streamReplies);
|
|
444
484
|
renderModelOptions();
|
|
445
485
|
document.querySelector("#effectiveModelValue").textContent = state.codexRuntime?.model || state.config.model || "Codex 默认";
|
|
446
486
|
document.querySelector("#effectiveEffortValue").textContent = state.codexRuntime?.effort || state.config.effort || "Codex 默认";
|
|
@@ -691,23 +731,79 @@ function renderChatPanel() {
|
|
|
691
731
|
return;
|
|
692
732
|
}
|
|
693
733
|
const renderKey = `messages:${sessionKey(session)}:${responding}:${JSON.stringify(state.sessionMessages)}`;
|
|
694
|
-
const html = state.sessionMessages
|
|
734
|
+
const html = renderConversationMessages(state.sessionMessages, responding) + (responding ? renderTypingIndicator() : "");
|
|
735
|
+
setChatMessagesHtml(html, renderKey);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function renderConversationMessages(messages, responding) {
|
|
739
|
+
const html = [];
|
|
740
|
+
let lastUserCreatedAt;
|
|
741
|
+
let index = 0;
|
|
742
|
+
while (index < messages.length) {
|
|
743
|
+
const message = messages[index];
|
|
744
|
+
if (message.kind === "progress") {
|
|
745
|
+
const progress = [];
|
|
746
|
+
while (index < messages.length && messages[index].kind === "progress") {
|
|
747
|
+
progress.push(messages[index]);
|
|
748
|
+
index += 1;
|
|
749
|
+
}
|
|
750
|
+
const nextMessage = messages[index];
|
|
751
|
+
const active = responding && !nextMessage;
|
|
752
|
+
const completedAt = nextMessage?.role === "assistant"
|
|
753
|
+
? nextMessage.createdAt
|
|
754
|
+
: progress.at(-1)?.createdAt;
|
|
755
|
+
html.push(renderProgressGroup(progress, lastUserCreatedAt, completedAt, active));
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (message.role === "user") lastUserCreatedAt = message.createdAt;
|
|
759
|
+
html.push(renderChatMessage(message));
|
|
760
|
+
index += 1;
|
|
761
|
+
}
|
|
762
|
+
return html.join("");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function renderChatMessage(message) {
|
|
766
|
+
return `<article class="chat-message is-${escapeAttr(message.role)}${message.attachments?.length ? " has-attachments" : ""}">
|
|
695
767
|
<div class="message-meta"><span>${message.role === "user" ? "你" : "Codex"}</span>${message.createdAt ? `<time datetime="${escapeAttr(message.createdAt)}">${escapeHtml(messageTime(message.createdAt))}</time>` : ""}</div>
|
|
696
768
|
<div class="message-bubble">${message.text ? renderMarkdown(message.text) : ""}${renderMessageAttachments(message.attachments)}</div>
|
|
697
|
-
</article
|
|
698
|
-
|
|
769
|
+
</article>`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function renderProgressGroup(messages, startedAt, completedAt, active) {
|
|
773
|
+
const duration = formatProcessingDuration(startedAt, active ? new Date().toISOString() : completedAt);
|
|
774
|
+
return `<details class="chat-progress-group"${active ? " open" : ""}>
|
|
775
|
+
<summary>
|
|
776
|
+
<span class="progress-summary-title"><i data-lucide="chevron-right"></i>处理过程</span>
|
|
777
|
+
<span>${active ? "已处理" : "处理用时"} ${escapeHtml(duration)}</span>
|
|
778
|
+
</summary>
|
|
779
|
+
<ol class="progress-list">${messages.map((message) => `<li>${renderMarkdown(message.text)}</li>`).join("")}</ol>
|
|
780
|
+
</details>`;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function formatProcessingDuration(startValue, endValue) {
|
|
784
|
+
const start = new Date(startValue || "").getTime();
|
|
785
|
+
const end = new Date(endValue || "").getTime();
|
|
786
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return "--";
|
|
787
|
+
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
|
788
|
+
if (seconds < 60) return `${seconds} 秒`;
|
|
789
|
+
const minutes = Math.floor(seconds / 60);
|
|
790
|
+
const remaining = seconds % 60;
|
|
791
|
+
return remaining ? `${minutes} 分 ${remaining} 秒` : `${minutes} 分钟`;
|
|
699
792
|
}
|
|
700
793
|
|
|
701
794
|
function renderSessionRuntimeControls(session) {
|
|
702
795
|
const disabled = !session || state.sendingMessage || state.savingSessionRuntime;
|
|
703
796
|
els.sessionModelInput.disabled = disabled;
|
|
704
797
|
els.sessionEffortInput.disabled = disabled;
|
|
798
|
+
els.sessionStreamInput.disabled = disabled;
|
|
705
799
|
const renderKey = session ? [
|
|
706
800
|
sessionKey(session),
|
|
707
801
|
session.model || "",
|
|
708
802
|
session.effort || "",
|
|
803
|
+
typeof session.streamReplies === "boolean" ? String(session.streamReplies) : "inherit",
|
|
709
804
|
state.config?.model || "",
|
|
710
805
|
state.config?.effort || "",
|
|
806
|
+
String(Boolean(state.config?.streamReplies)),
|
|
711
807
|
state.codexRuntime?.model || "",
|
|
712
808
|
state.codexRuntime?.effort || "",
|
|
713
809
|
state.codexModels.length
|
|
@@ -718,6 +814,7 @@ function renderSessionRuntimeControls(session) {
|
|
|
718
814
|
if (!session) {
|
|
719
815
|
els.sessionModelInput.innerHTML = '<option value="">选择会话后设置</option>';
|
|
720
816
|
els.sessionEffortInput.innerHTML = '<option value="">选择会话后设置</option>';
|
|
817
|
+
els.sessionStreamInput.innerHTML = '<option value="">选择会话后设置</option>';
|
|
721
818
|
return;
|
|
722
819
|
}
|
|
723
820
|
|
|
@@ -739,6 +836,14 @@ function renderSessionRuntimeControls(session) {
|
|
|
739
836
|
els.sessionModelInput.innerHTML = options.map((option) => `<option value="${escapeAttr(option.value)}"${option.title ? ` title="${escapeAttr(option.title)}"` : ""}>${escapeHtml(option.label)}</option>`).join("");
|
|
740
837
|
els.sessionModelInput.value = session.model || "";
|
|
741
838
|
setSessionEffortOptions(session.model || "", session.effort || "");
|
|
839
|
+
els.sessionStreamInput.innerHTML = [
|
|
840
|
+
`<option value="">继承全局(${state.config?.streamReplies ? "开启" : "关闭"})</option>`,
|
|
841
|
+
'<option value="on">开启</option>',
|
|
842
|
+
'<option value="off">关闭</option>'
|
|
843
|
+
].join("");
|
|
844
|
+
els.sessionStreamInput.value = typeof session.streamReplies === "boolean"
|
|
845
|
+
? session.streamReplies ? "on" : "off"
|
|
846
|
+
: "";
|
|
742
847
|
}
|
|
743
848
|
|
|
744
849
|
function setSessionEffortOptions(modelOverride, preferredEffort) {
|
|
@@ -783,18 +888,23 @@ async function saveSessionRuntimeSettings() {
|
|
|
783
888
|
const key = sessionKey(session);
|
|
784
889
|
const model = els.sessionModelInput.value;
|
|
785
890
|
const effort = els.sessionEffortInput.value;
|
|
891
|
+
const stream = els.sessionStreamInput.value;
|
|
786
892
|
state.savingSessionRuntime = true;
|
|
787
893
|
renderChatPanel();
|
|
788
894
|
try {
|
|
789
895
|
const result = await api(`/api/sessions/${encodeURIComponent(session.accountId)}/${encodeURIComponent(session.id)}`, {
|
|
790
896
|
method: "PATCH",
|
|
791
|
-
body: {
|
|
897
|
+
body: {
|
|
898
|
+
model: model || null,
|
|
899
|
+
effort: effort || null,
|
|
900
|
+
streamReplies: stream ? stream === "on" : null
|
|
901
|
+
}
|
|
792
902
|
});
|
|
793
903
|
const index = state.sessions.findIndex((candidate) => sessionKey(candidate) === key);
|
|
794
904
|
if (index >= 0) state.sessions[index] = result.session;
|
|
795
905
|
els.sessionRuntimeToolbar.dataset.renderKey = "";
|
|
796
906
|
renderSessions();
|
|
797
|
-
toast("
|
|
907
|
+
toast("会话设置已更新");
|
|
798
908
|
} catch (error) {
|
|
799
909
|
toast(error.message, true);
|
|
800
910
|
await refreshData(false);
|
|
@@ -859,6 +969,8 @@ async function sendSessionMessage(event) {
|
|
|
859
969
|
const files = [...state.chatFiles];
|
|
860
970
|
if (!session || (!text && !files.length) || state.sendingMessage) return;
|
|
861
971
|
const key = sessionKey(session);
|
|
972
|
+
const streaming = session.streamReplies ?? Boolean(state.config?.streamReplies);
|
|
973
|
+
let progressSequence = 0;
|
|
862
974
|
state.sendingMessage = true;
|
|
863
975
|
state.sessionMessages.push({
|
|
864
976
|
id: `pending-${Date.now()}`,
|
|
@@ -881,10 +993,29 @@ async function sendSessionMessage(event) {
|
|
|
881
993
|
const body = new FormData();
|
|
882
994
|
body.append("text", text);
|
|
883
995
|
files.forEach((file) => body.append("files", file, file.name));
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
body
|
|
887
|
-
|
|
996
|
+
const url = `/api/sessions/${encodeURIComponent(session.accountId)}/${encodeURIComponent(session.id)}/messages`;
|
|
997
|
+
if (streaming) {
|
|
998
|
+
await streamApi(`${url}?stream=1`, { method: "POST", body }, (streamEvent) => {
|
|
999
|
+
if (state.selectedSessionKey !== key) return;
|
|
1000
|
+
if (streamEvent.type === "progress" && streamEvent.message?.trim()) {
|
|
1001
|
+
state.sessionMessages.push({
|
|
1002
|
+
id: `progress-${Date.now()}-${progressSequence++}`,
|
|
1003
|
+
role: "assistant",
|
|
1004
|
+
text: streamEvent.message.trim(),
|
|
1005
|
+
kind: "progress",
|
|
1006
|
+
createdAt: new Date().toISOString(),
|
|
1007
|
+
attachments: []
|
|
1008
|
+
});
|
|
1009
|
+
scheduleStreamingRender();
|
|
1010
|
+
}
|
|
1011
|
+
if (streamEvent.type === "done" && streamEvent.result?.message) {
|
|
1012
|
+
state.sessionMessages.push(streamEvent.result.message);
|
|
1013
|
+
scheduleStreamingRender();
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
} else {
|
|
1017
|
+
await api(url, { method: "POST", body });
|
|
1018
|
+
}
|
|
888
1019
|
await refreshData(false);
|
|
889
1020
|
if (state.selectedSessionKey === key) {
|
|
890
1021
|
await loadSelectedSessionMessages();
|
|
@@ -903,6 +1034,15 @@ async function sendSessionMessage(event) {
|
|
|
903
1034
|
}
|
|
904
1035
|
}
|
|
905
1036
|
|
|
1037
|
+
function scheduleStreamingRender() {
|
|
1038
|
+
if (streamingRenderFrame) return;
|
|
1039
|
+
streamingRenderFrame = requestAnimationFrame(() => {
|
|
1040
|
+
streamingRenderFrame = 0;
|
|
1041
|
+
renderChatPanel();
|
|
1042
|
+
scrollChatToEnd();
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
906
1046
|
function handleChatFileSelection(event) {
|
|
907
1047
|
const nextFiles = [...event.target.files];
|
|
908
1048
|
event.target.value = "";
|
|
@@ -1115,7 +1255,8 @@ async function saveSettings(event) {
|
|
|
1115
1255
|
codexBackend: document.querySelector("#backendInput").value,
|
|
1116
1256
|
codexExecSandbox: document.querySelector("#sandboxInput").value || null,
|
|
1117
1257
|
model: document.querySelector("#modelInput").value.trim(),
|
|
1118
|
-
effort: document.querySelector("#effortInput").value.trim()
|
|
1258
|
+
effort: document.querySelector("#effortInput").value.trim(),
|
|
1259
|
+
streamReplies: document.querySelector("#streamRepliesInput").checked
|
|
1119
1260
|
}
|
|
1120
1261
|
});
|
|
1121
1262
|
state.config = result.config;
|
|
@@ -1159,6 +1300,46 @@ async function api(url, options = {}) {
|
|
|
1159
1300
|
return data;
|
|
1160
1301
|
}
|
|
1161
1302
|
|
|
1303
|
+
async function streamApi(url, options, onEvent) {
|
|
1304
|
+
const headers = {};
|
|
1305
|
+
if (state.requestToken) headers["X-Codex-Weixin-Token"] = state.requestToken;
|
|
1306
|
+
const response = await fetch(url, {
|
|
1307
|
+
method: options.method || "POST",
|
|
1308
|
+
headers,
|
|
1309
|
+
body: options.body
|
|
1310
|
+
});
|
|
1311
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1312
|
+
if (!contentType.startsWith("application/x-ndjson")) {
|
|
1313
|
+
const data = await response.json().catch(() => ({}));
|
|
1314
|
+
if (!response.ok) throw new Error(data.error || `请求失败 (${response.status})`);
|
|
1315
|
+
return data.result;
|
|
1316
|
+
}
|
|
1317
|
+
if (!response.ok || !response.body) {
|
|
1318
|
+
throw new Error(`请求失败 (${response.status})`);
|
|
1319
|
+
}
|
|
1320
|
+
const reader = response.body.getReader();
|
|
1321
|
+
const decoder = new TextDecoder();
|
|
1322
|
+
let buffer = "";
|
|
1323
|
+
let result;
|
|
1324
|
+
const consumeLine = (line) => {
|
|
1325
|
+
if (!line.trim()) return;
|
|
1326
|
+
const streamEvent = JSON.parse(line);
|
|
1327
|
+
if (streamEvent.type === "error") throw new Error(streamEvent.error || "过程进度失败");
|
|
1328
|
+
onEvent(streamEvent);
|
|
1329
|
+
if (streamEvent.type === "done") result = streamEvent.result;
|
|
1330
|
+
};
|
|
1331
|
+
while (true) {
|
|
1332
|
+
const { value, done } = await reader.read();
|
|
1333
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
1334
|
+
const lines = buffer.split(/\r?\n/);
|
|
1335
|
+
buffer = lines.pop() || "";
|
|
1336
|
+
for (const line of lines) consumeLine(line);
|
|
1337
|
+
if (done) break;
|
|
1338
|
+
}
|
|
1339
|
+
if (buffer) consumeLine(buffer);
|
|
1340
|
+
return result;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1162
1343
|
function emptyState(icon, title, description = "", action = "") {
|
|
1163
1344
|
return `<div class="empty-state"><div class="empty-state-inner"><span class="empty-icon"><i data-lucide="${escapeAttr(icon)}"></i></span><h2>${escapeHtml(title)}</h2>${description ? `<p>${escapeHtml(description)}</p>` : ""}${action}</div></div>`;
|
|
1164
1345
|
}
|
package/dist/web/index.html
CHANGED
|
@@ -109,6 +109,14 @@
|
|
|
109
109
|
<span>推理强度</span>
|
|
110
110
|
<select id="sessionEffortInput" disabled><option value="">继承全局设置</option></select>
|
|
111
111
|
</label>
|
|
112
|
+
<label>
|
|
113
|
+
<span>过程进度</span>
|
|
114
|
+
<select id="sessionStreamInput" disabled>
|
|
115
|
+
<option value="">继承全局设置</option>
|
|
116
|
+
<option value="on">开启</option>
|
|
117
|
+
<option value="off">关闭</option>
|
|
118
|
+
</select>
|
|
119
|
+
</label>
|
|
112
120
|
</div>
|
|
113
121
|
<div class="chat-messages" id="chatMessages" aria-live="polite"></div>
|
|
114
122
|
<form class="chat-composer" id="chatComposer">
|
|
@@ -184,6 +192,26 @@
|
|
|
184
192
|
</select>
|
|
185
193
|
<small class="field-hint effective-setting"><span>当前生效</span><strong id="effectiveEffortValue">正在读取</strong></small>
|
|
186
194
|
</label>
|
|
195
|
+
<label class="toggle-field" for="streamRepliesInput">
|
|
196
|
+
<span>过程进度</span>
|
|
197
|
+
<span class="toggle-control">
|
|
198
|
+
<input id="streamRepliesInput" name="streamReplies" type="checkbox">
|
|
199
|
+
<span aria-hidden="true"></span>
|
|
200
|
+
</span>
|
|
201
|
+
</label>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="settings-group">
|
|
205
|
+
<h2>版本</h2>
|
|
206
|
+
<div class="settings-update">
|
|
207
|
+
<span class="settings-update-version">
|
|
208
|
+
<small>当前版本</small>
|
|
209
|
+
<strong id="settingsVersionValue">--</strong>
|
|
210
|
+
</span>
|
|
211
|
+
<button class="button button-secondary" id="updateCheckButton" type="button">
|
|
212
|
+
<i data-lucide="refresh-cw"></i>
|
|
213
|
+
<span>检查更新</span>
|
|
214
|
+
</button>
|
|
187
215
|
</div>
|
|
188
216
|
</div>
|
|
189
217
|
<div class="form-actions">
|
|
@@ -206,9 +234,9 @@
|
|
|
206
234
|
</div>
|
|
207
235
|
<div class="modal-body update-modal-body">
|
|
208
236
|
<div class="update-version-flow" aria-label="版本升级">
|
|
209
|
-
<span><small>当前版本</small><strong id="updateCurrentVersion">--</strong></span>
|
|
210
|
-
<
|
|
211
|
-
<span class="is-latest"><small>最新版本</small><strong id="updateLatestVersion">--</strong></span>
|
|
237
|
+
<span class="update-version-item"><small>当前版本</small><strong id="updateCurrentVersion">--</strong></span>
|
|
238
|
+
<span class="update-version-arrow" aria-hidden="true"><i data-lucide="arrow-right"></i></span>
|
|
239
|
+
<span class="update-version-item is-latest"><small>最新版本</small><strong id="updateLatestVersion">--</strong></span>
|
|
212
240
|
</div>
|
|
213
241
|
<p class="update-description">更新期间微信连接会短暂中断,完成后服务将自动重启并恢复连接。</p>
|
|
214
242
|
<div class="update-progress" id="updateProgress" aria-live="polite" hidden>
|
package/dist/web/styles.css
CHANGED
|
@@ -165,7 +165,7 @@ h2 { margin: 0; font-size: 19px; line-height: 1.3; }
|
|
|
165
165
|
.chat-header { min-height: 82px; padding: 17px 20px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
|
166
166
|
.chat-header h2 { max-width: 620px; overflow: hidden; font-size: 17px; text-overflow: ellipsis; white-space: nowrap; }
|
|
167
167
|
.chat-context { max-width: 720px; margin: 5px 0 0; overflow: hidden; color: var(--ink-soft); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
|
168
|
-
.chat-runtime-toolbar { min-height: 54px; padding: 9px 20px; border-bottom: 1px solid var(--line); background: #f7f9f7; display: grid; grid-template-columns: auto minmax(
|
|
168
|
+
.chat-runtime-toolbar { min-height: 54px; padding: 9px 20px; border-bottom: 1px solid var(--line); background: #f7f9f7; display: grid; grid-template-columns: auto minmax(170px, 1fr) minmax(150px, .72fr) minmax(130px, .55fr); align-items: center; gap: 12px; }
|
|
169
169
|
.chat-runtime-title { color: var(--ink-soft); display: inline-flex; align-items: center; gap: 6px; font-size: 9px; font-weight: 800; letter-spacing: .04em; white-space: nowrap; }
|
|
170
170
|
.chat-runtime-title svg { width: 14px; height: 14px; color: var(--green-dark); }
|
|
171
171
|
.chat-runtime-toolbar label { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 7px; }
|
|
@@ -176,6 +176,16 @@ h2 { margin: 0; font-size: 19px; line-height: 1.3; }
|
|
|
176
176
|
.chat-message.is-user { margin-left: auto; justify-items: end; }
|
|
177
177
|
.chat-message.is-assistant { margin-right: auto; justify-items: start; }
|
|
178
178
|
.chat-message.has-attachments { width: min(78%, 720px); }
|
|
179
|
+
.chat-progress-group { width: min(78%, 720px); margin: 0 0 22px; color: var(--ink-soft); }
|
|
180
|
+
.chat-progress-group summary { min-height: 32px; padding: 0 3px; list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 14px; cursor: pointer; font-size: 10px; font-weight: 700; }
|
|
181
|
+
.chat-progress-group summary::-webkit-details-marker { display: none; }
|
|
182
|
+
.chat-progress-group summary:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; }
|
|
183
|
+
.progress-summary-title { color: var(--ink); display: inline-flex; align-items: center; gap: 6px; }
|
|
184
|
+
.progress-summary-title svg { width: 14px; height: 14px; color: var(--green-dark); transition: transform 160ms ease; }
|
|
185
|
+
.chat-progress-group[open] .progress-summary-title svg { transform: rotate(90deg); }
|
|
186
|
+
.progress-list { margin: 5px 0 0 8px; padding: 8px 12px 8px 24px; border-left: 2px solid #b9c9c0; font-size: 11px; line-height: 1.6; }
|
|
187
|
+
.progress-list li + li { margin-top: 7px; }
|
|
188
|
+
.progress-list p { margin: 0; }
|
|
179
189
|
.message-meta { padding: 0 3px; color: var(--ink-soft); display: flex; align-items: center; gap: 8px; font-size: 9px; font-weight: 750; }
|
|
180
190
|
.message-meta time { font-weight: 550; }
|
|
181
191
|
.message-bubble { max-width: 100%; padding: 13px 15px; border: 1px solid var(--line); border-radius: 7px; background: white; box-shadow: 0 4px 14px rgb(23 34 29 / 6%); font-size: 13px; line-height: 1.65; overflow-wrap: anywhere; }
|
|
@@ -248,8 +258,21 @@ h2 { margin: 0; font-size: 19px; line-height: 1.3; }
|
|
|
248
258
|
.settings-form { border-top: 1px solid var(--ink); }
|
|
249
259
|
.settings-group { padding: 26px 0 30px; border-bottom: 1px solid var(--line); display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 30px; }
|
|
250
260
|
.settings-group h2 { font-size: 14px; }
|
|
251
|
-
.settings-group > label, .field-grid { width: 100%; max-width: 720px; grid-column: 2; }
|
|
261
|
+
.settings-group > label, .field-grid, .settings-update { width: 100%; max-width: 720px; grid-column: 2; }
|
|
252
262
|
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
|
263
|
+
.settings-update { min-height: 44px; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
|
264
|
+
.settings-update-version { min-width: 0; display: grid; gap: 4px; }
|
|
265
|
+
.settings-update-version small { color: var(--ink-soft); font-size: 9px; font-weight: 700; }
|
|
266
|
+
.settings-update-version strong { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 14px; }
|
|
267
|
+
.button.is-loading svg { animation: spin .8s linear infinite; }
|
|
268
|
+
.toggle-field { min-height: 44px; padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius); background: #f8faf8; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
|
269
|
+
.toggle-control { width: 38px; height: 22px; position: relative; flex: 0 0 38px; }
|
|
270
|
+
.toggle-control input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
271
|
+
.toggle-control > span { position: absolute; inset: 0; border: 1px solid #aeb8b2; border-radius: 11px; background: #dfe4e1; transition: background-color .16s ease, border-color .16s ease; }
|
|
272
|
+
.toggle-control > span::after { content: ""; width: 16px; height: 16px; position: absolute; left: 2px; top: 2px; border-radius: 50%; background: white; box-shadow: 0 1px 3px rgb(23 34 29 / 24%); transition: transform .16s ease; }
|
|
273
|
+
.toggle-control input:checked + span { border-color: var(--green-dark); background: var(--green); }
|
|
274
|
+
.toggle-control input:checked + span::after { transform: translateX(16px); }
|
|
275
|
+
.toggle-control input:focus-visible + span { outline: 3px solid color-mix(in srgb, var(--focus) 30%, transparent); outline-offset: 2px; }
|
|
253
276
|
label { min-width: 0; display: grid; gap: 8px; color: var(--ink-soft); font-size: 11px; font-weight: 700; }
|
|
254
277
|
.field-hint { color: var(--ink-soft); font-size: 10px; font-weight: 500; line-height: 1.5; }
|
|
255
278
|
.effective-setting { min-width: 0; display: flex; align-items: center; gap: 7px; }
|
|
@@ -273,11 +296,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--focus); outline:
|
|
|
273
296
|
.update-modal-icon { width: 42px; height: 42px; flex: 0 0 42px; border-radius: var(--radius); color: var(--green-dark); background: var(--green-soft); display: grid; place-items: center; }
|
|
274
297
|
.update-modal-icon svg { width: 21px; height: 21px; }
|
|
275
298
|
.update-modal-body { display: grid; gap: 17px; }
|
|
276
|
-
.update-version-flow { min-height: 86px; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--canvas); display: grid; grid-template-columns: minmax(0, 1fr)
|
|
277
|
-
.update-version-
|
|
299
|
+
.update-version-flow { min-height: 86px; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--canvas); display: grid; grid-template-columns: minmax(0, 1fr) 54px minmax(0, 1fr); align-items: center; gap: 12px; }
|
|
300
|
+
.update-version-item { min-width: 0; }
|
|
278
301
|
.update-version-flow small { margin-bottom: 5px; color: var(--ink-soft); font-size: 9px; font-weight: 750; display: block; }
|
|
279
302
|
.update-version-flow strong { overflow: hidden; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
|
280
|
-
.update-version-
|
|
303
|
+
.update-version-arrow { width: 100%; color: #7e8b84; display: flex; align-items: center; gap: 5px; }
|
|
304
|
+
.update-version-arrow::before { content: ""; height: 1px; flex: 1; background: #c5cec9; }
|
|
305
|
+
.update-version-arrow svg { width: 15px; height: 15px; flex: 0 0 15px; stroke-width: 1.8; }
|
|
306
|
+
.update-version-flow .is-latest { text-align: right; }
|
|
281
307
|
.update-version-flow .is-latest strong { color: var(--green-dark); }
|
|
282
308
|
.update-description { margin: 0; color: var(--ink-soft); font-size: 11px; line-height: 1.65; }
|
|
283
309
|
.update-progress { min-height: 64px; padding: 12px 14px; border: 1px solid #b9ddc7; border-radius: var(--radius); background: var(--green-soft); display: flex; align-items: center; gap: 13px; }
|
|
@@ -365,7 +391,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--focus); outline:
|
|
|
365
391
|
.composer-files { padding-left: 45px; }
|
|
366
392
|
.composer-hint { margin-left: 45px; }
|
|
367
393
|
.settings-group { grid-template-columns: 1fr; gap: 16px; }
|
|
368
|
-
.settings-group > label, .field-grid { grid-column: 1; }
|
|
394
|
+
.settings-group > label, .field-grid, .settings-update { grid-column: 1; }
|
|
369
395
|
.field-grid { grid-template-columns: 1fr; }
|
|
370
396
|
.qr-stage { min-height: 342px; padding-inline: 16px; }
|
|
371
397
|
.qr-frame { width: min(280px, calc(100vw - 72px)); height: min(280px, calc(100vw - 72px)); }
|