@bolloon/bolloon-agent 0.3.0 → 0.3.3
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/agents/parse-tool-call.js +36 -0
- package/dist/agents/pi-sdk-tools.js +19 -7
- package/dist/cli/loading-tui.js +78 -11
- package/dist/documents/reader.js +3 -0
- package/dist/index.js +31 -2
- package/dist/judgeness/auto-add.js +145 -0
- package/dist/judgeness/protocol.js +214 -0
- package/dist/judgeness/rank.js +78 -0
- package/dist/judgeness/reflect.js +93 -0
- package/dist/judgeness/store.js +481 -0
- package/dist/judgeness/types.js +19 -0
- package/dist/judgeness/visibility.js +118 -0
- package/dist/scripts/dedup-session-messages.js +68 -0
- package/dist/web/client-hearth.js +67 -0
- package/dist/web/client.js +351 -41
- package/dist/web/routes-hearth.js +371 -0
- package/dist/web/server.js +356 -10
- package/dist/web/style.css +27 -0
- package/dist/web/ui/message-renderer.js +13 -2
- package/dist/web/ui/step-timeline.js +17 -0
- package/dist/web/util/dual-mode.js +87 -0
- package/package.json +2 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* client-hearth.ts — judgeness 前端模块 (占位实现)
|
|
3
|
+
*
|
|
4
|
+
* 防御期: 接 routes-hearth.ts, 渲染最小骨架 UI (My Hearth / Discover / Visit).
|
|
5
|
+
* 真实样式与组件待相持期接入 bolloon 现有 ui 设计 (HIG 等参考).
|
|
6
|
+
*
|
|
7
|
+
* 这个文件会被 esbuild 打包进 dist/web/client.js (与 src/web/client.ts 一致链路).
|
|
8
|
+
*
|
|
9
|
+
* 部署约定: 本文件 import 的 url 都是 '/api/hearth/*', 与 server.ts 端口 54188 对齐.
|
|
10
|
+
*/
|
|
11
|
+
// 默认接受 JSON-LD (agent 头等公民); 人类用 ?view=human 切
|
|
12
|
+
async function fetchHearth(path, opts = {}) {
|
|
13
|
+
const res = await fetch(path, { headers: { Accept: opts.json ? 'application/ld+json' : '*/*' } });
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
throw new Error(`hearth fetch ${path} -> ${res.status}`);
|
|
16
|
+
return await res.json();
|
|
17
|
+
}
|
|
18
|
+
// 三视图 stub: 由主 client.ts 路由到 #/hearth / #/hearth/discover / #/hearth/visit/<pk>
|
|
19
|
+
export async function renderMyHearth(root) {
|
|
20
|
+
root.innerHTML = '<h1>My Hearth</h1><p>Loading…</p>';
|
|
21
|
+
try {
|
|
22
|
+
const data = await fetchHearth('/api/hearth');
|
|
23
|
+
root.innerHTML = `
|
|
24
|
+
<h1>My Hearth</h1>
|
|
25
|
+
<dl>
|
|
26
|
+
<dt>Service</dt><dd>${data.service} v${data.version}</dd>
|
|
27
|
+
<dt>Root</dt><dd><code>${data.rootPath}</code></dd>
|
|
28
|
+
<dt>Description count</dt><dd>${data.descriptionCount}</dd>
|
|
29
|
+
<dt>Visibility channels</dt><dd>${data.visibilityChannels}</dd>
|
|
30
|
+
<dt>Allowlist count</dt><dd>${data.allowlistCount}</dd>
|
|
31
|
+
<dt>Defense mode</dt><dd>${data.defenseMode ? 'ON (write APIs disabled)' : 'OFF (反攻期)'}</dd>
|
|
32
|
+
</dl>
|
|
33
|
+
<p>视图: <a href="#/hearth/discover">Discover</a> · <a href="#/hearth/visit/__self__">Visit self</a></p>`;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
root.innerHTML = `<h1>My Hearth</h1><p style="color:red">Error: ${e.message}</p>`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function renderDiscover(root) {
|
|
40
|
+
root.innerHTML = '<h1>Discover</h1><p>Searching…</p>';
|
|
41
|
+
try {
|
|
42
|
+
const data = await fetchHearth('/api/hearth/discover');
|
|
43
|
+
// data 可能是 JSON-LD @graph 或 list
|
|
44
|
+
const items = Array.isArray(data) ? data : (data['@graph'] ?? []);
|
|
45
|
+
const cards = items.slice(0, 50).map((it) => {
|
|
46
|
+
const id = it['@id'] ?? it.descriptionId ?? '?';
|
|
47
|
+
const vis = it.visibility ?? '?';
|
|
48
|
+
const state = it.openState ?? '?';
|
|
49
|
+
return `<li><code>${id}</code> · <span>${vis}</span> · <span>${state}</span></li>`;
|
|
50
|
+
}).join('');
|
|
51
|
+
root.innerHTML = `<h1>Discover</h1><ul>${cards || '<li>(no results)</li>'}</ul>`;
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
root.innerHTML = `<h1>Discover</h1><p style="color:red">Error: ${e.message}</p>`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function renderVisit(root, _pubkey) {
|
|
58
|
+
root.innerHTML = `<h1>Visit</h1><p>Visit 视图待相持期实现 (plan §C2).</p>`;
|
|
59
|
+
}
|
|
60
|
+
// 注册 router hook — 主 client.ts 在启动时调用
|
|
61
|
+
export function registerHearthRoutes(router, getRoot) {
|
|
62
|
+
router.on('/hearth', () => void renderMyHearth(getRoot()));
|
|
63
|
+
router.on('/hearth/discover', () => void renderDiscover(getRoot()));
|
|
64
|
+
router.on('/hearth/visit/:pk', (_p, params) => {
|
|
65
|
+
void renderVisit(getRoot(), params['pk'] ?? '__self__');
|
|
66
|
+
});
|
|
67
|
+
}
|
package/dist/web/client.js
CHANGED
|
@@ -125,6 +125,13 @@
|
|
|
125
125
|
node.appendChild(marker);
|
|
126
126
|
node.appendChild(label2);
|
|
127
127
|
node.appendChild(args);
|
|
128
|
+
const errContainer = document.createElement("div");
|
|
129
|
+
errContainer.className = "step-timeline-error-wrap";
|
|
130
|
+
errContainer.style.display = "none";
|
|
131
|
+
const errEl = document.createElement("span");
|
|
132
|
+
errEl.className = "step-timeline-error";
|
|
133
|
+
errContainer.appendChild(errEl);
|
|
134
|
+
node.appendChild(errContainer);
|
|
128
135
|
if (existing[htmlIdx] && existing[htmlIdx] !== node) {
|
|
129
136
|
listEl.replaceChild(node, existing[htmlIdx]);
|
|
130
137
|
} else {
|
|
@@ -142,6 +149,16 @@
|
|
|
142
149
|
argsEl.textContent = argStr;
|
|
143
150
|
argsEl.style.display = argStr ? "" : "none";
|
|
144
151
|
}
|
|
152
|
+
const errWrap = node.querySelector(".step-timeline-error-wrap");
|
|
153
|
+
const errEl2 = node.querySelector(".step-timeline-error");
|
|
154
|
+
if (step.status === "error" && step.error && errWrap && errEl2) {
|
|
155
|
+
errEl2.textContent = String(step.error);
|
|
156
|
+
errEl2.setAttribute("title", String(step.error));
|
|
157
|
+
errWrap.style.display = "";
|
|
158
|
+
} else if (errWrap && errEl2) {
|
|
159
|
+
errWrap.style.display = "none";
|
|
160
|
+
errEl2.textContent = "";
|
|
161
|
+
}
|
|
145
162
|
htmlIdx++;
|
|
146
163
|
}
|
|
147
164
|
while (listEl.children.length > htmlIdx) {
|
|
@@ -356,6 +373,31 @@
|
|
|
356
373
|
function parseToolCall(content, ctx) {
|
|
357
374
|
if (!content) return null;
|
|
358
375
|
const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, "");
|
|
376
|
+
try {
|
|
377
|
+
const result = /* @__PURE__ */ function _diagProbe() {
|
|
378
|
+
return null;
|
|
379
|
+
}();
|
|
380
|
+
const probe = function _doParse() {
|
|
381
|
+
const m1 = strippedContent.match(/<invoke\s+name=["']([\w]+)["']/);
|
|
382
|
+
if (m1) {
|
|
383
|
+
return { name: m1[1], args: { __probe_invoketag: "1" } };
|
|
384
|
+
}
|
|
385
|
+
const m2 = strippedContent.match(/<function_calls>[\s\S]*?<invoke\s+name=["']([\w]+)["']/);
|
|
386
|
+
if (m2) {
|
|
387
|
+
return { name: m2[1], args: { __probe_function_calls: "1" } };
|
|
388
|
+
}
|
|
389
|
+
const m3 = strippedContent.match(/\{[\s\S]*?"name"\s*:\s*["']([\w]+)["']/);
|
|
390
|
+
if (m3) {
|
|
391
|
+
return { name: m3[1], args: { __probe_json_name: "1" } };
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}();
|
|
395
|
+
console.warn(
|
|
396
|
+
"[parseToolCall diag] rawLen=" + content.length + " strippedLen=" + strippedContent.length + " probeName=" + (probe?.name ?? "null") + " rawHead=" + JSON.stringify(content.slice(0, 500))
|
|
397
|
+
);
|
|
398
|
+
} catch (diagErr) {
|
|
399
|
+
console.warn("[parseToolCall diag] diag-self-failed:", String(diagErr));
|
|
400
|
+
}
|
|
359
401
|
const jsonPatterns = [
|
|
360
402
|
// markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
|
|
361
403
|
/(?:```(?:json|json5)?\s*\n?)?\{[\s\S]*?"name"\s*:\s*["'](\w+)["']\s*,\s*["']?(?:arguments|input)["']?\s*:\s*(\{[\s\S]*?\})\s*\}/
|
|
@@ -797,7 +839,7 @@
|
|
|
797
839
|
"'": "'"
|
|
798
840
|
})[c]);
|
|
799
841
|
}
|
|
800
|
-
function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
|
|
842
|
+
function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }, timestamp) {
|
|
801
843
|
const messagesEl2 = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
|
|
802
844
|
const messagesContainers2 = ctx.messagesContainers || /* @__PURE__ */ new Map();
|
|
803
845
|
const currentChannelId2 = ctx.currentChannelId;
|
|
@@ -866,9 +908,20 @@
|
|
|
866
908
|
return;
|
|
867
909
|
}
|
|
868
910
|
const rawContent = segments.filter((s) => s.type === "text" || s.type === "final").map((s) => s.content || "").join("\n");
|
|
911
|
+
let timeLabel = "";
|
|
912
|
+
try {
|
|
913
|
+
if (timestamp !== void 0 && timestamp !== null) {
|
|
914
|
+
const d = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
|
915
|
+
if (!isNaN(d.getTime())) {
|
|
916
|
+
timeLabel = d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
if (!timeLabel) timeLabel = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
|
869
922
|
const time = document.createElement("div");
|
|
870
923
|
time.className = "time";
|
|
871
|
-
time.textContent =
|
|
924
|
+
time.textContent = timeLabel;
|
|
872
925
|
if (type === "ai") {
|
|
873
926
|
div.appendChild(buildMessageActions(div, rawContent, ctx));
|
|
874
927
|
}
|
|
@@ -2168,16 +2221,27 @@
|
|
|
2168
2221
|
const session = await res.json();
|
|
2169
2222
|
const msgs = session.messages || [];
|
|
2170
2223
|
if (msgs.length > 0) {
|
|
2224
|
+
let lastType = null;
|
|
2225
|
+
let lastContent = null;
|
|
2226
|
+
const dedupedMsgs = msgs.filter((m) => {
|
|
2227
|
+
const same = lastType === m.type && lastContent === m.content;
|
|
2228
|
+
lastType = m.type;
|
|
2229
|
+
lastContent = m.content;
|
|
2230
|
+
return !same;
|
|
2231
|
+
});
|
|
2171
2232
|
const frag = document.createDocumentFragment();
|
|
2172
2233
|
const tmpContainer = document.createElement("div");
|
|
2173
2234
|
tmpContainer.style.display = "none";
|
|
2174
|
-
for (const msg of
|
|
2175
|
-
addMessage2(msg.content, msg.type, false, tmpContainer, msg.metadata?.usedJudgmentIds || []);
|
|
2235
|
+
for (const msg of dedupedMsgs) {
|
|
2236
|
+
addMessage2(msg.content, msg.type, false, tmpContainer, msg.metadata?.usedJudgmentIds || [], msg.timestamp);
|
|
2176
2237
|
}
|
|
2177
2238
|
while (tmpContainer.firstChild) {
|
|
2178
2239
|
frag.appendChild(tmpContainer.firstChild);
|
|
2179
2240
|
}
|
|
2180
2241
|
container.appendChild(frag);
|
|
2242
|
+
if (dedupedMsgs.length !== msgs.length) {
|
|
2243
|
+
console.log(`[loadSession] \u53BB\u91CD ${msgs.length - dedupedMsgs.length} \u6761\u76F8\u90BB\u91CD\u590D\u6D88\u606F (${msgs.length} \u2192 ${dedupedMsgs.length})`);
|
|
2244
|
+
}
|
|
2181
2245
|
} else {
|
|
2182
2246
|
addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
|
|
2183
2247
|
}
|
|
@@ -2202,9 +2266,21 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2202
2266
|
const session = await res.json();
|
|
2203
2267
|
container.innerHTML = "";
|
|
2204
2268
|
if (session.messages && session.messages.length > 0) {
|
|
2205
|
-
session.messages
|
|
2206
|
-
|
|
2269
|
+
const rawMsgs = session.messages;
|
|
2270
|
+
let lastType = null;
|
|
2271
|
+
let lastContent = null;
|
|
2272
|
+
const deduped = rawMsgs.filter((m) => {
|
|
2273
|
+
const same = lastType === m.type && lastContent === m.content;
|
|
2274
|
+
lastType = m.type;
|
|
2275
|
+
lastContent = m.content;
|
|
2276
|
+
return !same;
|
|
2277
|
+
});
|
|
2278
|
+
deduped.forEach((msg) => {
|
|
2279
|
+
addMessage2(msg.content, msg.type, false, container, msg.metadata?.usedJudgmentIds || [], msg.timestamp);
|
|
2207
2280
|
});
|
|
2281
|
+
if (deduped.length !== rawMsgs.length) {
|
|
2282
|
+
console.log(`[loadSession-v2] \u53BB\u91CD ${rawMsgs.length - deduped.length} \u6761`);
|
|
2283
|
+
}
|
|
2208
2284
|
} else {
|
|
2209
2285
|
addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
|
|
2210
2286
|
}
|
|
@@ -2214,8 +2290,8 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2214
2290
|
addMessage2("\u4F60\u597D\uFF01\u6211\u662F Bolloon Agent\u3002\u6709\u4EC0\u4E48\u6211\u53EF\u4EE5\u5E2E\u4F60\u7684\u5417\uFF1F", "ai", false, container);
|
|
2215
2291
|
}
|
|
2216
2292
|
}
|
|
2217
|
-
function addMessage2(content, type, save = true, container, usedJudgmentIds = []) {
|
|
2218
|
-
return MR_addMessage(content, type, save, container, usedJudgmentIds, getRendererCtx());
|
|
2293
|
+
function addMessage2(content, type, save = true, container, usedJudgmentIds = [], timestamp = void 0) {
|
|
2294
|
+
return MR_addMessage(content, type, save, container, usedJudgmentIds, getRendererCtx(), timestamp);
|
|
2219
2295
|
}
|
|
2220
2296
|
function finalizeTimelineAsMessage2() {
|
|
2221
2297
|
return MR_finalizeTimelineAsMessage(getRendererCtx());
|
|
@@ -2393,13 +2469,13 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2393
2469
|
const container = messagesContainers.get(targetChannelId) || messagesEl;
|
|
2394
2470
|
if (msg.type === "ai") {
|
|
2395
2471
|
if (!MR_hasStreamingText()) {
|
|
2396
|
-
addMessage2(msg.content, "ai", true, container, lastUsedJudgmentIds || []);
|
|
2472
|
+
addMessage2(msg.content, "ai", true, container, lastUsedJudgmentIds || [], msg.timestamp);
|
|
2397
2473
|
} else {
|
|
2398
2474
|
MR_replaceStreamingText?.(msg.content);
|
|
2399
2475
|
MR_finalizeTimelineAsMessage(getRendererCtx());
|
|
2400
2476
|
}
|
|
2401
2477
|
} else if (msg.type === "user") {
|
|
2402
|
-
if (msg.source === "remote"
|
|
2478
|
+
if (msg.source === "remote") {
|
|
2403
2479
|
addMessage2(msg.content, "user", true, container);
|
|
2404
2480
|
}
|
|
2405
2481
|
}
|
|
@@ -2484,22 +2560,19 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2484
2560
|
addMessage2(data.content, "user", true, container);
|
|
2485
2561
|
}
|
|
2486
2562
|
} else if (data.type === "ai") {
|
|
2563
|
+
const allPreviews = container.querySelectorAll(".message-ai.preview");
|
|
2564
|
+
allPreviews.forEach((el) => el.remove());
|
|
2565
|
+
currentPreviewBubble = null;
|
|
2487
2566
|
addMessage2(data.content || "", "ai", true, container, lastUsedJudgmentIds || []);
|
|
2488
|
-
if (currentPreviewBubble) {
|
|
2489
|
-
currentPreviewBubble.remove();
|
|
2490
|
-
currentPreviewBubble = null;
|
|
2491
|
-
}
|
|
2492
2567
|
} else if (data.type === "reply-preview") {
|
|
2493
2568
|
const previewContent = data.content || "";
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
currentPreviewBubble
|
|
2501
|
-
const msgs = container.querySelectorAll(".message-ai.preview");
|
|
2502
|
-
currentPreviewBubble = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
2569
|
+
const oldPreviews = container.querySelectorAll(".message-ai.preview");
|
|
2570
|
+
oldPreviews.forEach((el) => el.remove());
|
|
2571
|
+
addMessage2(previewContent, "ai", false, container, []);
|
|
2572
|
+
const newPreview = container.querySelector(".message-ai:not(.preview):last-of-type") || container.lastElementChild;
|
|
2573
|
+
if (newPreview) {
|
|
2574
|
+
newPreview.classList.add("preview");
|
|
2575
|
+
currentPreviewBubble = newPreview;
|
|
2503
2576
|
}
|
|
2504
2577
|
} else if (data.type === "stream") {
|
|
2505
2578
|
if (false) handleStreamTokenEvent(data);
|
|
@@ -2573,6 +2646,10 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2573
2646
|
persistLastMessageToServer("user", text);
|
|
2574
2647
|
const channel = channels.find((c) => c.id === currentChannelId);
|
|
2575
2648
|
const channelDid = channel?.did || "";
|
|
2649
|
+
const attachmentsForSend = pendingAttachments.slice();
|
|
2650
|
+
pendingAttachments = [];
|
|
2651
|
+
const chipsEl = document.getElementById("input-attachment-chips");
|
|
2652
|
+
if (chipsEl) chipsEl.innerHTML = "";
|
|
2576
2653
|
console.log("[\u53D1\u9001\u6D88\u606F] \u9891\u9053 DID:", channelDid);
|
|
2577
2654
|
try {
|
|
2578
2655
|
const res = await fetch("/message", {
|
|
@@ -2581,7 +2658,9 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2581
2658
|
body: JSON.stringify({
|
|
2582
2659
|
text,
|
|
2583
2660
|
channelId: currentChannelId,
|
|
2584
|
-
channelDid
|
|
2661
|
+
channelDid,
|
|
2662
|
+
attachments: attachmentsForSend
|
|
2663
|
+
// 后端解析为 LLM contextHint
|
|
2585
2664
|
})
|
|
2586
2665
|
});
|
|
2587
2666
|
if (!res.ok) {
|
|
@@ -2940,11 +3019,103 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2940
3019
|
}, true);
|
|
2941
3020
|
}
|
|
2942
3021
|
var inputArea = document.querySelector(".input-area");
|
|
3022
|
+
var pendingAttachments = [];
|
|
3023
|
+
function fileToBase64Local(file) {
|
|
3024
|
+
return new Promise((resolve2, reject) => {
|
|
3025
|
+
const r = new FileReader();
|
|
3026
|
+
r.onerror = () => reject(r.error || new Error("FileReader error"));
|
|
3027
|
+
r.onload = () => {
|
|
3028
|
+
const s = String(r.result || "");
|
|
3029
|
+
const idx = s.indexOf(",");
|
|
3030
|
+
resolve2(idx >= 0 ? s.slice(idx + 1) : s);
|
|
3031
|
+
};
|
|
3032
|
+
r.readAsDataURL(file);
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
function appendAttachmentChip(name) {
|
|
3036
|
+
let chipsEl = document.getElementById("input-attachment-chips");
|
|
3037
|
+
if (!chipsEl) {
|
|
3038
|
+
chipsEl = document.createElement("div");
|
|
3039
|
+
chipsEl.id = "input-attachment-chips";
|
|
3040
|
+
chipsEl.style.cssText = "padding:8px 12px 4px;display:flex;gap:8px;flex-wrap:wrap;font-size:13px;";
|
|
3041
|
+
if (inputArea && inputArea.parentNode) inputArea.parentNode.insertBefore(chipsEl, inputArea);
|
|
3042
|
+
}
|
|
3043
|
+
const chip = document.createElement("span");
|
|
3044
|
+
chip.className = "attach-chip";
|
|
3045
|
+
chip.style.cssText = "background:linear-gradient(135deg,#dbeafe,#e0e7ff);border:1px solid #6366f1;border-radius:14px;padding:5px 12px;display:inline-flex;align-items:center;gap:6px;color:#3730a3;font-weight:500;box-shadow:0 1px 3px rgba(99,102,241,0.18);animation:attach-pop-in 0.25s ease-out;";
|
|
3046
|
+
chip.textContent = `\u{1F4CE} ${name}`;
|
|
3047
|
+
chip.title = "\u9644\u4EF6\u5DF2\u52A0\u5165\u672C\u6761\u6D88\u606F";
|
|
3048
|
+
chipsEl.appendChild(chip);
|
|
3049
|
+
return chipsEl;
|
|
3050
|
+
}
|
|
3051
|
+
function ensureFileDropOverlay() {
|
|
3052
|
+
let overlay = document.getElementById("file-drop-overlay");
|
|
3053
|
+
if (overlay) return overlay;
|
|
3054
|
+
overlay = document.createElement("div");
|
|
3055
|
+
overlay.id = "file-drop-overlay";
|
|
3056
|
+
overlay.style.cssText = "position:fixed;inset:0;background:rgba(99,102,241,0.18);backdrop-filter:blur(2px);z-index:9998;display:none;align-items:center;justify-content:center;pointer-events:none;transition:opacity 0.18s;";
|
|
3057
|
+
overlay.innerHTML = `
|
|
3058
|
+
<div style="
|
|
3059
|
+
background:white;
|
|
3060
|
+
border:3px dashed #6366f1;
|
|
3061
|
+
border-radius:18px;
|
|
3062
|
+
padding:48px 64px;
|
|
3063
|
+
box-shadow:0 24px 60px rgba(99,102,241,0.25);
|
|
3064
|
+
color:#4338ca;
|
|
3065
|
+
text-align:center;
|
|
3066
|
+
max-width:520px;
|
|
3067
|
+
animation:attach-pulse 1.4s ease-in-out infinite;
|
|
3068
|
+
">
|
|
3069
|
+
<div style="font-size:64px;line-height:1;margin-bottom:16px;">\u{1F4E5}</div>
|
|
3070
|
+
<div style="font-size:22px;font-weight:600;margin-bottom:8px;">\u677E\u5F00\u4E0A\u4F20\u5230 Bolloon</div>
|
|
3071
|
+
<div style="font-size:14px;color:#64748b;">\u62D6\u5165\u6587\u4EF6\u7ACB\u5373\u4F5C\u4E3A\u9644\u4EF6\u53D1\u7ED9 AI, \u6700\u5927 10MB/\u6587\u4EF6</div>
|
|
3072
|
+
</div>
|
|
3073
|
+
`;
|
|
3074
|
+
document.body.appendChild(overlay);
|
|
3075
|
+
return overlay;
|
|
3076
|
+
}
|
|
3077
|
+
function ensureAttachStyles() {
|
|
3078
|
+
if (document.getElementById("attach-style-tag")) return;
|
|
3079
|
+
const s = document.createElement("style");
|
|
3080
|
+
s.id = "attach-style-tag";
|
|
3081
|
+
s.textContent = `
|
|
3082
|
+
@keyframes attach-pop-in {
|
|
3083
|
+
0% { transform: scale(0.6); opacity: 0; }
|
|
3084
|
+
60% { transform: scale(1.08); opacity: 1; }
|
|
3085
|
+
100% { transform: scale(1); opacity: 1; }
|
|
3086
|
+
}
|
|
3087
|
+
@keyframes attach-pulse {
|
|
3088
|
+
0%, 100% { transform: scale(1); box-shadow: 0 24px 60px rgba(99,102,241,0.25); }
|
|
3089
|
+
50% { transform: scale(1.04); box-shadow: 0 30px 80px rgba(99,102,241,0.4); }
|
|
3090
|
+
}
|
|
3091
|
+
@keyframes attach-success-flash {
|
|
3092
|
+
0% { background: linear-gradient(135deg,#dcfce7,#bbf7d0); }
|
|
3093
|
+
100% { background: linear-gradient(135deg,#dbeafe,#e0e7ff); }
|
|
3094
|
+
}
|
|
3095
|
+
.drop-target {
|
|
3096
|
+
outline: 3px dashed #6366f1 !important;
|
|
3097
|
+
outline-offset: 4px !important;
|
|
3098
|
+
background-color: rgba(99,102,241,0.08) !important;
|
|
3099
|
+
}
|
|
3100
|
+
`;
|
|
3101
|
+
document.head.appendChild(s);
|
|
3102
|
+
}
|
|
3103
|
+
ensureAttachStyles();
|
|
3104
|
+
var _dropStyle = document.createElement("style");
|
|
3105
|
+
_dropStyle.textContent = `
|
|
3106
|
+
body.file-drag-active { outline: 4px dashed #6366f1; outline-offset: -8px; }
|
|
3107
|
+
`;
|
|
3108
|
+
document.head.appendChild(_dropStyle);
|
|
2943
3109
|
if (input && inputArea) {
|
|
2944
3110
|
const onDragOver = (e) => {
|
|
2945
|
-
if (e.dataTransfer
|
|
3111
|
+
if (!e.dataTransfer) return;
|
|
3112
|
+
const types = Array.from(e.dataTransfer.types || []);
|
|
3113
|
+
const hasJudgment = types.includes("application/x-bolloon-judgment");
|
|
3114
|
+
const hasFiles = types.includes("Files");
|
|
3115
|
+
if (hasJudgment || hasFiles) {
|
|
2946
3116
|
e.preventDefault();
|
|
2947
|
-
e.
|
|
3117
|
+
e.stopPropagation();
|
|
3118
|
+
e.dataTransfer.dropEffect = hasFiles ? "copy" : "copy";
|
|
2948
3119
|
inputArea.classList.add("drop-target");
|
|
2949
3120
|
}
|
|
2950
3121
|
};
|
|
@@ -2953,27 +3124,166 @@ ${data.error || "channel not found"}`, "error");
|
|
|
2953
3124
|
inputArea.classList.remove("drop-target");
|
|
2954
3125
|
}
|
|
2955
3126
|
};
|
|
2956
|
-
const onDrop = (e) => {
|
|
3127
|
+
const onDrop = async (e) => {
|
|
2957
3128
|
inputArea.classList.remove("drop-target");
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
input.style.
|
|
2970
|
-
|
|
2971
|
-
|
|
3129
|
+
if (!e.dataTransfer) return;
|
|
3130
|
+
const types = Array.from(e.dataTransfer.types || []);
|
|
3131
|
+
if (types.includes("application/x-bolloon-judgment")) {
|
|
3132
|
+
const raw = e.dataTransfer.getData("application/x-bolloon-judgment");
|
|
3133
|
+
if (!raw) return;
|
|
3134
|
+
e.preventDefault();
|
|
3135
|
+
try {
|
|
3136
|
+
const { id, decision } = JSON.parse(raw);
|
|
3137
|
+
const prefix = input.value.trim() ? input.value.trim() + "\n" : "";
|
|
3138
|
+
input.value = `${prefix}\u6309\u6211\u7684\u5224\u65AD #${id?.substring(0, 8) || ""} \u6267\u884C: ${decision}`;
|
|
3139
|
+
input.focus();
|
|
3140
|
+
input.style.transition = "box-shadow 0.3s";
|
|
3141
|
+
input.style.boxShadow = "0 0 0 2px #2563eb";
|
|
3142
|
+
setTimeout(() => {
|
|
3143
|
+
input.style.boxShadow = "";
|
|
3144
|
+
}, 800);
|
|
3145
|
+
} catch {
|
|
3146
|
+
}
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
if (types.includes("Files")) {
|
|
3150
|
+
e.preventDefault();
|
|
3151
|
+
const ov = document.getElementById("file-drop-overlay");
|
|
3152
|
+
if (ov) ov.style.display = "none";
|
|
3153
|
+
document.body.classList.remove("file-drag-active");
|
|
3154
|
+
const files = Array.from(e.dataTransfer.files || []);
|
|
3155
|
+
if (files.length === 0) return;
|
|
3156
|
+
const totalBytes = files.reduce((s, f) => s + f.size, 0);
|
|
3157
|
+
const fmtSize = (b) => b < 1024 ? `${b}B` : b < 1024 * 1024 ? `${(b / 1024).toFixed(1)}KB` : `${(b / 1024 / 1024).toFixed(2)}MB`;
|
|
3158
|
+
if (typeof showSimpleToast === "function") {
|
|
3159
|
+
showSimpleToast(`\u{1F4E5} \u6536\u5230 ${files.length} \u4E2A\u6587\u4EF6 (${fmtSize(totalBytes)}), \u4E0A\u4F20\u4E2D\u2026`);
|
|
3160
|
+
}
|
|
3161
|
+
const chipsEl = appendAttachmentChip(`\u23F3 \u4E0A\u4F20\u4E2D ${files.length} \u4E2A (${fmtSize(totalBytes)})\u2026`);
|
|
3162
|
+
inputArea.classList.add("drop-target");
|
|
3163
|
+
setTimeout(() => inputArea.classList.remove("drop-target"), 600);
|
|
3164
|
+
for (const file of files) {
|
|
3165
|
+
try {
|
|
3166
|
+
const progressChip = document.createElement("span");
|
|
3167
|
+
progressChip.style.cssText = "background:#fef3c7;color:#92400e;border:1px solid #fbbf24;border-radius:14px;padding:5px 12px;display:inline-flex;align-items:center;gap:6px;font-weight:500;animation:attach-pop-in 0.25s ease-out;";
|
|
3168
|
+
progressChip.innerHTML = `\u23F3 <strong>${file.name}</strong> \u8BFB\u53D6\u4E2D\u2026`;
|
|
3169
|
+
chipsEl.appendChild(progressChip);
|
|
3170
|
+
const dataB64 = await fileToBase64Local(file);
|
|
3171
|
+
progressChip.innerHTML = `\u23F3 <strong>${file.name}</strong> \u4E0A\u4F20\u4E2D (${fmtSize(file.size)})\u2026`;
|
|
3172
|
+
const res = await fetch("/api/attachments/upload", {
|
|
3173
|
+
method: "POST",
|
|
3174
|
+
headers: { "Content-Type": "application/json" },
|
|
3175
|
+
body: JSON.stringify({
|
|
3176
|
+
filename: file.name,
|
|
3177
|
+
mimeType: file.type || "application/octet-stream",
|
|
3178
|
+
content: dataB64
|
|
3179
|
+
})
|
|
3180
|
+
});
|
|
3181
|
+
if (!res.ok) {
|
|
3182
|
+
const errText = await res.text().catch(() => "");
|
|
3183
|
+
throw new Error(`HTTP ${res.status}: ${errText}`);
|
|
3184
|
+
}
|
|
3185
|
+
const out = await res.json();
|
|
3186
|
+
if (!out.ok) throw new Error(out.error || "upload failed");
|
|
3187
|
+
pendingAttachments.push({
|
|
3188
|
+
attachmentId: out.attachmentId,
|
|
3189
|
+
filename: out.filename,
|
|
3190
|
+
mimeType: out.mimeType,
|
|
3191
|
+
size: out.size,
|
|
3192
|
+
url: out.url
|
|
3193
|
+
});
|
|
3194
|
+
const prefix = input.value ? input.value + "\n" : "";
|
|
3195
|
+
input.value = `${prefix}\u{1F4CE} ${out.filename}`;
|
|
3196
|
+
input.focus();
|
|
3197
|
+
progressChip.style.background = "linear-gradient(135deg,#dcfce7,#bbf7d0)";
|
|
3198
|
+
progressChip.style.borderColor = "#16a34a";
|
|
3199
|
+
progressChip.style.color = "#15803d";
|
|
3200
|
+
progressChip.innerHTML = `\u2705 <strong>${out.filename}</strong> (${fmtSize(out.size)})`;
|
|
3201
|
+
input.style.transition = "box-shadow 0.4s ease, transform 0.2s ease";
|
|
3202
|
+
input.style.boxShadow = "0 0 0 3px #16a34a, 0 0 18px rgba(34,197,94,0.4)";
|
|
3203
|
+
input.style.transform = "scale(1.005)";
|
|
3204
|
+
setTimeout(() => {
|
|
3205
|
+
input.style.boxShadow = "0 0 0 2px #16a34a";
|
|
3206
|
+
input.style.transform = "";
|
|
3207
|
+
}, 200);
|
|
3208
|
+
setTimeout(() => {
|
|
3209
|
+
input.style.boxShadow = "";
|
|
3210
|
+
}, 1200);
|
|
3211
|
+
if (typeof showSimpleToast === "function") {
|
|
3212
|
+
showSimpleToast(`\u2705 \u9644\u4EF6 ${out.filename} (${fmtSize(out.size)}) \u5DF2\u4E0A\u4F20, \u7B49\u5F85\u53D1\u9001`);
|
|
3213
|
+
}
|
|
3214
|
+
} catch (err) {
|
|
3215
|
+
console.error("[drag-file] \u4E0A\u4F20\u5931\u8D25:", err);
|
|
3216
|
+
if (chipsEl) {
|
|
3217
|
+
const failChip = document.createElement("span");
|
|
3218
|
+
failChip.style.cssText = "background:#fee2e2;color:#b91c1c;border:1px solid #ef4444;border-radius:14px;padding:5px 12px;display:inline-flex;align-items:center;gap:6px;font-weight:500;animation:attach-pop-in 0.25s ease-out;";
|
|
3219
|
+
failChip.innerHTML = `\u274C <strong>${file.name}</strong>: ${err?.message || "\u4E0A\u4F20\u5931\u8D25"}`;
|
|
3220
|
+
chipsEl.appendChild(failChip);
|
|
3221
|
+
}
|
|
3222
|
+
if (typeof showSimpleToast === "function") {
|
|
3223
|
+
showSimpleToast(`\u274C ${file.name} \u4E0A\u4F20\u5931\u8D25: ${err?.message || ""}`);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
if (pendingAttachments.length > 0) {
|
|
3228
|
+
const ready = document.createElement("span");
|
|
3229
|
+
ready.style.cssText = "background:#f1f5f9;color:#0f172a;border:1px dashed #94a3b8;border-radius:12px;padding:4px 10px;display:inline-flex;align-items:center;gap:6px;font-size:12px;font-style:italic;";
|
|
3230
|
+
ready.innerHTML = `\u{1F4E8} \u5171 ${pendingAttachments.length} \u4E2A\u9644\u4EF6\u5F85\u53D1\u9001, \u6309 \u21A9 \u53D1\u9001`;
|
|
3231
|
+
chipsEl.appendChild(ready);
|
|
3232
|
+
}
|
|
3233
|
+
return;
|
|
2972
3234
|
}
|
|
2973
3235
|
};
|
|
2974
3236
|
inputArea.addEventListener("dragover", onDragOver);
|
|
2975
3237
|
inputArea.addEventListener("dragleave", onDragLeave);
|
|
2976
3238
|
inputArea.addEventListener("drop", onDrop);
|
|
3239
|
+
let pageDragDepth = 0;
|
|
3240
|
+
const onPageDragEnter = (e) => {
|
|
3241
|
+
if (!e.dataTransfer) return;
|
|
3242
|
+
const types = Array.from(e.dataTransfer.types || []);
|
|
3243
|
+
if (types.includes("Files")) {
|
|
3244
|
+
e.preventDefault();
|
|
3245
|
+
pageDragDepth++;
|
|
3246
|
+
const overlay = ensureFileDropOverlay();
|
|
3247
|
+
overlay.style.display = "flex";
|
|
3248
|
+
document.body.classList.add("file-drag-active");
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
const onPageDragOver = (e) => {
|
|
3252
|
+
if (!e.dataTransfer) return;
|
|
3253
|
+
const types = Array.from(e.dataTransfer.types || []);
|
|
3254
|
+
if (types.includes("Files")) {
|
|
3255
|
+
e.preventDefault();
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
const onPageDragLeave = (e) => {
|
|
3259
|
+
if (!e.dataTransfer) return;
|
|
3260
|
+
const types = Array.from(e.dataTransfer.types || []);
|
|
3261
|
+
if (types.includes("Files")) {
|
|
3262
|
+
pageDragDepth = Math.max(0, pageDragDepth - 1);
|
|
3263
|
+
if (pageDragDepth === 0) {
|
|
3264
|
+
const overlay = document.getElementById("file-drop-overlay");
|
|
3265
|
+
if (overlay) overlay.style.display = "none";
|
|
3266
|
+
document.body.classList.remove("file-drag-active");
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
3270
|
+
window.addEventListener("dragenter", onPageDragEnter);
|
|
3271
|
+
window.addEventListener("dragover", onPageDragOver);
|
|
3272
|
+
window.addEventListener("dragleave", onPageDragLeave);
|
|
3273
|
+
const onPageDrop = (e) => {
|
|
3274
|
+
if (!e.dataTransfer) return;
|
|
3275
|
+
const types = Array.from(e.dataTransfer.types || []);
|
|
3276
|
+
if (types.includes("Files") && e.target !== input && !inputArea?.contains(e.target)) {
|
|
3277
|
+
e.preventDefault();
|
|
3278
|
+
}
|
|
3279
|
+
if (types.includes("Files")) {
|
|
3280
|
+
pageDragDepth = 0;
|
|
3281
|
+
const overlay = document.getElementById("file-drop-overlay");
|
|
3282
|
+
if (overlay) overlay.style.display = "none";
|
|
3283
|
+
document.body.classList.remove("file-drag-active");
|
|
3284
|
+
}
|
|
3285
|
+
};
|
|
3286
|
+
window.addEventListener("drop", onPageDrop);
|
|
2977
3287
|
}
|
|
2978
3288
|
if (themeToggle) {
|
|
2979
3289
|
themeToggle.addEventListener("click", toggleTheme);
|