dsh-fou 0.2.0 → 0.3.0
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/client.js +178 -1
- package/desk.html +291 -0
- package/docs/20-/346/214/202/346/235/277/345/215/217/350/256/256.md +131 -35
- package/index.js +36 -0
- package/package.json +12 -1
- package/panels.html +9 -2
- package/settings.html +185 -0
package/client.js
CHANGED
|
@@ -185,6 +185,10 @@ window.__ModuleLoader__.load({
|
|
|
185
185
|
pillCrossHint: '点一下进入会话列表(滚轮切会话)',
|
|
186
186
|
pillListHint: '展开会话列表(直接选)',
|
|
187
187
|
pillSlotHint: '切换应用(点一下换下一个)',
|
|
188
|
+
firstRunHint: '滚轮切功能 · Shift+滚轮切应用 · 右键开间 · 对会话说「给我一份 checklist」',
|
|
189
|
+
deskTitle: '桌布',
|
|
190
|
+
settingsTitle: '设置',
|
|
191
|
+
addNoSession: '先开一个会话,我才好替你说话',
|
|
188
192
|
noSession: '没有活动会话',
|
|
189
193
|
noSessionHint: '打开一个会话后,就能从这里发消息',
|
|
190
194
|
noScope: '会话作用域不可用',
|
|
@@ -268,6 +272,10 @@ window.__ModuleLoader__.load({
|
|
|
268
272
|
pillCrossHint: 'Click to scrub sessions (wheel to switch)',
|
|
269
273
|
pillListHint: 'Open session list (pick directly)',
|
|
270
274
|
pillSlotHint: 'Switch app (click to cycle)',
|
|
275
|
+
firstRunHint: 'Scroll to switch pane · Shift+scroll to switch app · Right-click for the pane · Say "give me a checklist"',
|
|
276
|
+
deskTitle: 'Tablecloth',
|
|
277
|
+
settingsTitle: 'Settings',
|
|
278
|
+
addNoSession: 'Open a session first, then I can speak for you',
|
|
271
279
|
noSession: 'No active session',
|
|
272
280
|
noSessionHint: 'Open a session and you can message it from here',
|
|
273
281
|
noScope: 'Session scope unavailable',
|
|
@@ -1185,6 +1193,23 @@ window.__ModuleLoader__.load({
|
|
|
1185
1193
|
// 切换只用**点击左栏**,不用滚轮 —— 滚轮已经归三态了,不能再分一个含义。
|
|
1186
1194
|
const [pillApp, setPillApp] = React.useState(null);
|
|
1187
1195
|
const [pillAppTick, setPillAppTick] = React.useState(0);
|
|
1196
|
+
// 首次教学(2026-11-10):localStorage 里没有 fou.seen.v1 就显示一句怎么用。
|
|
1197
|
+
const SEEN_KEY = 'fou.seen.v1';
|
|
1198
|
+
const [learning, setLearning] = React.useState(() => {
|
|
1199
|
+
try {
|
|
1200
|
+
return window.localStorage.getItem(SEEN_KEY) !== '1';
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
const markSeen = React.useCallback(() => {
|
|
1206
|
+
try {
|
|
1207
|
+
window.localStorage.setItem(SEEN_KEY, '1');
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
/* storage unavailable */
|
|
1210
|
+
}
|
|
1211
|
+
setLearning(false);
|
|
1212
|
+
}, []);
|
|
1188
1213
|
// 横滚(2026-11-09):简讯放不下才滚。用 ref 量真实宽度,别靠字数猜。
|
|
1189
1214
|
const pillTextRef = React.useRef(null);
|
|
1190
1215
|
const [pillOverflow, setPillOverflow] = React.useState(false);
|
|
@@ -1216,6 +1241,12 @@ window.__ModuleLoader__.load({
|
|
|
1216
1241
|
* 硬挂载(sheet.mount)不属于这三种,单独记 'mount'(三个图标都不亮)。 */
|
|
1217
1242
|
const sheetMode = sheet ? (sheet.mount ? 'mount' : modeOfUrl(sheet.url)) : 'browse';
|
|
1218
1243
|
|
|
1244
|
+
// 桌面(2026-11-10):右键 → 打开桌面。用 ref 让"React 之外"的右键处理器能调它。
|
|
1245
|
+
// ⚠ 必须**声明在使用之前** —— openDesk 一赋值就读它。
|
|
1246
|
+
// 2026-11-10 踩过 TDZ:`Cannot access before initialization`,
|
|
1247
|
+
// 整个 CourierBar 崩掉,浮游直接从页面上消失(用户:"浮游坏了")。
|
|
1248
|
+
const deskOpenRef = React.useRef(null);
|
|
1249
|
+
|
|
1219
1250
|
/** 三个模式图标点下去:换页(同一个框,换内容)。 */
|
|
1220
1251
|
const openModeSheet = (want) => {
|
|
1221
1252
|
const params = new URLSearchParams();
|
|
@@ -1253,6 +1284,46 @@ window.__ModuleLoader__.load({
|
|
|
1253
1284
|
setMode('panel');
|
|
1254
1285
|
};
|
|
1255
1286
|
|
|
1287
|
+
// ── 桌面(2026-11-10 用户:"右键打开一个桌面")
|
|
1288
|
+
//
|
|
1289
|
+
// 它是**摊开的菜单**:原来右键里藏着的(浏览/编辑/挂板/挂靠/设置),
|
|
1290
|
+
// 全摆在这一页的图标上。判据:**不告诉用户有什么,把东西摆出来让他看见。**
|
|
1291
|
+
//
|
|
1292
|
+
// 坞条目通过 URL 传过去(桌面是 iframe,读不到浮游的 React 状态)。
|
|
1293
|
+
// ⚠ URL 有长度上限,但坞条目一般 <10 个,够用。
|
|
1294
|
+
const openDesk = () => {
|
|
1295
|
+
const params = new URLSearchParams();
|
|
1296
|
+
if (activeId) {
|
|
1297
|
+
params.set('notify', activeId);
|
|
1298
|
+
params.set('notifyName', activeTitle || '');
|
|
1299
|
+
}
|
|
1300
|
+
const items = dockEntries
|
|
1301
|
+
.map((entry) => ({
|
|
1302
|
+
id: entry.id,
|
|
1303
|
+
label: entry.label || entry.id,
|
|
1304
|
+
icon: entry.icon || '',
|
|
1305
|
+
}))
|
|
1306
|
+
.filter((item) => item.id !== 'fou.hop');
|
|
1307
|
+
if (items.length > 0) params.set('dock', JSON.stringify(items));
|
|
1308
|
+
const qs = params.toString();
|
|
1309
|
+
const url = '/dsh-fou/desk.html' + (qs ? '?' + qs : '');
|
|
1310
|
+
setRecentOpen(false);
|
|
1311
|
+
setMenuTab(null);
|
|
1312
|
+
setSheet({ url, title: t('deskTitle') });
|
|
1313
|
+
setMode('panel');
|
|
1314
|
+
};
|
|
1315
|
+
deskOpenRef.current = openDesk;
|
|
1316
|
+
|
|
1317
|
+
// 设置页(2026-11-10 用户第 1 条):点 ⚙ 打开一**页**,不是一个菜单。
|
|
1318
|
+
// 判据:**菜单回答"能做什么",设置页回答"现在是什么"。**
|
|
1319
|
+
const openSettings = () => {
|
|
1320
|
+
setRecentOpen(false);
|
|
1321
|
+
setMenuTab(null);
|
|
1322
|
+
setMenu(null);
|
|
1323
|
+
setSheet({ url: '/dsh-fou/settings.html?mode=' + encodeURIComponent(mode), title: t('settingsTitle') });
|
|
1324
|
+
setMode('panel');
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1256
1327
|
// 把"打开这个 url"的能力交给 factory 里的链接右键处理器(它活在 React 之外)。
|
|
1257
1328
|
// 挂载时注册,卸载时撤销;同时把"坞还没出来时收到的请求"补开一次。
|
|
1258
1329
|
React.useEffect(() => {
|
|
@@ -1432,6 +1503,17 @@ window.__ModuleLoader__.load({
|
|
|
1432
1503
|
return;
|
|
1433
1504
|
}
|
|
1434
1505
|
event.preventDefault();
|
|
1506
|
+
// ── 右键 = 打开桌面(2026-11-10 用户:"右键打开一个桌面")
|
|
1507
|
+
//
|
|
1508
|
+
// 原来的右键菜单被**摊开**成了桌面上的图标(浏览/编辑/挂板/插件/设置)。
|
|
1509
|
+
// 判据:**菜单是"告诉你有几项",桌面是"把东西摆出来"。**
|
|
1510
|
+
// ——而桌面能留白,菜单不能。
|
|
1511
|
+
const open = deskOpenRef.current;
|
|
1512
|
+
if (typeof open === 'function') {
|
|
1513
|
+
open();
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
// 兜底(正常不会走到):桌面还没挂上时,退回旧菜单
|
|
1435
1517
|
const box = { w: 196, h: 320 };
|
|
1436
1518
|
setMenuTab(null);
|
|
1437
1519
|
setMenu({
|
|
@@ -1938,6 +2020,92 @@ window.__ModuleLoader__.load({
|
|
|
1938
2020
|
return;
|
|
1939
2021
|
}
|
|
1940
2022
|
|
|
2023
|
+
// 「设置页动了一下」(2026-11-10 用户第 1 条):设置页是 iframe,
|
|
2024
|
+
// 它只负责"显示当前值 + 报动作",真正的改动在这里做。
|
|
2025
|
+
if (data.type === 'fou:setting') {
|
|
2026
|
+
const action = typeof data.action === 'string' ? data.action : '';
|
|
2027
|
+
if (action === 'mode') {
|
|
2028
|
+
const next = typeof data.value === 'string' ? data.value : '';
|
|
2029
|
+
if (next === 'orb' || next === 'pill' || next === 'panel') {
|
|
2030
|
+
setMode(next);
|
|
2031
|
+
saveMode(next);
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (action === 'reset') {
|
|
2036
|
+
resetPos();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (action === 'collapse') {
|
|
2040
|
+
collapse();
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// 「桌面点了一个图标」(2026-11-10):桌面是 iframe,它只负责"被点",
|
|
2047
|
+
// 真正的动作在这里执行 —— 因为它要动浮游自己的状态(形态 / 开间)。
|
|
2048
|
+
//
|
|
2049
|
+
// ⚠ 只用**已存在**的函数与状态(openModeSheet / dockEntries / setSheet / setMode /
|
|
2050
|
+
// setMenu / setMenuTab)。设置仍走旧菜单 —— 它是浮游自身的开关,不需要"摆出来"。
|
|
2051
|
+
if (data.type === 'fou:desk') {
|
|
2052
|
+
const action = typeof data.action === 'string' ? data.action : '';
|
|
2053
|
+
if (action === 'browse' || action === 'edit' || action === 'panels') {
|
|
2054
|
+
setMenu(null);
|
|
2055
|
+
setMenuTab(null);
|
|
2056
|
+
openModeSheet(action === 'panels' ? 'form' : action);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
if (action === 'dock') {
|
|
2060
|
+
const id = typeof data.id === 'string' ? data.id : '';
|
|
2061
|
+
if (!id) return;
|
|
2062
|
+
const entry = dockEntries.find((item) => item && item.id === id);
|
|
2063
|
+
if (!entry) return;
|
|
2064
|
+
setMenu(null);
|
|
2065
|
+
setMenuTab(null);
|
|
2066
|
+
// 硬挂载优先(2026-11-08):有 mount 就挂进开间,是活的界面;
|
|
2067
|
+
// 没有 mount 才退回 run(遥控 / 让它自己弹窗)。
|
|
2068
|
+
if (typeof entry.mount === 'function') {
|
|
2069
|
+
setSheet({ mount: entry, title: entry.label });
|
|
2070
|
+
setMode('panel');
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (typeof entry.run === 'function') {
|
|
2074
|
+
try {
|
|
2075
|
+
entry.run();
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
/* 坏条目不能弄坏桌面 */
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (action === 'setting') {
|
|
2083
|
+
openSettings();
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
// ➕ 挂载插件(2026-11-10 用户第 3 条):
|
|
2087
|
+
// **不写一句"你可以这样做",而是**替他说出口**。
|
|
2088
|
+
// 桌布上点 ➕ → 浮游给当前会话发一条消息 → agent 收到就去做。
|
|
2089
|
+
// 判据:文字的引导要求用户自己做一步;界面的引导是替他把那步做了。
|
|
2090
|
+
if (action === 'addPlugin') {
|
|
2091
|
+
const id = activeIdRef.current;
|
|
2092
|
+
if (!id) {
|
|
2093
|
+
setFailure(t('addNoSession'));
|
|
2094
|
+
setMode('panel');
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
const say = notifyRef.current;
|
|
2098
|
+
if (typeof say === 'function') {
|
|
2099
|
+
void say(
|
|
2100
|
+
id,
|
|
2101
|
+
'浮游的桌布上还空着一个位置。帮我把插件挂上去 —— 先读 skill `fou-mount`,再按里面的模板接一个。'
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1941
2109
|
// 「记住一件东西」:编辑器的 [保存] 落稿后回报,记进"最近编辑",
|
|
1942
2110
|
// 让"最近"记的是**稿**,而不是"我打开过编辑器"(2026-11-08)。
|
|
1943
2111
|
if (data.type === 'fou:remember') {
|
|
@@ -2148,7 +2316,14 @@ window.__ModuleLoader__.load({
|
|
|
2148
2316
|
})();
|
|
2149
2317
|
|
|
2150
2318
|
const latest = rows.length > 0 ? rows[rows.length - 1] : null;
|
|
2151
|
-
|
|
2319
|
+
// 首次安装的一次性教学(2026-11-10「引导四格」的「用户·主动」):
|
|
2320
|
+
// 装好后第一次打开,胶囊不显示"暂无会话",而是显示一句怎么用;
|
|
2321
|
+
// 一滚轮就写标记,从此不再出现。**不弹窗、不打扰** —— 它只在"用户第一次看它"时说话。
|
|
2322
|
+
const pillText = learning
|
|
2323
|
+
? t('firstRunHint')
|
|
2324
|
+
: latest
|
|
2325
|
+
? pickBrief(latest.text) || t('hint')
|
|
2326
|
+
: t('hint');
|
|
2152
2327
|
// 简讯排版(2026-11-09 用户):开头是**会话名**,中间只留一个「·」,
|
|
2153
2328
|
// 结尾留空隔 —— 这样滚起来每一段都像"一条信息",有整体感。
|
|
2154
2329
|
// 会话名太长就截短:胶囊空间有限,名字只是"定位",不是内容。
|
|
@@ -2219,6 +2394,8 @@ window.__ModuleLoader__.load({
|
|
|
2219
2394
|
const el = mode === 'orb' ? bubbleRef.current : pillWrapRef.current;
|
|
2220
2395
|
if (!el) return undefined;
|
|
2221
2396
|
const onWheel = (event) => {
|
|
2397
|
+
// 首次教学:任何一次滚轮都算"看过了"。
|
|
2398
|
+
if (learning) markSeen();
|
|
2222
2399
|
// 列表浮层自己会滚 —— 在它的范围内**不劫持**滚轮。
|
|
2223
2400
|
// 判据:任何"浮在上面"的可滚动区域,必须在它自己的范围内接管滚轮。
|
|
2224
2401
|
// (否则滚列表 = 滚胶囊,列表永远滚不动。)
|
package/desk.html
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>桌布 · 浮游</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ── 白布 ──────────────────────────────────────────────
|
|
9
|
+
2026-11-10 用户:"右键打开一个桌面。" → 后改名「桌布」:
|
|
10
|
+
"截至目前 它是铺在 DSH 上的。即使以后独立了,它也要铺在 Windows 上。"
|
|
11
|
+
—— 所以「桌布」不是"桌面",它是**一层布**,盖在别的东西上。
|
|
12
|
+
这一页**不是菜单**,是**摊开的菜单**:原来右键里藏的东西,全摆在这儿。
|
|
13
|
+
|
|
14
|
+
三条判据(用户定的):
|
|
15
|
+
① 空白本身是内容 —— 桌布不填满,留白就是"还能放"。
|
|
16
|
+
② 图标是可点的,不是文字说明 —— 点一下比读一句快。
|
|
17
|
+
③ 分组靠间距,不靠标题 —— 用户:"我们用设计做说明。"
|
|
18
|
+
*/
|
|
19
|
+
:root {
|
|
20
|
+
--ink: #1f2937;
|
|
21
|
+
--dim: #6b7280;
|
|
22
|
+
--line: #e5e7eb;
|
|
23
|
+
--card: #ffffff;
|
|
24
|
+
--hover: #f3f4f6;
|
|
25
|
+
--accent: #2563eb;
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
html, body { height: 100%; margin: 0; }
|
|
29
|
+
body {
|
|
30
|
+
font: 13px/1.5 -apple-system, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
|
31
|
+
color: var(--ink);
|
|
32
|
+
/* 白布:极浅的暖灰,不是纯白 —— 纯白在毛玻璃上会发灰 */
|
|
33
|
+
background: linear-gradient(180deg, #fbfbfc 0%, #f7f8fa 100%);
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
overflow: auto;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
header {
|
|
40
|
+
flex: none;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
padding: 10px 14px 8px;
|
|
45
|
+
border-bottom: 1px solid var(--line);
|
|
46
|
+
background: rgba(255, 255, 255, .7);
|
|
47
|
+
backdrop-filter: blur(8px);
|
|
48
|
+
position: sticky;
|
|
49
|
+
top: 0;
|
|
50
|
+
z-index: 2;
|
|
51
|
+
}
|
|
52
|
+
header .title { font-weight: 600; font-size: 13px; }
|
|
53
|
+
header .grow { flex: 1; }
|
|
54
|
+
header .hint { color: var(--dim); font-size: 11px; }
|
|
55
|
+
|
|
56
|
+
main {
|
|
57
|
+
flex: 1;
|
|
58
|
+
padding: 18px 14px 36px;
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: 26px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* 一组图标 */
|
|
65
|
+
.group { display: flex; flex-direction: column; gap: 10px; }
|
|
66
|
+
/* 组与组之间没有标题,只有一条极淡的分隔 + 间距(判据③) */
|
|
67
|
+
.grid {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-wrap: wrap;
|
|
70
|
+
gap: 10px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tile {
|
|
74
|
+
width: 84px;
|
|
75
|
+
height: 84px;
|
|
76
|
+
border: 0;
|
|
77
|
+
border-radius: 14px;
|
|
78
|
+
background: var(--card);
|
|
79
|
+
box-shadow: 0 1px 2px #0000000a, 0 4px 14px #0000000d;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
gap: 6px;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
color: var(--ink);
|
|
87
|
+
font: inherit;
|
|
88
|
+
transition: transform .12s ease, box-shadow .12s ease, background .12s ease;
|
|
89
|
+
padding: 6px;
|
|
90
|
+
}
|
|
91
|
+
.tile:hover {
|
|
92
|
+
background: #fff;
|
|
93
|
+
transform: translateY(-1px);
|
|
94
|
+
box-shadow: 0 2px 4px #0000000f, 0 8px 22px #00000014;
|
|
95
|
+
}
|
|
96
|
+
.tile:active { transform: translateY(0) scale(.985); }
|
|
97
|
+
.tile .ico { font-size: 24px; line-height: 1; }
|
|
98
|
+
.tile .nm {
|
|
99
|
+
font-size: 11px;
|
|
100
|
+
color: var(--dim);
|
|
101
|
+
max-width: 72px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
text-overflow: ellipsis;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ➕ 磁贴:虚线边 —— 它不是一个东西,是一个位置("还能放"的那个口子) */
|
|
108
|
+
.tile-add {
|
|
109
|
+
background: transparent;
|
|
110
|
+
border: 1.5px dashed #cfd6e0;
|
|
111
|
+
box-shadow: none;
|
|
112
|
+
}
|
|
113
|
+
.tile-add:hover {
|
|
114
|
+
background: #fff;
|
|
115
|
+
border-color: var(--accent);
|
|
116
|
+
box-shadow: 0 2px 8px #2563eb1a;
|
|
117
|
+
}
|
|
118
|
+
.tile-add .ico { color: var(--accent); font-size: 26px; }
|
|
119
|
+
.tile-add .nm { color: var(--accent); }
|
|
120
|
+
|
|
121
|
+
/* 插件图标:给一点颜色,因为它们是"别人的东西" */
|
|
122
|
+
.tile.plugin { background: #fff; }
|
|
123
|
+
.tile.plugin .ico { filter: saturate(1.1); }
|
|
124
|
+
|
|
125
|
+
/* 空组:不画空盒子,只留一句灰话(判据①) */
|
|
126
|
+
.empty { color: var(--dim); font-size: 11.5px; padding: 2px 2px 0; }
|
|
127
|
+
|
|
128
|
+
/* 右下角:给"还能放什么"留一个视觉上的口子 */
|
|
129
|
+
.tail {
|
|
130
|
+
margin-top: auto;
|
|
131
|
+
padding-top: 18px;
|
|
132
|
+
color: #b6bcc6;
|
|
133
|
+
font-size: 11px;
|
|
134
|
+
text-align: center;
|
|
135
|
+
}
|
|
136
|
+
.tail code {
|
|
137
|
+
background: #eef1f5;
|
|
138
|
+
padding: 1px 5px;
|
|
139
|
+
border-radius: 4px;
|
|
140
|
+
color: #4b5563;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<header>
|
|
146
|
+
<span class="title">桌布</span>
|
|
147
|
+
<span class="grow"></span>
|
|
148
|
+
<span class="hint" id="hint"></span>
|
|
149
|
+
</header>
|
|
150
|
+
|
|
151
|
+
<main id="main">
|
|
152
|
+
<!-- ① 功能 -->
|
|
153
|
+
<div class="group" id="gFunc">
|
|
154
|
+
<div class="grid">
|
|
155
|
+
<button class="tile" data-act="browse" title="回到最近打开的那一页">
|
|
156
|
+
<span class="ico">👁</span><span class="nm">浏览</span>
|
|
157
|
+
</button>
|
|
158
|
+
<button class="tile" data-act="edit" title="编辑即保存、发送即落稿">
|
|
159
|
+
<span class="ico">✏️</span><span class="nm">编辑</span>
|
|
160
|
+
</button>
|
|
161
|
+
<button class="tile" data-act="panels" title="盛放挂表的地方">
|
|
162
|
+
<span class="ico">📌</span><span class="nm">挂板</span>
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- ② 插件(从浮游传进来) -->
|
|
168
|
+
<div class="group" id="gPlug" style="display:none">
|
|
169
|
+
<div class="grid" id="plugGrid"></div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- ③ 设置(只留一个入口 —— 点了回浮游自己的设置菜单) -->
|
|
173
|
+
<div class="group" id="gSet">
|
|
174
|
+
<div class="grid">
|
|
175
|
+
<button class="tile" data-act="setting" title="浮游自身的设置:复位位置 / 收起 / 躲鼠标">
|
|
176
|
+
<span class="ico">⚙</span><span class="nm">设置</span>
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="tail" id="tail"></div>
|
|
182
|
+
</main>
|
|
183
|
+
|
|
184
|
+
<script>
|
|
185
|
+
(function () {
|
|
186
|
+
'use strict';
|
|
187
|
+
|
|
188
|
+
// ── 和浮游通信 ──────────────────────────────────────────
|
|
189
|
+
// 协议(2026-11-10):`type` 以 `fou:` 开头,挂板/桌布会被浮游原样转发。
|
|
190
|
+
function tell(action, extra) {
|
|
191
|
+
var msg = { type: 'fou:desk', action: action };
|
|
192
|
+
if (extra) for (var k in extra) msg[k] = extra[k];
|
|
193
|
+
try {
|
|
194
|
+
window.parent.postMessage(msg, window.location.origin);
|
|
195
|
+
} catch (e) { /* 不在浮游里就静默 */ }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── 读浮游传进来的坞条目 ────────────────────────────────
|
|
199
|
+
// URL: ?dock=<encodeURIComponent(JSON)>
|
|
200
|
+
// 形如 [{"id":"song.dock","label":"地颂","icon":"♫"}, …]
|
|
201
|
+
function readDock() {
|
|
202
|
+
try {
|
|
203
|
+
var q = new URLSearchParams(location.search).get('dock');
|
|
204
|
+
if (!q) return [];
|
|
205
|
+
var arr = JSON.parse(q);
|
|
206
|
+
if (!Array.isArray(arr)) return [];
|
|
207
|
+
return arr.filter(function (x) {
|
|
208
|
+
return x && typeof x.id === 'string' && x.id !== '';
|
|
209
|
+
});
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function paintDock() {
|
|
216
|
+
var dock = readDock();
|
|
217
|
+
var grid = document.getElementById('plugGrid');
|
|
218
|
+
var group = document.getElementById('gPlug');
|
|
219
|
+
if (!dock.length) {
|
|
220
|
+
// 没有插件:给一个 ➕(2026-11-10 用户第 3 条)。
|
|
221
|
+
// 判据:**不是写一句"你可以这样做",是给一个能点的东西。**
|
|
222
|
+
// 点击 → 浮游给当前会话发一条消息,让 agent 去做(它知道自己会什么)。
|
|
223
|
+
group.style.display = '';
|
|
224
|
+
grid.innerHTML = '';
|
|
225
|
+
var add = document.createElement('button');
|
|
226
|
+
add.className = 'tile tile-add';
|
|
227
|
+
add.dataset.act = 'addPlugin';
|
|
228
|
+
add.title = '添加插件 —— 会请会话说一声,让 agent 帮你挂';
|
|
229
|
+
var ai = document.createElement('span');
|
|
230
|
+
ai.className = 'ico';
|
|
231
|
+
ai.textContent = '+';
|
|
232
|
+
var an = document.createElement('span');
|
|
233
|
+
an.className = 'nm';
|
|
234
|
+
an.textContent = '挂载插件';
|
|
235
|
+
add.appendChild(ai);
|
|
236
|
+
add.appendChild(an);
|
|
237
|
+
grid.appendChild(add);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
group.style.display = '';
|
|
241
|
+
grid.innerHTML = '';
|
|
242
|
+
dock.forEach(function (item) {
|
|
243
|
+
var b = document.createElement('button');
|
|
244
|
+
b.className = 'tile plugin';
|
|
245
|
+
b.dataset.act = 'dock';
|
|
246
|
+
b.dataset.id = item.id;
|
|
247
|
+
b.title = String(item.label || item.id);
|
|
248
|
+
var ico = document.createElement('span');
|
|
249
|
+
ico.className = 'ico';
|
|
250
|
+
ico.textContent = String(item.icon || '●');
|
|
251
|
+
var nm = document.createElement('span');
|
|
252
|
+
nm.className = 'nm';
|
|
253
|
+
nm.textContent = String(item.label || item.id);
|
|
254
|
+
b.appendChild(ico);
|
|
255
|
+
b.appendChild(nm);
|
|
256
|
+
grid.appendChild(b);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── 点击 ────────────────────────────────────────────────
|
|
261
|
+
document.addEventListener('click', function (event) {
|
|
262
|
+
var el = event.target;
|
|
263
|
+
while (el && el !== document.body) {
|
|
264
|
+
if (el.classList && el.classList.contains('tile')) break;
|
|
265
|
+
el = el.parentNode;
|
|
266
|
+
}
|
|
267
|
+
if (!el || el === document.body) return;
|
|
268
|
+
var act = el.dataset.act;
|
|
269
|
+
if (!act) return;
|
|
270
|
+
if (act === 'dock') tell('dock', { id: el.dataset.id });
|
|
271
|
+
else tell(act);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── 空白态的教学:只在"什么都没有"时说话 ────────────────
|
|
275
|
+
function paintTail() {
|
|
276
|
+
var dock = readDock();
|
|
277
|
+
var tail = document.getElementById('tail');
|
|
278
|
+
document.getElementById('hint').textContent = '点图标即可';
|
|
279
|
+
if (!dock.length) {
|
|
280
|
+
tail.textContent = '桌布是留白的 —— 它不填满,因为还能放。';
|
|
281
|
+
} else {
|
|
282
|
+
tail.textContent = '';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
paintDock();
|
|
287
|
+
paintTail();
|
|
288
|
+
})();
|
|
289
|
+
</script>
|
|
290
|
+
</body>
|
|
291
|
+
</html>
|
|
@@ -1,35 +1,124 @@
|
|
|
1
1
|
# 挂板协议
|
|
2
2
|
|
|
3
|
-
> 建立:2026-11-06 16:25 |
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
3
|
+
> 建立:2026-11-06 16:25 | **重写:2026-11-10 14:55**(补"给 Agent 的第一章")
|
|
4
|
+
> 性质:📸 **快照**(必须覆盖,写水位)| **水位:2026-11-10 14:55**
|
|
5
|
+
> **重写原因**:2026-11-10 的「**在场者测试**」—— 一台什么都不知道的新机器,五轮才用上挂板,
|
|
6
|
+
> 且造出了**纯 html 而不是挂表**。根因:**这份协议是"给我们看的技术规格",不是"给 Agent 的操作指引"。**
|
|
7
|
+
> **本文的检验标准**:**只给这份文档、撤掉所有人,一个 Agent 能不能做对。**
|
|
8
|
+
> 上游:`00-总纲.md` §三
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## 第零章 ⭐ 给 Agent 的(读完这一章就够动**手**了)
|
|
13
|
+
|
|
14
|
+
## 0.1 三个词(先立词汇)
|
|
15
|
+
|
|
16
|
+
| 词 | 定义 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| **挂板** | **专门用来盛放挂表的界面**。它是容器,不是内容。 |
|
|
19
|
+
| **挂表** | **一张能交互、能提交反馈的表** —— 能勾选、能写备注、点提交后**有东西落盘**。 |
|
|
20
|
+
| **链接** | **把一张挂表挂上挂板的动作**。("把它挂上去") |
|
|
21
|
+
|
|
22
|
+
> **⚠ 最容易错的一点**:**"一张漂亮的 HTML 页面"不是挂表。**
|
|
23
|
+
> **挂表必须能**交互 + 提交 + 落盘**。少了提交链,它就只是网页。**
|
|
24
|
+
|
|
25
|
+
## 0.2 当用户这么说,你要这么做
|
|
26
|
+
|
|
27
|
+
| 用户说 | 你做 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **"用挂表问我"** / **"做个表给我填"** / **"列出来我勾"** | **造一张挂表** → **链接到挂板** → 告诉用户"已挂到挂板,打开看" |
|
|
30
|
+
| **"给我一份 checklist,我们对照着打勾、写评语"** | 造**清单型**挂表(多行勾选 + 每行备注) |
|
|
31
|
+
| **"一份验收表,逐项验收并评价,另有评论区我要打字"** | 造**验收型**挂表(勾选网格 + 大文本区) |
|
|
32
|
+
| **"批注表,我要按段落批注"** | 造**批注型**挂表(分段落 + 每段一个文本区) |
|
|
33
|
+
| **"挂板上有什么"** | 列挂板上的表(调 `/dsh-fou/panels`) |
|
|
34
|
+
| **"在浮游里打开这个链接"** | 挂一个 `kind:"url"` 条目(见 §三) |
|
|
35
|
+
|
|
36
|
+
**→ 用户说的可能是**任何说法**。**你的判断依据不是"他用了哪个词",是"他要一张能填的表"。**
|
|
37
|
+
|
|
38
|
+
## 0.3 三个示范句(给用户看的,你照着理解意图)
|
|
39
|
+
|
|
40
|
+
> **① "给我一份 check list,我们要对照着打勾,写评语。"**
|
|
41
|
+
> **② "一份验收表,逐项验收并评价,另有评论区我要打字。"**
|
|
42
|
+
> **③ "批注表,我要按段落批注。"**
|
|
43
|
+
|
|
44
|
+
**——这三个句式覆盖三种交互形态。用户说别的,你也照这三种的骨架变。**
|
|
45
|
+
|
|
46
|
+
## 0.4 挂表长什么样(**照抄这一份,别自己发明**)
|
|
47
|
+
|
|
48
|
+
**参考实现**:`accept.html`(同仓库根目录,19 KB,**是唯一被验证过的挂表**)。
|
|
49
|
+
|
|
50
|
+
**它由五条链组成,一条都不能少**:
|
|
51
|
+
|
|
52
|
+
| # | 链 | 在 `accept.html` 里对应 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **1** | **标题 + 说明** | `<h1 id="sheetTitle">` + `.note` |
|
|
55
|
+
| **2** | **可勾选的行**(每行:勾选 + 备注) | `<table id="grid">` |
|
|
56
|
+
| **3** | **自由文本区**(可多个,如 评论区/优化区/话痨区) | 三个 `<textarea>` |
|
|
57
|
+
| **4** | **[提交] 按钮 → POST** | `fetch('/dsh-fou/accept', {method:'POST', …})` |
|
|
58
|
+
| **5** | **草稿自动存**(localStorage,刷新不丢) | `saveDraft()` / `loadDraft()` |
|
|
59
|
+
|
|
60
|
+
**第 4 条是最容易漏的** —— **漏了它,你得到的就是"纯 html",不是挂表。**
|
|
61
|
+
|
|
62
|
+
> # **判据:没有"提交后落盘"的表,不算挂表。**
|
|
63
|
+
|
|
64
|
+
## 0.5 怎么算做对了(当场能验)
|
|
65
|
+
|
|
66
|
+
1. **挂板里能看到它**(打开挂板 → 有这一条,标着「验收表 / 批注表 / 清单」之类)
|
|
67
|
+
2. **能勾、能写字**(鼠标点得动)
|
|
68
|
+
3. **点提交 → 有回应**(页面提示已保存 / 或生成了文件)
|
|
69
|
+
4. **`D:\dsh-work\_挂板\` 下多了文件**(`.json` + `.md`)
|
|
70
|
+
|
|
71
|
+
**四条全中才算成。** 只中前两条 = 你造的只是个网页。
|
|
72
|
+
|
|
73
|
+
## 0.6 做错了怎么办
|
|
74
|
+
|
|
75
|
+
| 现象 | 原因 | 怎么修 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| **挂板里是个纯网页,勾不了** | **自己发明了 HTML 结构** | 回去照 §0.4 五条链重做,或直接复制 `accept.html` 改内容 |
|
|
78
|
+
| **能勾,但点提交没反应** | **没有 POST 那一步** | 加 `fetch` 到 `/dsh-fou/accept`(或你自己的路由) |
|
|
79
|
+
| **提交了,但没文件** | **POST 的 body 格式不对** | 照 `accept.html` 的提交体抄 |
|
|
80
|
+
| **挂板里根本没有** | **只生成了文件,没链接到挂板** | 调 `POST /dsh-fou/hang` 把它挂上去(见 §三) |
|
|
81
|
+
| **刷新后填的东西没了** | 没做草稿自动存 | 加 `localStorage` 存/读 |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 第一章 一句话
|
|
12
86
|
|
|
13
87
|
> **挂板的能力是「挂」;它只管挂得住、看得见,不管挂的是什么。**
|
|
14
88
|
|
|
15
89
|
---
|
|
16
90
|
|
|
17
|
-
##
|
|
91
|
+
## 第二章 形式开放(核心决定)
|
|
18
92
|
|
|
19
93
|
**挂件类型**:
|
|
20
94
|
|
|
21
95
|
| kind | 内容 | 落盘 |
|
|
22
96
|
|---|---|---|
|
|
23
|
-
| `html` | 一页 HTML | ✅ 存内容 |
|
|
97
|
+
| `html` | 一页 HTML(**挂表就在这一类**) | ✅ 存内容 |
|
|
24
98
|
| `md` / `text` / `json` | 文本 | ✅ 存内容 |
|
|
25
99
|
| `path` 类(md/html 用 `path`) | **只读引用一个文件** | ❌ **不复制**,改了就变 |
|
|
26
100
|
| **`url`** | **只记地址** | ❌ **不复制,永远最新** |
|
|
27
101
|
|
|
28
102
|
**认不出的 `kind` 一律当 `text`,不报错** —— **"挂"不该因为格式失败。**
|
|
29
103
|
|
|
104
|
+
## 挂表是 `html` 的一个**子集**(2026-11-10 明确)
|
|
105
|
+
|
|
106
|
+
**挂表 = `kind:"html"` 且 `form: true`。**
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "id": "…", "title": "…", "kind": "html", "file": "x.html",
|
|
110
|
+
"form": true, "formKind": "accept" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**`formKind` 是**自由文本**(`accept` / `annotate` / `review` / `reply` / 你自己起的名字都行)
|
|
114
|
+
—— 挂板只拿它做首标签,**认不出的照样显示原值,不会隐藏。**
|
|
115
|
+
|
|
116
|
+
> # **我们定的只是"进门的规矩",不是"屋里的摆法"。**
|
|
117
|
+
> **——鼓励用户创造自己的挂表。**
|
|
118
|
+
|
|
30
119
|
---
|
|
31
120
|
|
|
32
|
-
##
|
|
121
|
+
## 第三章 外部 URL 可作为挂件
|
|
33
122
|
|
|
34
123
|
**契约**:
|
|
35
124
|
|
|
@@ -47,24 +136,16 @@ body: { "title": "任意标题", "kind": "url", "url": "http://127.0.0.1:端口/
|
|
|
47
136
|
3. **任何 HTTP 地址都可以**(本机服务 / 外网页面)
|
|
48
137
|
4. **同一 `id` 重挂 = 覆盖**(不重复出现)
|
|
49
138
|
|
|
50
|
-
|
|
139
|
+
**验证过的实例(2026-11-06)**:
|
|
51
140
|
|
|
52
141
|
| 挂件 | 地址 | 用途 |
|
|
53
142
|
|---|---|---|
|
|
54
143
|
| `pedia-docs-8799` | `http://127.0.0.1:8799/` | 百科的官方文档搜索(649 篇) |
|
|
55
144
|
| `accept-*` | `/dsh-fou/accept?sheet=…` | 验收表(本插件自己的页面) |
|
|
56
145
|
|
|
57
|
-
**这就是"官方浮窗做不到、挂板能做到"的那条**:**官方浮窗只装文件;挂板能装 URL 与活页面。**
|
|
58
|
-
|
|
59
146
|
---
|
|
60
147
|
|
|
61
|
-
##
|
|
62
|
-
|
|
63
|
-
**需求(用户原话)**:
|
|
64
|
-
|
|
65
|
-
> **"左键单击/右键选择 URL 直接在浮游中打开。我预估这是低本高收的。"**
|
|
66
|
-
|
|
67
|
-
**契约(要有,先定)**:
|
|
148
|
+
## 第四章 链接入口:右键「在浮游打开」
|
|
68
149
|
|
|
69
150
|
> **任何在 DSH 界面里可右键的链接,菜单里应有一项「在浮游打开」;**
|
|
70
151
|
> **选中后,等价于调一次 `hang({ kind:'url', url: <该链接> })`。**
|
|
@@ -75,37 +156,40 @@ body: { "title": "任意标题", "kind": "url", "url": "http://127.0.0.1:端口/
|
|
|
75
156
|
|---|---|---|
|
|
76
157
|
| 多标签 | ✅ | ✅(挂板可并列) |
|
|
77
158
|
| 退出保存 | ✅ | ✅(index.json) |
|
|
78
|
-
| 标签页↔浮窗互转 | ✅ | ✅(挂板本身就是浮游里的) |
|
|
79
159
|
| **换会话还在吗** | ❌ **没了** | ✅ **还在** |
|
|
80
160
|
|
|
81
161
|
> **差异只有一条,但它是根:右栏跟会话,浮游跟人。**
|
|
82
162
|
|
|
83
|
-
|
|
163
|
+
**实现约束**:
|
|
84
164
|
|
|
85
|
-
1. **只做右键,不做左键单击**
|
|
86
|
-
2.
|
|
87
|
-
3. 若官方将来自己加「在浮游打开」,**让官方**(见总纲
|
|
165
|
+
1. **只做右键,不做左键单击**
|
|
166
|
+
2. **不改官方菜单的既有三项**
|
|
167
|
+
3. 若官方将来自己加「在浮游打开」,**让官方**(见总纲 §三④)
|
|
168
|
+
|
|
169
|
+
**✅ 已实现(2026-11-08)**:真实目标家族是 `url`(值 `{sessionId,url}`)/ `file` / `directory` / `image` / `tab` / `session`;`resolve` 收到的是 **`value` 直接体**。
|
|
88
170
|
|
|
89
171
|
---
|
|
90
172
|
|
|
91
|
-
##
|
|
173
|
+
## 第五章 挂上去之后怎么活(L2)
|
|
92
174
|
|
|
93
175
|
**两条约定,别的一条都不用管**:
|
|
94
176
|
|
|
95
|
-
|
|
177
|
+
## 5.1 结果落盘:挂件自己 POST 自己的路由
|
|
96
178
|
|
|
97
179
|
**挂板不帮忙存档** —— 它不知道你的数据长什么样。
|
|
98
180
|
|
|
99
|
-
|
|
181
|
+
**挂表的参考实现**:`POST /dsh-fou/accept` → 写成 `<时间>.json` + `<时间>.md`,并更新 `latest`。
|
|
182
|
+
|
|
183
|
+
## 5.2 要通知会话:发一条 `fou:` 开头的消息
|
|
100
184
|
|
|
101
185
|
```js
|
|
102
186
|
window.parent.postMessage({ type: 'fou:your-event', … }, window.location.origin);
|
|
103
187
|
```
|
|
104
188
|
|
|
105
|
-
- **`type` 必须以 `fou:` 开头** ——
|
|
189
|
+
- **`type` 必须以 `fou:` 开头** —— 挂板会原样往上转一层给浮游
|
|
106
190
|
- **已知可用**:`fou:accept-saved`({ notify, notifyName, summary, mdPath })
|
|
107
191
|
|
|
108
|
-
|
|
192
|
+
## 5.3 通知对象从哪来:URL 参数
|
|
109
193
|
|
|
110
194
|
**浮游打开挂板时会带** `?notify=<会话id>¬ifyName=<名字>`,**挂板自动转给 `url` 挂件**。
|
|
111
195
|
|
|
@@ -113,7 +197,7 @@ window.parent.postMessage({ type: 'fou:your-event', … }, window.location.origi
|
|
|
113
197
|
|
|
114
198
|
---
|
|
115
199
|
|
|
116
|
-
##
|
|
200
|
+
## 第六章 五条纪律
|
|
117
201
|
|
|
118
202
|
1. **软依赖** —— 挂板不在,你的东西照样能用
|
|
119
203
|
2. **不替你容错** —— 挂件内部报错由挂件自己负责
|
|
@@ -123,11 +207,23 @@ window.parent.postMessage({ type: 'fou:your-event', … }, window.location.origi
|
|
|
123
207
|
|
|
124
208
|
---
|
|
125
209
|
|
|
126
|
-
##
|
|
210
|
+
## 第七章 已知未做
|
|
127
211
|
|
|
128
212
|
| # | 事 | 状态 |
|
|
129
213
|
|---|---|---|
|
|
130
|
-
| 1 |
|
|
131
|
-
| 2 |
|
|
132
|
-
| 3 |
|
|
133
|
-
| 4 |
|
|
214
|
+
| 1 | **界面空白态提示**(浏览/挂表/挂载三处,各给一句可复制的命令) | 🔴 未做(见 `60-设计-引导四格.md`) |
|
|
215
|
+
| 2 | **checklist / 评分表模板**(补齐第三、四种交互形态) | 🔴 未做 |
|
|
216
|
+
| 3 | 挂件「Open with」(用外部应用打开) | 🔴 未做 |
|
|
217
|
+
| 4 | 错误兜底四种(找不到/太大/非文本/服务不可用) | 🟡 提示很薄 |
|
|
218
|
+
| 5 | 挂件排序 / 分组 | 🔴 现在只有"最新在前" |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 附录:在场者测试(怎么检验这份协议是否完整)
|
|
223
|
+
|
|
224
|
+
> **撤掉所有在场者,只给这份文档,让一个什么都不知道的 Agent 做一遍。**
|
|
225
|
+
> **它做对了 → 协议完整;它做错了 → 错的那一步就是协议的空白。**
|
|
226
|
+
|
|
227
|
+
**2026-11-10 实测结论**:旧版协议**失败**(造出纯 html)。
|
|
228
|
+
**根因**:协议里没写"挂表必须有提交链"、没写"照抄 `accept.html`"、没写"怎么算做对"。
|
|
229
|
+
**本章(第零章)就是补这三样。**
|
package/index.js
CHANGED
|
@@ -23,6 +23,8 @@ const ROUTES = {
|
|
|
23
23
|
sheet: '/dsh-fou/sheet',
|
|
24
24
|
panels: '/dsh-fou/panels',
|
|
25
25
|
panelsPage: '/dsh-fou/panels.html',
|
|
26
|
+
deskPage: '/dsh-fou/desk.html',
|
|
27
|
+
settingsPage: '/dsh-fou/settings.html',
|
|
26
28
|
panel: '/dsh-fou/panel',
|
|
27
29
|
hang: '/dsh-fou/hang',
|
|
28
30
|
editor: '/dsh-fou/editor.html',
|
|
@@ -41,6 +43,8 @@ const MAX_PANEL_BYTES = 4 * 1024 * 1024
|
|
|
41
43
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
42
44
|
const TABLE_FILE = join(HERE, 'accept.html')
|
|
43
45
|
const PANELS_FILE = join(HERE, 'panels.html')
|
|
46
|
+
const DESK_FILE = join(HERE, 'desk.html')
|
|
47
|
+
const SETTINGS_FILE = join(HERE, 'settings.html')
|
|
44
48
|
const EDITOR_FILE = join(HERE, 'editor.html')
|
|
45
49
|
const VIEW_FILE = join(HERE, 'view.html')
|
|
46
50
|
const DRAFT_DIR_NAME = '_草稿'
|
|
@@ -670,6 +674,38 @@ export function apply(ctx, config) {
|
|
|
670
674
|
}
|
|
671
675
|
},
|
|
672
676
|
],
|
|
677
|
+
[
|
|
678
|
+
// 桌面(2026-11-10 用户:"右键打开一个桌面")
|
|
679
|
+
// 它是"摊开的菜单":原来右键里藏着的东西,全摆在这一页上。
|
|
680
|
+
ROUTES.deskPage,
|
|
681
|
+
async (req, res) => {
|
|
682
|
+
if (req.method !== 'GET') {
|
|
683
|
+
sendJSON(res, 405, { error: '只支持 GET' })
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
sendHTML(res, 200, await readFile(DESK_FILE, 'utf8'))
|
|
688
|
+
} catch (error) {
|
|
689
|
+
sendHTML(res, 500, '<h1>桌面页缺失</h1><p>' + DESK_FILE + '</p>')
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
[
|
|
694
|
+
// 设置页(2026-11-10 用户第 1 条):点 ⚙ 打开的是"一页",不是"一个菜单"。
|
|
695
|
+
// 判据:菜单回答"能做什么",设置页回答"现在是什么"。
|
|
696
|
+
ROUTES.settingsPage,
|
|
697
|
+
async (req, res) => {
|
|
698
|
+
if (req.method !== 'GET') {
|
|
699
|
+
sendJSON(res, 405, { error: '只支持 GET' })
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
sendHTML(res, 200, await readFile(SETTINGS_FILE, 'utf8'))
|
|
704
|
+
} catch (error) {
|
|
705
|
+
sendHTML(res, 500, '<h1>设置页缺失</h1><p>' + SETTINGS_FILE + '</p>')
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
],
|
|
673
709
|
[
|
|
674
710
|
ROUTES.panels,
|
|
675
711
|
async (req, res) => {
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A floating container layer over DSH: chat without leaving the page, travel between sessions, and open anything — a web page, a file, or a plugin's own UI — in one pane. · 浮游:DSH 之上的一层悬浮容器 —— 不离开当前位置,就能碰到别处。",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/DSL-Orange/dsh-fou.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/DSL-Orange/dsh-fou#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/DSL-Orange/dsh-fou/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
7
16
|
"exports": {
|
|
8
17
|
".": "./index.js",
|
|
9
18
|
"./client": "./client.js",
|
|
@@ -16,6 +25,8 @@
|
|
|
16
25
|
"panels.html",
|
|
17
26
|
"editor.html",
|
|
18
27
|
"view.html",
|
|
28
|
+
"desk.html",
|
|
29
|
+
"settings.html",
|
|
19
30
|
"cordis.patch.yml",
|
|
20
31
|
"locale/*.json",
|
|
21
32
|
"README.md",
|
package/panels.html
CHANGED
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
iframe { flex:1; border:0; background:#fff; }
|
|
31
31
|
pre { flex:1; margin:0; padding:14px 16px; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere; font:12.5px/1.6 ui-monospace,Consolas,"Cascadia Mono",monospace; background:#fff; }
|
|
32
32
|
.empty { margin:auto; color:var(--dim); text-align:center; padding:24px; line-height:1.9; }
|
|
33
|
+
.empty .say { margin:6px auto; max-width:420px; padding:5px 10px; border:1px dashed var(--line); border-radius:8px; color:#1f2937; background:#fafbfd; font-size:12px; }
|
|
34
|
+
.empty .also { margin-top:10px; font-size:11px; }
|
|
35
|
+
.empty b { color:#111827; }
|
|
33
36
|
.empty code { background:#eef1f5; padding:1px 5px; border-radius:4px; }
|
|
34
37
|
.err { color:#b91c1c; padding:14px 16px; }
|
|
35
38
|
</style>
|
|
@@ -47,8 +50,12 @@
|
|
|
47
50
|
<div class="body" id="body">
|
|
48
51
|
<div class="empty" id="empty">
|
|
49
52
|
挂板是空的。<br />
|
|
50
|
-
|
|
51
|
-
<
|
|
53
|
+
<b>对任意会话说下面任一句</b>,就能得到一张能填、能提交的表:
|
|
54
|
+
<div class="say">「给我一份 check list,我们对照着打勾、写评语」</div>
|
|
55
|
+
<div class="say">「一份验收表,逐项验收并评价,另有评论区我要打字」</div>
|
|
56
|
+
<div class="say">「批注表,我要按段落批注」</div>
|
|
57
|
+
<div class="also">也可以说「把这份内容挂到挂板上」——任何 <code>html</code> / <code>md</code> / <code>json</code> / 纯文本都行。</div>
|
|
58
|
+
<span style="font-size:10px;opacity:.5">(技术接口:<code>POST /dsh-fou/hang</code>)</span>
|
|
52
59
|
</div>
|
|
53
60
|
</div>
|
|
54
61
|
</main>
|
package/settings.html
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>设置 · 浮游</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ── 设置页(2026-11-10 用户第 1 条)
|
|
9
|
+
原来点 ⚙ 弹的是"右键页"(一个菜单),这是错的:
|
|
10
|
+
· 菜单是"动作列表",设置是"一组当前值"。
|
|
11
|
+
· 动作点完就消失;设置要**显示现在的状态**。
|
|
12
|
+
判据:**菜单回答"能做什么",设置页回答"现在是什么"。**
|
|
13
|
+
|
|
14
|
+
用户自己也说了:"尽管它现在会很空旷。"
|
|
15
|
+
—— 空旷是对的:设置项少,说明浮游自己该做的决定少。
|
|
16
|
+
*/
|
|
17
|
+
:root {
|
|
18
|
+
--ink: #1f2937;
|
|
19
|
+
--dim: #6b7280;
|
|
20
|
+
--line: #e5e7eb;
|
|
21
|
+
--card: #fff;
|
|
22
|
+
--accent: #2563eb;
|
|
23
|
+
}
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
html, body { height: 100%; margin: 0; }
|
|
26
|
+
body {
|
|
27
|
+
font: 13px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
|
28
|
+
color: var(--ink);
|
|
29
|
+
background: linear-gradient(180deg, #fbfbfc 0%, #f7f8fa 100%);
|
|
30
|
+
overflow: auto;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
header {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
padding: 10px 14px 8px;
|
|
38
|
+
border-bottom: 1px solid var(--line);
|
|
39
|
+
background: rgba(255, 255, 255, .7);
|
|
40
|
+
backdrop-filter: blur(8px);
|
|
41
|
+
position: sticky;
|
|
42
|
+
top: 0;
|
|
43
|
+
z-index: 2;
|
|
44
|
+
}
|
|
45
|
+
header .title { font-weight: 600; font-size: 13px; }
|
|
46
|
+
header .grow { flex: 1; }
|
|
47
|
+
header .hint { color: var(--dim); font-size: 11px; }
|
|
48
|
+
|
|
49
|
+
main { padding: 14px; display: flex; flex-direction: column; gap: 14px; }
|
|
50
|
+
|
|
51
|
+
.sec {
|
|
52
|
+
background: var(--card);
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
box-shadow: 0 1px 2px #0000000a, 0 4px 14px #0000000d;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
.sec > .label {
|
|
58
|
+
padding: 9px 14px 4px;
|
|
59
|
+
color: var(--dim);
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
letter-spacing: .04em;
|
|
62
|
+
}
|
|
63
|
+
.row {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 10px;
|
|
67
|
+
padding: 10px 14px;
|
|
68
|
+
border-top: 1px solid #f1f3f6;
|
|
69
|
+
}
|
|
70
|
+
.row:first-of-type { border-top: 0; }
|
|
71
|
+
.row .name { flex: 1; }
|
|
72
|
+
.row .desc { color: var(--dim); font-size: 11px; }
|
|
73
|
+
|
|
74
|
+
/* 三选一:形态 */
|
|
75
|
+
.choice { display: flex; gap: 6px; }
|
|
76
|
+
.choice button {
|
|
77
|
+
border: 1px solid var(--line);
|
|
78
|
+
background: #fff;
|
|
79
|
+
color: var(--ink);
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
padding: 5px 12px;
|
|
82
|
+
font: inherit;
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
transition: all .12s ease;
|
|
86
|
+
}
|
|
87
|
+
.choice button:hover { border-color: #c7cfda; }
|
|
88
|
+
.choice button.on {
|
|
89
|
+
border-color: var(--accent);
|
|
90
|
+
color: var(--accent);
|
|
91
|
+
background: #eff4ff;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* 动作按钮 */
|
|
96
|
+
.act {
|
|
97
|
+
border: 1px solid var(--line);
|
|
98
|
+
background: #fff;
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
padding: 5px 12px;
|
|
101
|
+
font: inherit;
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
color: var(--ink);
|
|
105
|
+
}
|
|
106
|
+
.act:hover { background: #f7f8fa; border-color: #c7cfda; }
|
|
107
|
+
|
|
108
|
+
/* 说明 */
|
|
109
|
+
.foot {
|
|
110
|
+
color: #b6bcc6;
|
|
111
|
+
font-size: 11px;
|
|
112
|
+
text-align: center;
|
|
113
|
+
padding: 6px 0 20px;
|
|
114
|
+
line-height: 1.8;
|
|
115
|
+
}
|
|
116
|
+
.foot code { background: #eef1f5; padding: 1px 5px; border-radius: 4px; color: #4b5563; }
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<header>
|
|
121
|
+
<span class="title">设置</span>
|
|
122
|
+
<span class="grow"></span>
|
|
123
|
+
<span class="hint" id="hint"></span>
|
|
124
|
+
</header>
|
|
125
|
+
|
|
126
|
+
<main>
|
|
127
|
+
<!-- 位置 -->
|
|
128
|
+
<div class="sec">
|
|
129
|
+
<div class="label">位置</div>
|
|
130
|
+
<div class="row">
|
|
131
|
+
<span class="name">把它送回原处
|
|
132
|
+
<div class="desc">拖动过后忘了在哪,点这个</div>
|
|
133
|
+
</span>
|
|
134
|
+
<button class="act" data-act="reset">复位位置</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="foot" id="foot">
|
|
139
|
+
这里只剩一件事 —— 因为它别处做不到。<br>
|
|
140
|
+
形态在胶囊上滚一下就有,收起就是切到球。
|
|
141
|
+
</div>
|
|
142
|
+
</main>
|
|
143
|
+
|
|
144
|
+
<script>
|
|
145
|
+
(function () {
|
|
146
|
+
'use strict';
|
|
147
|
+
|
|
148
|
+
function tell(action, extra) {
|
|
149
|
+
var msg = { type: 'fou:setting', action: action };
|
|
150
|
+
if (extra) for (var k in extra) msg[k] = extra[k];
|
|
151
|
+
try { window.parent.postMessage(msg, window.location.origin); } catch (e) { /* 不在浮游里 */ }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 当前形态从 URL 来:?mode=orb|pill|panel
|
|
155
|
+
function currentMode() {
|
|
156
|
+
try { return new URLSearchParams(location.search).get('mode') || ''; } catch (e) { return ''; }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function paintMode() {
|
|
160
|
+
var cur = currentMode();
|
|
161
|
+
var btns = document.querySelectorAll('#modeChoice button');
|
|
162
|
+
for (var i = 0; i < btns.length; i++) {
|
|
163
|
+
var on = btns[i].dataset.mode === cur;
|
|
164
|
+
btns[i].classList.toggle('on', on);
|
|
165
|
+
}
|
|
166
|
+
document.getElementById('hint').textContent = cur ? '' : '点一下就换';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
document.addEventListener('click', function (event) {
|
|
170
|
+
var el = event.target;
|
|
171
|
+
var btn = null;
|
|
172
|
+
while (el && el !== document.body) {
|
|
173
|
+
if (el.tagName === 'BUTTON') { btn = el; break; }
|
|
174
|
+
el = el.parentNode;
|
|
175
|
+
}
|
|
176
|
+
if (!btn) return;
|
|
177
|
+
if (btn.dataset.mode) { tell('mode', { value: btn.dataset.mode }); return; }
|
|
178
|
+
if (btn.dataset.act) { tell(btn.dataset.act); return; }
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
paintMode();
|
|
182
|
+
})();
|
|
183
|
+
</script>
|
|
184
|
+
</body>
|
|
185
|
+
</html>
|