@bolloon/bolloon-agent 0.2.7 → 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/dist/agents/agent-manifest-protocol.js +52 -0
- package/dist/agents/peer-manifest-loader.js +210 -0
- package/dist/agents/pi-sdk.js +24 -41
- package/dist/agents/session-store.js +17 -3
- package/dist/agents/shell-guard.js +2 -2
- package/dist/agents/workflow-pivot-loop.js +12 -3
- package/dist/bootstrap/chat-archiver.js +276 -0
- package/dist/bootstrap/context-collector.js +6 -3
- package/dist/bootstrap/lifecycle-hooks.js +15 -1
- package/dist/bootstrap/memory-compressor.js +170 -0
- package/dist/bootstrap/persona-loader.js +94 -0
- package/dist/network/p2p-outbox.js +161 -0
- package/dist/network/peer-fs.js +420 -0
- package/dist/network/peer-resource-bridge.js +216 -0
- package/dist/web/client.js +29 -27
- package/dist/web/components/p2p/index.js +17 -5
- package/dist/web/components/p2p/p2p-modal.js +9 -2
- package/dist/web/server.js +660 -20
- package/dist/web/util/safe-name.js +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peer-resource-bridge.ts — 4 类资源 (groups/function/exportment/science) 的
|
|
3
|
+
* 本地读 + 远端落盘的统一封装.
|
|
4
|
+
*
|
|
5
|
+
* 设计目的 (2026-07-05):
|
|
6
|
+
* peer-fs.ts 已经有完整路径 helpers (writeGroup/writeFunction/...) + reader (listPeerResources).
|
|
7
|
+
* server.ts 三处要调 (两个 manifest.exchange.reply 落盘 + 一个 manifest.exchange 发送),
|
|
8
|
+
* 每处手写 4 个 await 又啰嗦又容易漏. 这里抽出来.
|
|
9
|
+
*
|
|
10
|
+
* 本地读: ~/.bolloon/local-resources/<category>/<id>.md frontmatter
|
|
11
|
+
* 远端落: peerFs.writeGroup/Function/Exportment/Science (atomic, 人类可读 md)
|
|
12
|
+
*
|
|
13
|
+
* frontmatter 字段就是类型本身的字段 (id/name/description/...),
|
|
14
|
+
* description 从 frontmatter 后面的第一个段落拿.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs/promises';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import * as peerFs from '../network/peer-fs.js';
|
|
20
|
+
const HOME = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
|
|
21
|
+
export const LOCAL_RESOURCES_ROOT = path.join(HOME, 'local-resources');
|
|
22
|
+
const EMPTY = { groups: [], functions: [], exportments: [], sciences: [] };
|
|
23
|
+
/**
|
|
24
|
+
* 简单 frontmatter 解析: 只支持 key: value / key: [..] (本文件自产自销, 不引第三方).
|
|
25
|
+
*/
|
|
26
|
+
function parseFrontmatter(text) {
|
|
27
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
28
|
+
if (!m)
|
|
29
|
+
return { fields: {}, body: text };
|
|
30
|
+
const fields = {};
|
|
31
|
+
for (const line of m[1].split('\n')) {
|
|
32
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/);
|
|
33
|
+
if (!kv)
|
|
34
|
+
continue;
|
|
35
|
+
const key = kv[1];
|
|
36
|
+
let raw = kv[2].trim();
|
|
37
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
38
|
+
try {
|
|
39
|
+
fields[key] = JSON.parse(raw);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
}
|
|
44
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
45
|
+
try {
|
|
46
|
+
fields[key] = JSON.parse(raw);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
51
|
+
fields[key] = raw;
|
|
52
|
+
}
|
|
53
|
+
return { fields, body: m[2].trim() };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 从 ~/.bolloon/local-resources/<category>/<id>.md 读所有本地资源.
|
|
57
|
+
* 文件不存在 / 目录不存在 → 返回空数组 (不报错, 跟 peer-fs 风格一致).
|
|
58
|
+
*/
|
|
59
|
+
async function readCategoryDir(category) {
|
|
60
|
+
try {
|
|
61
|
+
const dir = path.join(LOCAL_RESOURCES_ROOT, category);
|
|
62
|
+
const entries = await fs.readdir(dir);
|
|
63
|
+
return entries.filter((e) => e.endsWith('.md')).map((e) => path.join(dir, e));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function readGroupFile(p) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
72
|
+
const { fields, body } = parseFrontmatter(raw);
|
|
73
|
+
if (!fields.id)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
id: String(fields.id),
|
|
77
|
+
name: String(fields.name || fields.id),
|
|
78
|
+
description: body || undefined,
|
|
79
|
+
visibility: fields.visibility,
|
|
80
|
+
memberCount: typeof fields.memberCount === 'number' ? fields.memberCount : undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function readFunctionFile(p) {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
90
|
+
const { fields, body } = parseFrontmatter(raw);
|
|
91
|
+
if (!fields.capability)
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
capability: String(fields.capability),
|
|
95
|
+
description: body || undefined,
|
|
96
|
+
mediaType: fields.mediaType,
|
|
97
|
+
endpoint: fields.endpoint ? String(fields.endpoint) : undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function readExportmentFile(p) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
107
|
+
const { fields, body } = parseFrontmatter(raw);
|
|
108
|
+
if (!fields.name)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
name: String(fields.name),
|
|
112
|
+
description: body || undefined,
|
|
113
|
+
genre: fields.genre ? String(fields.genre) : undefined,
|
|
114
|
+
minPlayers: typeof fields.minPlayers === 'number' ? fields.minPlayers : undefined,
|
|
115
|
+
maxPlayers: typeof fields.maxPlayers === 'number' ? fields.maxPlayers : undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function readScienceFile(p) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
125
|
+
const { fields, body } = parseFrontmatter(raw);
|
|
126
|
+
if (!fields.id)
|
|
127
|
+
return null;
|
|
128
|
+
return {
|
|
129
|
+
id: String(fields.id),
|
|
130
|
+
title: String(fields.title || fields.id),
|
|
131
|
+
description: body || undefined,
|
|
132
|
+
status: fields.status,
|
|
133
|
+
tags: Array.isArray(fields.tags) ? fields.tags.map(String) : undefined,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 读本机所有 4 类资源. 任何失败 → 该类别返回空 (不阻塞其他类别).
|
|
142
|
+
*/
|
|
143
|
+
export async function loadLocalResources() {
|
|
144
|
+
const out = { ...EMPTY };
|
|
145
|
+
const [gFiles, fFiles, eFiles, sFiles] = await Promise.all([
|
|
146
|
+
readCategoryDir('groups'),
|
|
147
|
+
readCategoryDir('functions'),
|
|
148
|
+
readCategoryDir('exportments'),
|
|
149
|
+
readCategoryDir('sciences'),
|
|
150
|
+
]);
|
|
151
|
+
for (const f of gFiles) {
|
|
152
|
+
const v = await readGroupFile(f);
|
|
153
|
+
if (v)
|
|
154
|
+
out.groups.push(v);
|
|
155
|
+
}
|
|
156
|
+
for (const f of fFiles) {
|
|
157
|
+
const v = await readFunctionFile(f);
|
|
158
|
+
if (v)
|
|
159
|
+
out.functions.push(v);
|
|
160
|
+
}
|
|
161
|
+
for (const f of eFiles) {
|
|
162
|
+
const v = await readExportmentFile(f);
|
|
163
|
+
if (v)
|
|
164
|
+
out.exportments.push(v);
|
|
165
|
+
}
|
|
166
|
+
for (const f of sFiles) {
|
|
167
|
+
const v = await readScienceFile(f);
|
|
168
|
+
if (v)
|
|
169
|
+
out.sciences.push(v);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 把 manifest 里的 4 类资源落盘到 peerFs 对应目录.
|
|
175
|
+
* 静默容错: 单条失败不影响其他.
|
|
176
|
+
*/
|
|
177
|
+
export async function writeRemoteResources(publicKey, m) {
|
|
178
|
+
let groups = 0, functions = 0, exportments = 0, sciences = 0;
|
|
179
|
+
if (Array.isArray(m.groups)) {
|
|
180
|
+
for (const g of m.groups) {
|
|
181
|
+
try {
|
|
182
|
+
await peerFs.writeGroup(publicKey, g);
|
|
183
|
+
groups++;
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(m.functions)) {
|
|
189
|
+
for (const f of m.functions) {
|
|
190
|
+
try {
|
|
191
|
+
await peerFs.writeFunction(publicKey, f);
|
|
192
|
+
functions++;
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(m.exportments)) {
|
|
198
|
+
for (const e of m.exportments) {
|
|
199
|
+
try {
|
|
200
|
+
await peerFs.writeExportment(publicKey, e);
|
|
201
|
+
exportments++;
|
|
202
|
+
}
|
|
203
|
+
catch { }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(m.sciences)) {
|
|
207
|
+
for (const s of m.sciences) {
|
|
208
|
+
try {
|
|
209
|
+
await peerFs.writeScience(publicKey, s);
|
|
210
|
+
sciences++;
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { groups, functions, exportments, sciences };
|
|
216
|
+
}
|
package/dist/web/client.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// @ts-nocheck
|
|
3
2
|
// marked 库可能从 CDN 加载失败, 这里做安全降级 (避免 ReferenceError 让 addMessage 整体崩溃)
|
|
4
3
|
if (typeof marked === 'undefined') {
|
|
@@ -618,7 +617,7 @@ async function deleteChannel(channelId, e) {
|
|
|
618
617
|
if (currentChannelId) {
|
|
619
618
|
const ch = channels.find(c => c.id === currentChannelId);
|
|
620
619
|
if (channelNameEl)
|
|
621
|
-
channelNameEl.textContent = ch?.name
|
|
620
|
+
channelNameEl.textContent = safeChannelName(ch?.name, 'Bolloon Agent');
|
|
622
621
|
await selectChannel(currentChannelId);
|
|
623
622
|
}
|
|
624
623
|
else {
|
|
@@ -970,7 +969,7 @@ function renderChannels() {
|
|
|
970
969
|
<polyline points="9 18 15 12 9 6"></polyline>
|
|
971
970
|
</svg>
|
|
972
971
|
<div class="channel-icon">💬</div>
|
|
973
|
-
<span class="channel-name" title="${escapeHtml(ch.name)}">${escapeHtml(ch.name)}</span>
|
|
972
|
+
<span class="channel-name" title="${escapeHtml(safeChannelName(ch.name, ''))}">${escapeHtml(safeChannelName(ch.name))}</span>
|
|
974
973
|
<span class="agent-row-meta">
|
|
975
974
|
${walletBadge}
|
|
976
975
|
${toolsBadge}
|
|
@@ -1098,6 +1097,9 @@ function formatSessionName(sess) {
|
|
|
1098
1097
|
const escapeHtml = MR_escapeHtml || ((s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
1099
1098
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
1100
1099
|
}[c])));
|
|
1100
|
+
// 2026-07-06: 通用 channel name 兜底 — 防止 name=undefined/null/'undefined' 字串
|
|
1101
|
+
// 时 UI 出现 "undefined" 字面量. 委托 util/safe-name.ts (有单测覆盖).
|
|
1102
|
+
import { safeChannelName } from './util/safe-name.js';
|
|
1101
1103
|
function ensureMessageContainer(channelId) {
|
|
1102
1104
|
if (!messagesContainers.has(channelId)) {
|
|
1103
1105
|
const container = document.createElement('div');
|
|
@@ -1134,7 +1136,7 @@ async function selectChannel(channelId, targetSessionId = null) {
|
|
|
1134
1136
|
const channel = channels.find(c => c.id === channelId);
|
|
1135
1137
|
if (channel) {
|
|
1136
1138
|
if (channelNameEl)
|
|
1137
|
-
channelNameEl.textContent = channel.name;
|
|
1139
|
+
channelNameEl.textContent = safeChannelName(channel.name);
|
|
1138
1140
|
currentSessionId = targetSessionId || channel.currentSessionId || 'default';
|
|
1139
1141
|
if (targetSessionId) {
|
|
1140
1142
|
channel.currentSessionId = targetSessionId;
|
|
@@ -1532,7 +1534,7 @@ function connect(channelId) {
|
|
|
1532
1534
|
channel.name = data.newName;
|
|
1533
1535
|
renderChannels();
|
|
1534
1536
|
if (currentChannelId === data.channelId && channelNameEl) {
|
|
1535
|
-
channelNameEl.textContent = data.newName;
|
|
1537
|
+
channelNameEl.textContent = safeChannelName(data.newName);
|
|
1536
1538
|
}
|
|
1537
1539
|
}
|
|
1538
1540
|
}
|
|
@@ -1698,7 +1700,7 @@ async function refreshMentionChannels() {
|
|
|
1698
1700
|
const remote = [];
|
|
1699
1701
|
for (const p of (remoteData.peers || [])) {
|
|
1700
1702
|
for (const c of (p.channels || [])) {
|
|
1701
|
-
remote.push({ id: c.id, name: c.name, source: 'remote', ownerPublicKey: p.peerId });
|
|
1703
|
+
remote.push({ id: c.id, name: safeChannelName(c.name, '(远端未命名)'), source: 'remote', ownerPublicKey: p.peerId });
|
|
1702
1704
|
}
|
|
1703
1705
|
}
|
|
1704
1706
|
mentionChannels = [
|
|
@@ -1751,9 +1753,9 @@ function renderMentionDropdown(items) {
|
|
|
1751
1753
|
// 浅蓝 = 键盘高亮, 白 = 普通
|
|
1752
1754
|
const bg = i === mentionHighlightIdx ? '#eff6ff' : '#fff';
|
|
1753
1755
|
const borderLeft = i === mentionHighlightIdx ? '3px solid #93c5fd' : '3px solid transparent';
|
|
1754
|
-
return `<div class="mention-item" data-idx="${i}" data-channel-id="${escapeHtml(c.id)}" data-channel-name="${escapeHtml(c.name)}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
|
|
1756
|
+
return `<div class="mention-item" data-idx="${i}" data-channel-id="${escapeHtml(c.id)}" data-channel-name="${escapeHtml(safeChannelName(c.name, ''))}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
|
|
1755
1757
|
<span style="font-size:10px;color:${isLocal ? '#059669' : '#2563eb'};background:${isLocal ? '#d1fae5' : '#dbeafe'};padding:1px 6px;border-radius:3px;white-space:nowrap;">${tag}</span>
|
|
1756
|
-
<span style="flex:1;">${escapeHtml(c.name)}</span>${owner}
|
|
1758
|
+
<span style="flex:1;">${escapeHtml(safeChannelName(c.name))}</span>${owner}
|
|
1757
1759
|
</div>`;
|
|
1758
1760
|
}).join('');
|
|
1759
1761
|
mentionDropdownEl.innerHTML = headerHtml + rows;
|
|
@@ -1794,7 +1796,7 @@ function applyMention(channel) {
|
|
|
1794
1796
|
}
|
|
1795
1797
|
const before = input.value.slice(0, anchor); // 含 @
|
|
1796
1798
|
const after = input.value.slice(blockEnd); // query 之后 (可能用户已输入正文)
|
|
1797
|
-
const insert = `@${channel.name} `;
|
|
1799
|
+
const insert = `@${safeChannelName(channel.name)} `;
|
|
1798
1800
|
input.value = before + insert + after;
|
|
1799
1801
|
const newPos = before.length + insert.length;
|
|
1800
1802
|
input.focus();
|
|
@@ -1826,7 +1828,7 @@ function updateMentionDropdown() {
|
|
|
1826
1828
|
}
|
|
1827
1829
|
mentionQuery = m.query;
|
|
1828
1830
|
const q = m.query.toLowerCase();
|
|
1829
|
-
const items = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1831
|
+
const items = mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8);
|
|
1830
1832
|
mentionHighlightIdx = items.length > 0 ? 0 : -1;
|
|
1831
1833
|
renderMentionDropdown(items);
|
|
1832
1834
|
}
|
|
@@ -1843,7 +1845,7 @@ input.addEventListener('keydown', (e) => {
|
|
|
1843
1845
|
return;
|
|
1844
1846
|
mentionHighlightIdx = (mentionHighlightIdx + 1) % items.length;
|
|
1845
1847
|
const q = (mentionQuery || '').toLowerCase();
|
|
1846
|
-
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1848
|
+
const filtered = mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8);
|
|
1847
1849
|
renderMentionDropdown(filtered);
|
|
1848
1850
|
}
|
|
1849
1851
|
else if (e.key === 'ArrowUp') {
|
|
@@ -1852,7 +1854,7 @@ input.addEventListener('keydown', (e) => {
|
|
|
1852
1854
|
return;
|
|
1853
1855
|
mentionHighlightIdx = (mentionHighlightIdx - 1 + items.length) % items.length;
|
|
1854
1856
|
const q = (mentionQuery || '').toLowerCase();
|
|
1855
|
-
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1857
|
+
const filtered = mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8);
|
|
1856
1858
|
renderMentionDropdown(filtered);
|
|
1857
1859
|
}
|
|
1858
1860
|
else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
@@ -1861,7 +1863,7 @@ input.addEventListener('keydown', (e) => {
|
|
|
1861
1863
|
e.preventDefault();
|
|
1862
1864
|
e.stopPropagation();
|
|
1863
1865
|
const q = (mentionQuery || '').toLowerCase();
|
|
1864
|
-
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1866
|
+
const filtered = mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8);
|
|
1865
1867
|
const cur = filtered[mentionHighlightIdx];
|
|
1866
1868
|
if (cur)
|
|
1867
1869
|
applyMention(cur);
|
|
@@ -1914,7 +1916,7 @@ function setupMentionAutocomplete(inputEl) {
|
|
|
1914
1916
|
}
|
|
1915
1917
|
const before = inputEl.value.slice(0, anchor); // 含 @
|
|
1916
1918
|
const after = inputEl.value.slice(blockEnd);
|
|
1917
|
-
const insert = `@${channel.name} `;
|
|
1919
|
+
const insert = `@${safeChannelName(channel.name)} `;
|
|
1918
1920
|
inputEl.value = before + insert + after;
|
|
1919
1921
|
const newPos = before.length + insert.length;
|
|
1920
1922
|
inputEl.focus();
|
|
@@ -1941,10 +1943,10 @@ function setupMentionAutocomplete(inputEl) {
|
|
|
1941
1943
|
const owner = !isLocal && c.ownerPublicKey ? ` <span style="color:#9ca3af;font-size:11px;">(${c.ownerPublicKey.substring(0, 8)}…)</span>` : '';
|
|
1942
1944
|
const bg = i === localHighlight ? '#eff6ff' : '#fff';
|
|
1943
1945
|
const borderLeft = i === localHighlight ? '3px solid #93c5fd' : '3px solid transparent';
|
|
1944
|
-
return `<div class="mention-item" data-idx="${i}" data-channel-id="${escapeHtml(c.id)}" data-channel-name="${escapeHtml(c.name)}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1946
|
+
return `<div class="mention-item" data-idx="${i}" data-channel-id="${escapeHtml(c.id)}" data-channel-name="${escapeHtml(safeChannelName(c.name, ''))}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
|
|
1947
|
+
<span style="font-size:10px;color:${isLocal ? '#059669' : '#2563eb'};background:${isLocal ? '#d1fae5' : '#dbeafe'};padding:1px 6px;border-radius:3px;white-space:nowrap;">${tag}</span>
|
|
1948
|
+
<span style="flex:1;">${escapeHtml(safeChannelName(c.name))}</span>${owner}
|
|
1949
|
+
</div>`;
|
|
1948
1950
|
}).join('');
|
|
1949
1951
|
inputEl.__mentionDD.querySelectorAll('.mention-item').forEach((el) => {
|
|
1950
1952
|
const idx = parseInt(el.getAttribute('data-idx'));
|
|
@@ -1990,7 +1992,7 @@ function setupMentionAutocomplete(inputEl) {
|
|
|
1990
1992
|
}
|
|
1991
1993
|
localQuery = m.query;
|
|
1992
1994
|
const q = m.query.toLowerCase();
|
|
1993
|
-
const items = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1995
|
+
const items = mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8);
|
|
1994
1996
|
localHighlight = items.length > 0 ? 0 : -1;
|
|
1995
1997
|
renderLocal(items);
|
|
1996
1998
|
}
|
|
@@ -2005,7 +2007,7 @@ function setupMentionAutocomplete(inputEl) {
|
|
|
2005
2007
|
return;
|
|
2006
2008
|
localHighlight = (localHighlight + 1) % items.length;
|
|
2007
2009
|
const q = (localQuery || '').toLowerCase();
|
|
2008
|
-
renderLocal(mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8));
|
|
2010
|
+
renderLocal(mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8));
|
|
2009
2011
|
}
|
|
2010
2012
|
else if (e.key === 'ArrowUp') {
|
|
2011
2013
|
e.preventDefault();
|
|
@@ -2013,14 +2015,14 @@ function setupMentionAutocomplete(inputEl) {
|
|
|
2013
2015
|
return;
|
|
2014
2016
|
localHighlight = (localHighlight - 1 + items.length) % items.length;
|
|
2015
2017
|
const q = (localQuery || '').toLowerCase();
|
|
2016
|
-
renderLocal(mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8));
|
|
2018
|
+
renderLocal(mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8));
|
|
2017
2019
|
}
|
|
2018
2020
|
else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
2019
2021
|
if (items.length > 0) {
|
|
2020
2022
|
e.preventDefault();
|
|
2021
2023
|
e.stopPropagation();
|
|
2022
2024
|
const q = (localQuery || '').toLowerCase();
|
|
2023
|
-
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
2025
|
+
const filtered = mentionChannels.filter(c => safeChannelName(c.name).toLowerCase().includes(q)).slice(0, 8);
|
|
2024
2026
|
const cur = filtered[localHighlight];
|
|
2025
2027
|
if (cur)
|
|
2026
2028
|
applyLocal(cur);
|
|
@@ -2256,7 +2258,7 @@ function renderJudgments(items) {
|
|
|
2256
2258
|
? channels.find(c => c.id === currentChannelId)
|
|
2257
2259
|
: null;
|
|
2258
2260
|
if (chNameEl) {
|
|
2259
|
-
chNameEl.textContent = currentCh ? `(${currentCh.name})` : '(未选)';
|
|
2261
|
+
chNameEl.textContent = currentCh ? `(${safeChannelName(currentCh.name)})` : '(未选)';
|
|
2260
2262
|
}
|
|
2261
2263
|
if (all.length === 0) {
|
|
2262
2264
|
judgmentsList.innerHTML = '<div class="task-empty">还没有判断, 在上面记录第一条吧</div>';
|
|
@@ -2286,7 +2288,7 @@ function renderJudgments(items) {
|
|
|
2286
2288
|
const bound = all.filter(j => boundIds.has(j.id));
|
|
2287
2289
|
const unbound = all.filter(j => !boundIds.has(j.id));
|
|
2288
2290
|
if (titleEl)
|
|
2289
|
-
titleEl.textContent = `${currentCh.name} 的判断力 (已绑 ${bound.length} / 共 ${all.length})`;
|
|
2291
|
+
titleEl.textContent = `${safeChannelName(currentCh.name)} 的判断力 (已绑 ${bound.length} / 共 ${all.length})`;
|
|
2290
2292
|
let html = '';
|
|
2291
2293
|
if (bound.length > 0) {
|
|
2292
2294
|
html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:8px 4px 4px;">已绑定 (${bound.length})</div>`;
|
|
@@ -3259,7 +3261,7 @@ function renderRemoteChannels() {
|
|
|
3259
3261
|
<div class="remote-channel-row" data-peer-id="${escapeHtml(peer.publicKey)}" data-channel-id="${escapeHtml(c.id)}"
|
|
3260
3262
|
style="display:flex;align-items:center;gap:6px;padding:4px 6px;cursor:pointer;border-radius:4px;font-size:12px;">
|
|
3261
3263
|
<span>🤖</span>
|
|
3262
|
-
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(c.name
|
|
3264
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(safeChannelName(c.name, ''))}">${escapeHtml(safeChannelName(c.name))}</span>
|
|
3263
3265
|
</div>
|
|
3264
3266
|
`).join('')}
|
|
3265
3267
|
</div>
|
|
@@ -3408,7 +3410,7 @@ async function openShareToPeerModal(peerName, peerPublicKey) {
|
|
|
3408
3410
|
<label class="share-modal-row">
|
|
3409
3411
|
<input type="checkbox" data-cid="${escapeHtml(ch.id)}" ${isShared ? 'checked' : ''} class="share-modal-cb">
|
|
3410
3412
|
<div class="share-modal-row-info">
|
|
3411
|
-
<div class="share-modal-row-name">${escapeHtml(ch.name)}</div>
|
|
3413
|
+
<div class="share-modal-row-name">${escapeHtml(ch.name || '(未命名)')}</div>
|
|
3412
3414
|
<div class="share-modal-row-meta">
|
|
3413
3415
|
${isShared ? '✓ 已分享' : '未分享'} · ${escapeHtml(ch.id.slice(0, 24))}…
|
|
3414
3416
|
</div>
|
|
@@ -4190,7 +4192,7 @@ function renderWalletList() {
|
|
|
4190
4192
|
row.innerHTML = `
|
|
4191
4193
|
<span class="wallet-chain">${escapeHtml(chain)}</span>
|
|
4192
4194
|
<div class="wallet-info">
|
|
4193
|
-
<span class="wallet-agent" title="${escapeHtml(ch.name)}">${escapeHtml(ch.name)}</span>
|
|
4195
|
+
<span class="wallet-agent" title="${escapeHtml(ch.name || '')}">${escapeHtml(ch.name || '(未命名)')}</span>
|
|
4194
4196
|
<span class="wallet-address" title="${escapeHtml(ch.walletAddress)}">${escapeHtml(ch.walletAddress)}</span>
|
|
4195
4197
|
</div>
|
|
4196
4198
|
<div class="wallet-actions">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* P2P Modal - 纯 TypeScript 版本
|
|
4
3
|
*/
|
|
4
|
+
import { safeName as safeAnyName } from '../../util/safe-name.js';
|
|
5
5
|
class P2PModalUI {
|
|
6
6
|
modal = null;
|
|
7
7
|
overlay = null;
|
|
@@ -58,8 +58,14 @@ class P2PModalUI {
|
|
|
58
58
|
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
59
59
|
const irohData = await irohResp.json();
|
|
60
60
|
if (irohData.cid) {
|
|
61
|
+
// 2026-07-06: 防御 irohData.name 是 'undefined'/'null'/空白 的情况,
|
|
62
|
+
// 否则 UI 会渲染字面量 "undefined". (multi-confirmed fix: 避免再现 v0.2.7 的 bug)
|
|
63
|
+
const rawName = (typeof irohData.name === 'string' && irohData.name.trim()
|
|
64
|
+
&& irohData.name.trim() !== 'undefined'
|
|
65
|
+
&& irohData.name.trim() !== 'null') ? irohData.name.trim() : '';
|
|
66
|
+
const tailName = rawName.split('-').slice(-1)[0] || '';
|
|
61
67
|
identityData = {
|
|
62
|
-
name:
|
|
68
|
+
name: rawName || tailName || 'Bolloon',
|
|
63
69
|
did: irohData.did || '未知',
|
|
64
70
|
cid: irohData.cid || '未知',
|
|
65
71
|
nodeId: irohData.irohNodeId ? irohData.irohNodeId.substring(0, 20) + '...' : '未知'
|
|
@@ -77,7 +83,7 @@ class P2PModalUI {
|
|
|
77
83
|
<h3>身份信息</h3>
|
|
78
84
|
<div class="p2p-info-row">
|
|
79
85
|
<span class="label">名称:</span>
|
|
80
|
-
<span class="value">${this.escapeHtml(identityData.name
|
|
86
|
+
<span class="value">${this.escapeHtml(this.safeName(identityData.name, '未知'))}</span>
|
|
81
87
|
</div>
|
|
82
88
|
<div class="p2p-info-row">
|
|
83
89
|
<span class="label">CID:</span>
|
|
@@ -237,7 +243,7 @@ class P2PModalUI {
|
|
|
237
243
|
});
|
|
238
244
|
const data = await resp.json();
|
|
239
245
|
if (data.ok) {
|
|
240
|
-
result.innerHTML = `<div class="p2p-success">连接成功!节点: ${this.escapeHtml(data.nodeName
|
|
246
|
+
result.innerHTML = `<div class="p2p-success">连接成功!节点: ${this.escapeHtml(this.safeName(data.nodeName, cid.substring(0, 16)))}...</div>`;
|
|
241
247
|
result.className = 'p2p-result success';
|
|
242
248
|
// 清空输入框
|
|
243
249
|
input.value = '';
|
|
@@ -301,7 +307,7 @@ class P2PModalUI {
|
|
|
301
307
|
const time = new Date(info.time).toLocaleTimeString();
|
|
302
308
|
html += `
|
|
303
309
|
<div class="p2p-peer-item">
|
|
304
|
-
<span class="p2p-peer-name">${this.escapeHtml(info.name
|
|
310
|
+
<span class="p2p-peer-name">${this.escapeHtml(this.safeName(info.name, shortId))}</span>
|
|
305
311
|
<span class="p2p-peer-id">${shortId}</span>
|
|
306
312
|
<span class="p2p-peer-time">${time}</span>
|
|
307
313
|
</div>
|
|
@@ -314,6 +320,12 @@ class P2PModalUI {
|
|
|
314
320
|
div.textContent = str;
|
|
315
321
|
return div.innerHTML;
|
|
316
322
|
}
|
|
323
|
+
// 2026-07-06: 通用 name 兜底 — 防止 name=undefined/null/'undefined' 字串
|
|
324
|
+
// 时 UI 渲染字面量 "undefined".
|
|
325
|
+
safeName(input, fallback) {
|
|
326
|
+
// 2026-07-06: 委托共享 safe-name.ts (有单测覆盖); 保持方法签名兼容旧调用.
|
|
327
|
+
return safeAnyName(input, fallback || '未知');
|
|
328
|
+
}
|
|
317
329
|
showToast(message) {
|
|
318
330
|
const toast = document.createElement('div');
|
|
319
331
|
toast.className = 'p2p-toast';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* P2P Modal Web Component
|
|
3
3
|
*/
|
|
4
4
|
import { p2pManager } from './p2p-manager.js';
|
|
5
|
+
import { safeName as safeAnyName } from '../../util/safe-name.js';
|
|
5
6
|
export class P2PModal extends HTMLElement {
|
|
6
7
|
shadow = this.attachShadow({ mode: 'open' });
|
|
7
8
|
activeTab = 'identity';
|
|
@@ -107,7 +108,7 @@ export class P2PModal extends HTMLElement {
|
|
|
107
108
|
<div class="history-item-icon">💬</div>
|
|
108
109
|
<div class="history-item-info">
|
|
109
110
|
<div class="history-item-name">
|
|
110
|
-
${this.escapeHtml(item.name
|
|
111
|
+
${this.escapeHtml(this.safeName(item.name, 'Unknown'))}
|
|
111
112
|
${item.isPinned ? '<span class="pin-icon">📌</span>' : ''}
|
|
112
113
|
</div>
|
|
113
114
|
<div class="history-item-meta">
|
|
@@ -217,7 +218,7 @@ export class P2PModal extends HTMLElement {
|
|
|
217
218
|
div.innerHTML = `
|
|
218
219
|
<div class="peer-status"><span class="dot online"></span></div>
|
|
219
220
|
<div class="peer-info">
|
|
220
|
-
<div class="peer-name">${this.escapeHtml(peer.info?.name
|
|
221
|
+
<div class="peer-name">${this.escapeHtml(this.safeName(peer.info?.name, 'Unknown'))}</div>
|
|
221
222
|
<div class="peer-meta">${(peer.nodeId || '').substring(0, 16)}...</div>
|
|
222
223
|
</div>
|
|
223
224
|
`;
|
|
@@ -275,6 +276,12 @@ export class P2PModal extends HTMLElement {
|
|
|
275
276
|
div.textContent = text;
|
|
276
277
|
return div.innerHTML;
|
|
277
278
|
}
|
|
279
|
+
// 2026-07-06: 通用 name 兜底 — 防止 name=undefined/null/'undefined' 字串
|
|
280
|
+
// 时 UI 渲染字面量 "undefined".
|
|
281
|
+
safeName(input, fallback) {
|
|
282
|
+
// 2026-07-06: 委托共享 safe-name.ts (有单测覆盖); 保持方法签名兼容旧调用.
|
|
283
|
+
return safeAnyName(input, fallback || 'Unknown');
|
|
284
|
+
}
|
|
278
285
|
showToast(message) {
|
|
279
286
|
const toast = document.createElement('div');
|
|
280
287
|
toast.className = 'toast';
|