@clowder-ai/companion 0.1.0-alpha.2 → 0.1.0-alpha.20
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/NOTICE +10 -2
- package/README.md +59 -0
- package/manifest.json +5 -5
- package/package.json +19 -6
- package/plugin.yaml +5 -5
- package/renderer/call-transcript-view.mjs +238 -0
- package/renderer/client.mjs +13 -1
- package/renderer/companion-identity.mjs +42 -0
- package/renderer/conversation.mjs +144 -24
- package/renderer/decision-view.mjs +124 -0
- package/renderer/errors.mjs +4 -4
- package/renderer/index.html +168 -26
- package/renderer/living-body.mjs +147 -0
- package/renderer/living-edge.mjs +16 -0
- package/renderer/living-geometry.mjs +96 -0
- package/renderer/motion-facts.mjs +108 -0
- package/renderer/native-window-travel.mjs +89 -0
- package/renderer/native-work-motion.mjs +39 -0
- package/renderer/peer.mjs +1 -1
- package/renderer/pet-controls.mjs +130 -0
- package/renderer/pet-motion-assets.mjs +22 -0
- package/renderer/pet-motion.mjs +343 -0
- package/renderer/recent-bubble.mjs +74 -0
- package/renderer/settings-controller.mjs +171 -0
- package/renderer/settings-view.mjs +234 -0
- package/renderer/skins/ragdoll-v1.png +0 -0
- package/renderer/skins/xianxian-codex.webp +0 -0
- package/renderer/skins/xianxian-living/clips/carry.webm +0 -0
- package/renderer/skins/xianxian-living/clips/peek.webm +0 -0
- package/renderer/skins/xianxian-living/clips/peek_fade.webm +0 -0
- package/renderer/skins/xianxian-living/clips/pounce.webm +0 -0
- package/renderer/skins/xianxian-living/clips/sleep.webm +0 -0
- package/renderer/skins/xianxian-living/clips/think.webm +0 -0
- package/renderer/skins/xianxian-living/clips/trot.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wait.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wake.webm +0 -0
- package/renderer/skins/xianxian-living/layers/body.webp +0 -0
- package/renderer/skins/xianxian-living/layers/closed.webp +0 -0
- package/renderer/skins/xianxian-living/layers/earL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/earR.webp +0 -0
- package/renderer/skins/xianxian-living/layers/head.webp +0 -0
- package/renderer/skins/xianxian-living/layers/irisL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/irisR.webp +0 -0
- package/renderer/skins/xianxian-living/layers/meta.json +88 -0
- package/renderer/skins/xianxian-living/layers/mouth.webp +0 -0
- package/renderer/skins/xianxian-living/layers/scared.webp +0 -0
- package/renderer/skins/xianxian-living/layers/tail.webp +0 -0
- package/renderer/skins/xianxian-living/layers/whiteL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/whiteR.webp +0 -0
- package/renderer/skins/xianxian-sleeping-row.png +0 -0
- package/renderer/skins/xianxian-staged-thought-row.png +0 -0
- package/renderer/skins/xianxian-working-row.png +0 -0
- package/renderer/skins/yarn-ball.png +0 -0
- package/renderer/style.css +240 -308
- package/renderer/surface.mjs +350 -69
- package/renderer/transcript-view.mjs +60 -7
- package/source-lock.json +138 -4
|
@@ -7,66 +7,167 @@ export class CompanionConversation {
|
|
|
7
7
|
this.active = false;
|
|
8
8
|
this.generation = 0;
|
|
9
9
|
this.phase = 'idle';
|
|
10
|
+
this.audioMode = null;
|
|
11
|
+
this.retryMode = null;
|
|
10
12
|
this.muted = false;
|
|
11
13
|
this.silent = false;
|
|
14
|
+
this.failed = false;
|
|
15
|
+
this.recovering = false;
|
|
16
|
+
this.retiredCallId = null;
|
|
12
17
|
}
|
|
13
18
|
show(message) {
|
|
14
19
|
this.message = message;
|
|
15
|
-
this.render({ phase: this.phase, identity: this.identity,
|
|
20
|
+
this.render({ phase: this.phase, identity: this.identity, audioMode: this.audioMode, retryMode: this.retryMode,
|
|
21
|
+
muted: this.muted, silent: this.silent, failed: this.failed, recovering: this.recovering,
|
|
22
|
+
callId: this.callId, message });
|
|
16
23
|
}
|
|
17
24
|
current(generation) { return this.active && generation === this.generation; }
|
|
18
|
-
listening() {
|
|
19
|
-
|
|
25
|
+
listening() {
|
|
26
|
+
if (this.audioMode === 'receive_only') return '只听模式 · 麦克风未启用';
|
|
27
|
+
return this.muted ? '麦克风已静音' : '正在听,直接说话就好';
|
|
28
|
+
}
|
|
29
|
+
retrying(prefix) {
|
|
30
|
+
return this.audioMode === 'receive_only'
|
|
31
|
+
? `${prefix} · 麦克风未启用,点击只听重试`
|
|
32
|
+
: `${prefix} · 点击语音通话重试`;
|
|
33
|
+
}
|
|
34
|
+
stoppedMessage() {
|
|
35
|
+
return this.audioMode === 'receive_only'
|
|
36
|
+
? '只听已结束 · 麦克风未启用'
|
|
37
|
+
: '语音通话已结束 · 麦克风已关闭';
|
|
38
|
+
}
|
|
39
|
+
expectHostStop() {
|
|
40
|
+
if (!this.active) return false;
|
|
41
|
+
this.expectedHostStop = { generation: this.generation, pending: true };
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
async settleExpectedHostStop(callStopped) {
|
|
45
|
+
const expected = this.expectedHostStop;
|
|
46
|
+
if (!expected || expected.generation !== this.generation) return;
|
|
47
|
+
if (callStopped === false) {
|
|
48
|
+
this.expectedHostStop = undefined;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (callStopped === null) {
|
|
52
|
+
expected.pending = false;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.expectedHostStop = undefined;
|
|
56
|
+
if (!this.active) return;
|
|
57
|
+
this.failed = false;
|
|
58
|
+
await this.releaseLocal(this.stoppedMessage());
|
|
59
|
+
}
|
|
60
|
+
async begin(mode = 'duplex') {
|
|
20
61
|
if (this.active || this.stopping || this.changingDocuments) return;
|
|
62
|
+
if (mode === 'receive_only' && !this.identity?.audio?.supportedModes?.includes(mode)) return;
|
|
63
|
+
if (!['duplex', 'receive_only'].includes(mode)) return;
|
|
21
64
|
this.active = true;
|
|
65
|
+
this.audioMode = mode;
|
|
66
|
+
this.retryMode = null;
|
|
67
|
+
this.failed = false;
|
|
68
|
+
this.recovering = false;
|
|
22
69
|
this.phase = 'connecting';
|
|
70
|
+
this.callId = null;
|
|
71
|
+
this.expectedHostStop = undefined;
|
|
23
72
|
const generation = ++this.generation;
|
|
24
73
|
// Must reach the isolated preload while the click still has user activation.
|
|
25
74
|
const preparation = this.client.prepare();
|
|
26
75
|
this.show('正在连接…');
|
|
27
|
-
this.deadline = setTimeout(() => { if (this.current(generation)) void this.end('连接超时
|
|
76
|
+
this.deadline = setTimeout(() => { if (this.current(generation)) void this.end(this.retrying('连接超时'), true); }, 60_000);
|
|
28
77
|
try {
|
|
29
78
|
const peer = this.createPeer(event => {
|
|
30
79
|
if (!this.current(generation)) return;
|
|
31
|
-
if (event.type === '
|
|
32
|
-
if (event.type === '
|
|
33
|
-
|
|
80
|
+
if (event.type === 'error' && !this.callId && event.callId === this.retiredCallId) return;
|
|
81
|
+
if (event.type === 'connected') {
|
|
82
|
+
if (this.callId && this.callId !== event.callId) return;
|
|
83
|
+
this.callId = event.callId;
|
|
84
|
+
this.recovering = false;
|
|
85
|
+
clearTimeout(this.deadline); this.phase = 'talking'; this.show(this.listening());
|
|
86
|
+
}
|
|
87
|
+
if (event.type === 'error' && (!event.callId || !this.callId || event.callId === this.callId)) {
|
|
88
|
+
void this.end(explainError(event), true);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!this.callId || event.callId !== this.callId) return;
|
|
92
|
+
if (event.type === 'recovering') {
|
|
93
|
+
this.recovering = true;
|
|
94
|
+
this.show('恢复后通话继续,无需重新连接');
|
|
95
|
+
}
|
|
96
|
+
if (event.type === 'recovered') {
|
|
97
|
+
this.recovering = false;
|
|
98
|
+
this.show(this.listening());
|
|
99
|
+
}
|
|
34
100
|
if (event.type === 'transcript' || event.type === 'turn-done') this.transcript(event);
|
|
35
|
-
if (event.type === 'error') void this.end(explainError(event));
|
|
36
101
|
});
|
|
37
102
|
this.peer = peer;
|
|
38
|
-
|
|
103
|
+
if (mode === 'duplex') peer.muteMic(this.muted);
|
|
104
|
+
peer.muteSpeaker(this.silent);
|
|
39
105
|
// Submit capture intent while this same click is active. The Host waits
|
|
40
106
|
// for its matching preparation before creating the media document.
|
|
41
|
-
const [identity] = await Promise.all([preparation, peer.connect()]);
|
|
107
|
+
const [identity] = await Promise.all([preparation, peer.connect(mode === 'receive_only' ? mode : undefined)]);
|
|
42
108
|
if (!this.current(generation)) { await peer.close(); return; }
|
|
43
109
|
if (identity.phase !== 'ready') throw { code: 'unavailable' };
|
|
110
|
+
if (mode === 'receive_only' && !identity.audio?.supportedModes?.includes(mode)) throw { code: 'unavailable' };
|
|
44
111
|
this.identity = identity;
|
|
45
112
|
this.show(this.message);
|
|
46
113
|
} catch (error) {
|
|
47
|
-
if (this.current(generation)) await this.end(explainError(error));
|
|
114
|
+
if (this.current(generation)) await this.end(explainError(error), true);
|
|
48
115
|
}
|
|
49
116
|
}
|
|
50
117
|
async releaseLocal(message) {
|
|
51
118
|
this.active = false;
|
|
119
|
+
this.expectedHostStop = undefined;
|
|
120
|
+
if (this.callId) this.retiredCallId = this.callId;
|
|
52
121
|
++this.generation;
|
|
53
122
|
this.pendingText = undefined;
|
|
54
123
|
clearTimeout(this.deadline);
|
|
55
124
|
const peer = this.peer;
|
|
56
125
|
this.peer = undefined;
|
|
126
|
+
const releasedMode = this.audioMode;
|
|
127
|
+
this.audioMode = null;
|
|
128
|
+
this.retryMode = this.failed ? releasedMode : null;
|
|
57
129
|
this.phase = 'idle';
|
|
130
|
+
this.recovering = false;
|
|
131
|
+
this.callId = null;
|
|
58
132
|
this.show(message);
|
|
59
133
|
await Promise.allSettled([peer?.close(), this.stopScreen()]);
|
|
60
134
|
}
|
|
61
|
-
async end(message =
|
|
135
|
+
async end(message, failed = false) {
|
|
62
136
|
if (this.stopping) return this.stopping;
|
|
137
|
+
const receiveOnly = this.audioMode === 'receive_only';
|
|
138
|
+
message ??= this.stoppedMessage();
|
|
139
|
+
this.failed = failed;
|
|
63
140
|
const local = this.releaseLocal(message);
|
|
64
141
|
const operation = Promise.all([local, this.client.stop()]);
|
|
65
142
|
this.stopping = operation;
|
|
66
143
|
try { await operation; }
|
|
67
|
-
catch { this.show('麦克风已关闭 · 连接收尾尚未确认'); }
|
|
144
|
+
catch { this.show(receiveOnly ? '麦克风未启用 · 连接收尾尚未确认' : '麦克风已关闭 · 连接收尾尚未确认'); }
|
|
68
145
|
finally { if (this.stopping === operation) this.stopping = undefined; }
|
|
69
146
|
}
|
|
147
|
+
async hostStopped(reason) {
|
|
148
|
+
// Stop echoes must not erase a more useful local failure or manual ending.
|
|
149
|
+
if (!this.active) return;
|
|
150
|
+
if (reason === 'revoked' && this.expectedHostStop?.generation === this.generation) {
|
|
151
|
+
this.expectedHostStop = undefined;
|
|
152
|
+
this.failed = false;
|
|
153
|
+
await this.releaseLocal(this.stoppedMessage());
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const receiveOnly = this.audioMode === 'receive_only';
|
|
157
|
+
const messages = receiveOnly ? {
|
|
158
|
+
locked: '屏幕已锁定 · 只听已停止,解锁后可点击只听继续',
|
|
159
|
+
suspended: '电脑已休眠 · 只听已停止,可点击只听继续',
|
|
160
|
+
hidden: '猫猫已收起 · 只听已停止',
|
|
161
|
+
} : {
|
|
162
|
+
locked: '屏幕已锁定 · 语音通话已停止,解锁后可点击语音通话继续',
|
|
163
|
+
suspended: '电脑已休眠 · 语音通话已停止,可点击语音通话继续',
|
|
164
|
+
hidden: '猫猫已收起 · 语音通话已停止',
|
|
165
|
+
};
|
|
166
|
+
this.failed = !Object.hasOwn(messages, reason);
|
|
167
|
+
await this.releaseLocal(messages[reason] ?? (receiveOnly
|
|
168
|
+
? '只听连接已中断 · 麦克风未启用,点击只听重试'
|
|
169
|
+
: '语音通话已中断 · 麦克风已关闭,点击语音通话重试'));
|
|
170
|
+
}
|
|
70
171
|
async refresh() {
|
|
71
172
|
if (this.refreshing) return;
|
|
72
173
|
this.refreshing = true;
|
|
@@ -75,12 +176,21 @@ export class CompanionConversation {
|
|
|
75
176
|
const identity = await this.client.state();
|
|
76
177
|
if (generation !== this.generation) return;
|
|
77
178
|
this.identity = identity;
|
|
179
|
+
if (this.active && identity.audio?.activeMode) this.audioMode = identity.audio.activeMode;
|
|
78
180
|
if (this.active && ['closed', 'failed', 'idle'].includes(identity.phase)) {
|
|
79
|
-
|
|
80
|
-
|
|
181
|
+
if (this.expectedHostStop?.generation === this.generation) {
|
|
182
|
+
this.expectedHostStop = undefined;
|
|
183
|
+
this.failed = false;
|
|
184
|
+
await this.releaseLocal(this.stoppedMessage());
|
|
185
|
+
} else await this.end(this.retrying('语音连接已中断'), true);
|
|
186
|
+
} else {
|
|
187
|
+
if (this.expectedHostStop?.generation === this.generation && !this.expectedHostStop.pending)
|
|
188
|
+
this.expectedHostStop = undefined;
|
|
189
|
+
this.show(this.message ?? '点击语音通话,直接对我说话');
|
|
190
|
+
}
|
|
81
191
|
} catch (error) {
|
|
82
192
|
if (generation === this.generation) {
|
|
83
|
-
if (this.active) await this.end(explainError(error));
|
|
193
|
+
if (this.active) await this.end(explainError(error), true);
|
|
84
194
|
else this.show(explainError(error));
|
|
85
195
|
}
|
|
86
196
|
} finally { this.refreshing = false; }
|
|
@@ -95,29 +205,39 @@ export class CompanionConversation {
|
|
|
95
205
|
const identity = await changing;
|
|
96
206
|
await local;
|
|
97
207
|
this.identity = identity;
|
|
98
|
-
this.show(allowed ? '资料查询已开启 ·
|
|
208
|
+
this.show(allowed ? '资料查询已开启 · 点击语音通话继续' : '资料查询已暂停 · 点击语音通话继续');
|
|
99
209
|
} catch (error) { this.show(explainError(error)); }
|
|
100
210
|
finally { this.changingDocuments = false; }
|
|
101
211
|
}
|
|
102
212
|
async send(text) {
|
|
103
213
|
text = text.trim();
|
|
104
|
-
if (!text || !this.
|
|
214
|
+
if (!text || !this.identity || this.phase === 'connecting' || this.stopping || this.sending) return false;
|
|
105
215
|
if (!this.pendingText || this.pendingText.text !== text) this.pendingText = { text, id: this.uuid() };
|
|
106
216
|
const input = this.pendingText;
|
|
107
217
|
const generation = this.generation;
|
|
108
218
|
this.sending = true;
|
|
109
219
|
try {
|
|
110
|
-
await this.client.text(input.text, input.id);
|
|
220
|
+
const receipt = await this.client.text(input.text, input.id);
|
|
221
|
+
if (receipt?.delivery !== 'accepted') {
|
|
222
|
+
if (generation === this.generation) this.show(explainError({ code: 'unconfirmed' }));
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
111
225
|
if (this.pendingText === input) this.pendingText = undefined;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
226
|
+
// Voice may have ended; an accepted message still retires this draft.
|
|
227
|
+
// Keep the newer voice status rather than announcing that we are listening.
|
|
228
|
+
if (generation !== this.generation) return true;
|
|
229
|
+
this.transcript({ type: 'typed', role: 'user', text, receipt });
|
|
230
|
+
this.show(this.active ? this.listening() : '已发送 · 回答会留在同一段聊天中');
|
|
115
231
|
return true;
|
|
116
232
|
} catch (error) {
|
|
117
|
-
if (this.
|
|
233
|
+
if (generation === this.generation) this.show(explainError(error));
|
|
118
234
|
return false;
|
|
119
235
|
} finally { this.sending = false; }
|
|
120
236
|
}
|
|
121
|
-
muteMic() {
|
|
237
|
+
muteMic() {
|
|
238
|
+
if (this.audioMode === 'receive_only') return;
|
|
239
|
+
this.muted = !this.muted; this.peer?.muteMic(this.muted);
|
|
240
|
+
this.show(this.phase === 'talking' ? this.listening() : this.message);
|
|
241
|
+
}
|
|
122
242
|
muteSpeaker() { this.silent = !this.silent; this.peer?.muteSpeaker(this.silent); this.show(this.message); }
|
|
123
243
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
function unified(page) {
|
|
2
|
+
return page?.version === 1 && Array.isArray(page.items);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function knownRows(page) {
|
|
6
|
+
if (unified(page)) return page.items.length;
|
|
7
|
+
return (page?.approvals?.length ?? 0) + (page?.otherNeedsMe?.length ?? 0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function authenticationRequired(page) {
|
|
11
|
+
if (!unified(page)) return false;
|
|
12
|
+
const sources = Object.values(page.sources ?? {});
|
|
13
|
+
return sources.length > 0 && sources.every(source => source.status === 'unauthenticated');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Exact totals exist only on a complete, consistent unified read. Legacy pages never invent a sum. */
|
|
17
|
+
export function decisionBadge(page) {
|
|
18
|
+
if (!page) return { visible: true, label: '?', title: '待办暂不可读', state: 'unavailable' };
|
|
19
|
+
if (unified(page)) {
|
|
20
|
+
if (page.status === 'partial') {
|
|
21
|
+
return { visible: true, label: page.items.length > 0 ? '•' : '!', title: '仅部分读取', state: 'partial' };
|
|
22
|
+
}
|
|
23
|
+
if (page.status === 'unavailable') {
|
|
24
|
+
return authenticationRequired(page)
|
|
25
|
+
? { visible: true, label: '!', title: '需要登录', state: 'authentication' }
|
|
26
|
+
: { visible: true, label: '!', title: '待办暂不可读', state: 'unavailable' };
|
|
27
|
+
}
|
|
28
|
+
if (Number.isInteger(page.totalCount)) {
|
|
29
|
+
return page.totalCount === 0
|
|
30
|
+
? { visible: false, label: '0', title: '暂无待办', state: 'empty' }
|
|
31
|
+
: { visible: true, label: String(page.totalCount), title: `待办 · ${page.totalCount} 件`, state: 'available' };
|
|
32
|
+
}
|
|
33
|
+
return { visible: true, label: page.items.length > 0 ? '•' : '!', title: '数量未确认', state: 'uncertain' };
|
|
34
|
+
}
|
|
35
|
+
if (page.status !== 'available')
|
|
36
|
+
return { visible: true, label: '?', title: '待办暂不可读', state: 'unavailable' };
|
|
37
|
+
const hasKnownRows = knownRows(page) > 0
|
|
38
|
+
|| page.approvalCount > 0 || page.needsMeCount > 0 || page.otherNeedsMeCount > 0;
|
|
39
|
+
if (hasKnownRows) return { visible: true, label: '有待办', title: '有待办', state: 'available' };
|
|
40
|
+
return { visible: false, label: '0', title: '现在没有待办', state: 'empty' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function approvalMeta(approval) {
|
|
44
|
+
const state = {
|
|
45
|
+
open: '待你决定', accepted: '已接纳 · 待核结果', rejected: '已拒绝',
|
|
46
|
+
closed_without_decision: '已关闭',
|
|
47
|
+
};
|
|
48
|
+
const materialization = approval.materializationState === 'outcome_unknown' ? '结果未知'
|
|
49
|
+
: approval.materializationState === 'failed' ? '执行失败'
|
|
50
|
+
: approval.linkedNeedsMe ? '已关联待办' : '原处卡片';
|
|
51
|
+
return `审批 · ${state[approval.resolution] ?? '状态待核'} · ${materialization}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function navigation(item) {
|
|
55
|
+
const priorities = item.kind === 'approval'
|
|
56
|
+
? [['approval_card', '打开审批'], ['action', '打开处理入口'], ['origin', '打开原处']]
|
|
57
|
+
: [['action', '打开处理入口'], ['origin', '打开原处'], ['approval_card', '打开审批']];
|
|
58
|
+
const selected = priorities.find(([target]) => item.navigation.targets.includes(target));
|
|
59
|
+
return selected ? { variantRef: item.variantRef, target: selected[0], label: selected[1] } : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function decisionRows(page) {
|
|
63
|
+
if (unified(page)) {
|
|
64
|
+
if (page.status === 'unavailable') return [];
|
|
65
|
+
return page.items.map(item => ({
|
|
66
|
+
title: item.summary || '待处理事项',
|
|
67
|
+
meta: item.kind === 'approval' ? approvalMeta(item.approval)
|
|
68
|
+
: item.kind === 'judgment' ? '等你判断 · 请在原处处理' : '需要修复 · 请在原处处理',
|
|
69
|
+
variantRef: item.variantRef,
|
|
70
|
+
navigation: navigation(item),
|
|
71
|
+
previewable: false,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
if (!page || page.status !== 'available') return [];
|
|
75
|
+
return [
|
|
76
|
+
...(page.approvals ?? []).map(item => ({
|
|
77
|
+
title: item.summary || '待决策事项',
|
|
78
|
+
meta: approvalMeta(item),
|
|
79
|
+
proposalId: item.proposalId,
|
|
80
|
+
previewable: item.sourceFeatureId === 'F221'
|
|
81
|
+
&& /^[A-Za-z0-9_-]{1,200}$/.test(item.proposalId)
|
|
82
|
+
&& item.resolution === 'open' && item.materializationState === 'not_started',
|
|
83
|
+
})),
|
|
84
|
+
...(page.otherNeedsMe ?? []).map(item => ({ title: item.summary || '待处理事项', meta: '其他待处理 · 请向猫猫询问详情' })),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function decisionPresentation(page) {
|
|
89
|
+
const badge = decisionBadge(page);
|
|
90
|
+
if (!page) return { badge, message: '待办暂不可读 · 请稍后刷新', reloadLabel: '重试', pending: null, hasMore: false, renderRows: false };
|
|
91
|
+
if (unified(page)) {
|
|
92
|
+
const pending = Number.isInteger(page.totalCount) ? page.totalCount > 0 : page.items.length > 0 ? true : null;
|
|
93
|
+
if (page.status === 'partial') return {
|
|
94
|
+
badge, message: '仅部分读取 · 其余待办未能读取', reloadLabel: '重试', pending,
|
|
95
|
+
hasMore: page.page.hasMore, renderRows: true,
|
|
96
|
+
};
|
|
97
|
+
if (page.status === 'unavailable') return {
|
|
98
|
+
badge, message: authenticationRequired(page) ? '待办需要登录 · 请登录后重试' : '待办暂不可读 · 请稍后重试',
|
|
99
|
+
reloadLabel: '重试', pending: null, hasMore: false, renderRows: false,
|
|
100
|
+
};
|
|
101
|
+
if (page.totalCount === 0) return {
|
|
102
|
+
badge, message: '暂无待办', reloadLabel: '刷新', pending: false,
|
|
103
|
+
hasMore: page.page.hasMore, renderRows: true,
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
badge,
|
|
107
|
+
message: Number.isInteger(page.totalCount)
|
|
108
|
+
? `已读取 ${page.totalCount} 件待办`
|
|
109
|
+
: '已显示读取到的待办 · 数量未确认',
|
|
110
|
+
reloadLabel: '刷新', pending,
|
|
111
|
+
hasMore: page.page.hasMore, renderRows: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (page.status !== 'available') return {
|
|
115
|
+
badge, message: '待办暂不可读 · 请稍后刷新', reloadLabel: '重试', pending: null, hasMore: false, renderRows: false,
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
badge,
|
|
119
|
+
message: badge.state === 'empty' ? '现在没有待你处理的事项' : '已显示读取到的待办 · 暂无总数',
|
|
120
|
+
reloadLabel: '刷新', pending: badge.state === 'empty' ? false : true,
|
|
121
|
+
hasMore: page.page.hasMoreApprovals || page.page.hasMoreNeedsMe,
|
|
122
|
+
renderRows: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
package/renderer/errors.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const messages = {
|
|
2
|
-
permission_required: '
|
|
3
|
-
session_required: '连接已结束 ·
|
|
4
|
-
busy: '
|
|
5
|
-
selection_changed: '猫猫球的选择已更新 ·
|
|
2
|
+
permission_required: '请点击语音通话,或在系统提示中允许本次使用',
|
|
3
|
+
session_required: '连接已结束 · 点击语音通话继续',
|
|
4
|
+
busy: '会话暂时被占用,请稍后重试',
|
|
5
|
+
selection_changed: '猫猫球的选择已更新 · 点击语音通话继续',
|
|
6
6
|
carrier_unavailable: '实时语音暂不可用 · 可以打开聊天继续交流',
|
|
7
7
|
cancelled: '操作已取消',
|
|
8
8
|
unconfirmed: '发送尚未确认 · 文字已保留,可检查聊天记录后重试',
|
package/renderer/index.html
CHANGED
|
@@ -1,31 +1,173 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="zh-CN">
|
|
3
|
-
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4
|
-
<title>猫猫球</title><link rel="stylesheet" href="./theme.css"><link rel="stylesheet" href="./style.css"></head>
|
|
3
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>猫猫球</title><link rel="stylesheet" href="./style.css"></head>
|
|
5
4
|
<body>
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
<svg class="icon-definitions" aria-hidden="true">
|
|
6
|
+
<symbol id="i-mic" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M8 21h8"/></symbol>
|
|
7
|
+
<symbol id="i-mic-off" viewBox="0 0 24 24"><path d="M9 9v2a3 3 0 0 0 4.8 2.4M15 10V6a3 3 0 0 0-5.7-1.3M5 11a7 7 0 0 0 11.2 5.6M19 11a7 7 0 0 1-.7 3M12 18v3M8 21h8M3 3l18 18"/></symbol>
|
|
8
|
+
<symbol id="i-listen" viewBox="0 0 24 24"><path d="M4 13v-1a8 8 0 0 1 16 0v1M4 13h3v7H6a2 2 0 0 1-2-2v-5ZM20 13h-3v7h1a2 2 0 0 0 2-2v-5Z"/></symbol>
|
|
9
|
+
<symbol id="i-keyboard" viewBox="0 0 24 24"><rect x="2.5" y="5" width="19" height="14" rx="3"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M7 15h10"/></symbol>
|
|
10
|
+
<symbol id="i-history" viewBox="0 0 24 24"><path d="M3 11a9 9 0 1 1 2.6 7.4M3 4v7h7M12 7v5l3 2"/></symbol>
|
|
11
|
+
<symbol id="i-screen" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="13" rx="2.5"/><path d="M8 21h8M12 17v4"/></symbol>
|
|
12
|
+
<symbol id="i-book" viewBox="0 0 24 24"><path d="M12 6c-3-2-6-2-9-1v14c3-1 6-1 9 1 3-2 6-2 9-1V5c-3-1-6-1-9 1v14"/></symbol>
|
|
13
|
+
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.5 14A8.5 8.5 0 0 1 10 3.5 8.5 8.5 0 1 0 20.5 14Z"/></symbol>
|
|
14
|
+
<symbol id="i-end" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="3"/></symbol>
|
|
15
|
+
<symbol id="i-subtitles" viewBox="0 0 24 24"><rect x="2.5" y="4.5" width="19" height="15" rx="3"/><path d="M6 10h5M6 14h7M15 10h3M15 14h3"/></symbol>
|
|
16
|
+
<symbol id="i-expand" viewBox="0 0 24 24"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/></symbol>
|
|
17
|
+
</svg>
|
|
18
|
+
<main id="desktop" aria-label="猫猫球">
|
|
19
|
+
<div id="anchor">
|
|
20
|
+
<button id="pet" aria-label="猫猫:点击或右键打开名片,拖动移动" aria-expanded="false" aria-controls="actions menu chat transcript settings decisions">
|
|
21
|
+
<svg id="living-sit" viewBox="0 0 600 600" aria-hidden="true" hidden>
|
|
22
|
+
<defs>
|
|
23
|
+
<mask id="living-eye-left" maskUnits="userSpaceOnUse" x="0" y="0" width="600" height="600"><image href="./skins/xianxian-living/layers/whiteL.webp" width="600" height="600"/></mask>
|
|
24
|
+
<mask id="living-eye-right" maskUnits="userSpaceOnUse" x="0" y="0" width="600" height="600"><image href="./skins/xianxian-living/layers/whiteR.webp" width="600" height="600"/></mask>
|
|
25
|
+
</defs>
|
|
26
|
+
<g data-layer="tail"><image href="./skins/xianxian-living/layers/tail.webp" width="600" height="600"/></g>
|
|
27
|
+
<image href="./skins/xianxian-living/layers/body.webp" width="600" height="600"/>
|
|
28
|
+
<g data-layer="head">
|
|
29
|
+
<g data-layer="ear-left"><image href="./skins/xianxian-living/layers/earL.webp" width="600" height="600"/></g>
|
|
30
|
+
<g data-layer="ear-right"><image href="./skins/xianxian-living/layers/earR.webp" width="600" height="600"/></g>
|
|
31
|
+
<image href="./skins/xianxian-living/layers/head.webp" width="600" height="600"/>
|
|
32
|
+
<g mask="url(#living-eye-left)"><image data-layer="iris-left" href="./skins/xianxian-living/layers/irisL.webp" width="600" height="600"/></g>
|
|
33
|
+
<g mask="url(#living-eye-right)"><image data-layer="iris-right" href="./skins/xianxian-living/layers/irisR.webp" width="600" height="600"/></g>
|
|
34
|
+
<image data-layer="closed" href="./skins/xianxian-living/layers/closed.webp" width="600" height="600"/>
|
|
35
|
+
<image data-layer="mouth" href="./skins/xianxian-living/layers/mouth.webp" width="600" height="600"/>
|
|
36
|
+
<image data-layer="scared" href="./skins/xianxian-living/layers/scared.webp" width="600" height="600"/>
|
|
37
|
+
</g>
|
|
38
|
+
</svg>
|
|
39
|
+
<video id="living-video" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
40
|
+
<video id="living-video-transition" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
41
|
+
</button>
|
|
42
|
+
<div id="indicators">
|
|
43
|
+
<button id="share-badge" hidden aria-label="停止共享" title="停止共享"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg></button>
|
|
44
|
+
<button id="pending-badge" hidden aria-label="查看待决事项" title="查看待决事项"><span id="pending-count">?</span></button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<section id="bubble" class="floating" aria-label="最新消息" hidden>
|
|
48
|
+
<p id="bubble-first" hidden></p><p id="bubble-second" hidden></p>
|
|
49
|
+
</section>
|
|
50
|
+
<section id="actions" class="floating call-bar" aria-label="当前通话" hidden>
|
|
51
|
+
<div class="call-status-line"><span class="status-dot" aria-hidden="true"></span><strong id="call-state">通话中</strong><span id="call-mic-state">麦克风已开启</span></div>
|
|
52
|
+
<p id="call-work" class="call-work" hidden></p>
|
|
53
|
+
<div id="call-share-context" class="call-share-context" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="call-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
54
|
+
<div class="call-recent" aria-label="最新消息">
|
|
55
|
+
<small class="call-recent-label">最新消息</small>
|
|
56
|
+
<p id="call-bubble-first" hidden></p><p id="call-bubble-second" hidden></p>
|
|
57
|
+
</div>
|
|
58
|
+
<div id="active-actions" class="action-row call-actions">
|
|
59
|
+
<button id="mic" data-action="mute"><svg class="icon" aria-hidden="true"><use id="mic-icon" href="#i-mic"/></svg><span class="label">静音</span></button>
|
|
60
|
+
<button data-action="share" id="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
61
|
+
<button data-action="transcript"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
62
|
+
<button id="end" class="destructive" data-action="stop"><span>挂断</span></button>
|
|
63
|
+
</div>
|
|
64
|
+
<p id="status" role="status" hidden></p>
|
|
65
|
+
</section>
|
|
66
|
+
<section id="menu" class="floating name-card" aria-label="猫猫名片" hidden>
|
|
67
|
+
<div id="menu-call-context" class="call-context" hidden>
|
|
68
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="menu-call-state"></strong><span id="menu-call-mic-state"></span>
|
|
69
|
+
<small id="menu-call-work" hidden></small>
|
|
70
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
71
|
+
<div id="menu-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="menu-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
72
|
+
</div>
|
|
73
|
+
<strong id="menu-name" class="menu-title">猫猫</strong>
|
|
74
|
+
<div class="name-card-actions">
|
|
75
|
+
<button id="begin" class="primary" data-action="begin"><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">语音通话</span></button>
|
|
76
|
+
<button id="listen" data-action="listen" hidden><svg class="icon" aria-hidden="true"><use href="#i-listen"/></svg><span class="label">只听</span></button>
|
|
77
|
+
<button data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
78
|
+
</div>
|
|
79
|
+
<p id="menu-status" role="status" hidden></p>
|
|
80
|
+
<button data-action="decisions"><span aria-hidden="true">◇</span><span>待办</span><span id="menu-pending" class="menu-status">—</span></button>
|
|
81
|
+
<button data-action="history"><svg class="icon" aria-hidden="true"><use href="#i-history"/></svg><span>打开聊天</span></button>
|
|
82
|
+
<button id="menu-play" data-action="play" hidden><span aria-hidden="true">✦</span><span>逗宪宪</span></button>
|
|
83
|
+
<div class="menu-divider" role="separator"></div>
|
|
84
|
+
<button id="speaker" data-action="speaker" hidden><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">关闭播音</span></button>
|
|
85
|
+
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span><span class="menu-status">›</span></button>
|
|
86
|
+
<button id="menu-share" data-action="share" title="语音通话连接后可共享画面"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
87
|
+
<button id="documents" data-action="documents" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg><span>资料查询</span><span class="menu-status">开启</span></button>
|
|
88
|
+
<div class="menu-divider" role="separator"></div>
|
|
89
|
+
<button data-action="hide" class="quiet-action" title="可从 Clowder 的聊聊入口叫回来"><svg class="icon" aria-hidden="true"><use href="#i-moon"/></svg><span>收起猫猫</span></button>
|
|
90
|
+
</section>
|
|
91
|
+
<section id="chat" class="floating" aria-label="和猫猫写几句" hidden>
|
|
92
|
+
<div id="chat-call-context" class="call-context" hidden>
|
|
93
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="chat-call-state"></strong><span id="chat-call-mic-state"></span>
|
|
94
|
+
<small id="chat-call-work" hidden></small>
|
|
95
|
+
<div class="call-context-actions"><button id="chat-mic" data-action="mute">静音</button><button id="chat-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
96
|
+
<div id="chat-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="chat-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
97
|
+
</div>
|
|
98
|
+
<header><strong id="chat-name">和猫猫写几句</strong><button data-action="dismiss" aria-label="收起文字">收起</button></header>
|
|
99
|
+
<div id="chat-transcript" role="log" aria-live="polite"><p class="empty">想说什么,就写在这里。</p></div>
|
|
100
|
+
<form id="compose"><textarea id="message" aria-label="给猫猫输入消息" placeholder="输入消息…" rows="2" maxlength="8000"></textarea><button type="submit" class="primary">发送</button></form>
|
|
101
|
+
<small id="chat-status" role="status"></small>
|
|
102
|
+
<small><span id="connection-scope">语音与文字共用同一段聊天</span> <button id="history">在 Cat Café 中查看完整聊天 ↗</button></small>
|
|
103
|
+
</section>
|
|
104
|
+
<section id="transcript" class="floating call-transcript" data-expanded="false" aria-label="这次通话的字幕" hidden>
|
|
105
|
+
<div id="transcript-call-context" class="call-context transcript-call-context">
|
|
106
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="transcript-call-state"></strong><span id="transcript-call-mic-state"></span>
|
|
107
|
+
<small id="transcript-call-work" hidden></small>
|
|
108
|
+
<div class="call-context-actions">
|
|
109
|
+
<button id="transcript-return" class="transcript-return" type="button" data-action="transcript-return"><span aria-hidden="true">‹</span><span>返回通话</span></button>
|
|
110
|
+
<button id="transcript-mic" data-action="mute" aria-label="静音"><svg class="icon" aria-hidden="true"><use id="transcript-mic-icon" href="#i-mic"/></svg></button>
|
|
111
|
+
<button id="transcript-expand" data-action="transcript-expand"><svg class="icon" aria-hidden="true"><use href="#i-expand"/></svg><span id="transcript-expand-label">展开</span></button>
|
|
112
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
113
|
+
</div>
|
|
114
|
+
<div id="transcript-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="transcript-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
115
|
+
</div>
|
|
116
|
+
<div id="call-transcript-log" role="log" aria-live="polite"></div>
|
|
117
|
+
<button id="call-unread" class="new-content" type="button" hidden></button>
|
|
118
|
+
<form id="call-compose"><input id="call-message" aria-label="通话中给猫猫发消息" placeholder="发消息给猫猫…" maxlength="8000"><button type="submit" class="primary">发送</button></form>
|
|
119
|
+
<small id="call-transcript-status" role="status"></small>
|
|
120
|
+
<small class="call-transcript-foot">完整记录会留在 Cat Café 聊天中</small>
|
|
121
|
+
</section>
|
|
122
|
+
<section id="settings" class="floating settings-panel" aria-label="猫猫球设置" hidden>
|
|
123
|
+
<div id="settings-call-context" class="call-context" hidden>
|
|
124
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="settings-call-state"></strong><span id="settings-call-mic-state"></span>
|
|
125
|
+
<small id="settings-call-work" hidden></small>
|
|
126
|
+
<div class="call-context-actions"><button id="settings-mic" data-action="mute">静音</button><button id="settings-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
127
|
+
<div id="settings-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="settings-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
128
|
+
</div>
|
|
129
|
+
<header class="settings-head"><button id="settings-back" type="button" aria-label="返回名片">‹</button><strong id="settings-title">设置</strong></header>
|
|
130
|
+
<p id="settings-loading" class="settings-loading" hidden>正在读取设置…</p>
|
|
131
|
+
<div id="settings-page-main" class="settings-page">
|
|
132
|
+
<button id="settings-partner-open" class="settings-row" type="button" data-setting-control data-drilldown><span>陪伴者</span><span id="settings-partner-value" class="settings-value">—</span></button>
|
|
133
|
+
<button id="settings-look-open" class="settings-row" type="button" data-setting-control data-drilldown><span>外观</span><span id="settings-look-value" class="settings-value">—</span></button>
|
|
134
|
+
<button id="settings-behavior" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>自主活动</strong><small>空闲时自行走动</small></span><i aria-hidden="true"></i></button>
|
|
135
|
+
<button id="settings-proactive" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>主动提示</strong><small>有待办时在猫旁显示数量</small></span><i aria-hidden="true"></i></button>
|
|
136
|
+
<button id="settings-documents" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>资料查询</strong><small>允许陪伴者在通话中查询家里的资料</small></span><i aria-hidden="true"></i></button>
|
|
137
|
+
<footer class="settings-footer"><button id="settings-reset" type="button" data-setting-control>重置位置</button><button id="settings-disable-open" type="button" data-setting-control>停用桌面猫猫球…</button></footer>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="settings-page-partner" class="settings-page" hidden>
|
|
140
|
+
<p class="settings-copy">在桌面陪伴和回复你的猫。更换陪伴者将结束当前语音通话。</p>
|
|
141
|
+
<div id="settings-roster" class="settings-roster"></div>
|
|
142
|
+
<label id="settings-tone-field" class="settings-field"><span>人设基调</span><textarea id="settings-tone" rows="2" maxlength="200" data-setting-control></textarea><small><span>适用于所有陪伴者 · 下次语音通话生效</span><span id="settings-tone-count">0 / 200</span></small></label>
|
|
143
|
+
</div>
|
|
144
|
+
<div id="settings-page-look" class="settings-page" hidden>
|
|
145
|
+
<p class="settings-eyebrow">皮肤</p><p class="settings-copy">仅改变外观,不更换陪伴者。</p>
|
|
146
|
+
<div class="settings-skins">
|
|
147
|
+
<button id="settings-skin-xianxian-codex" type="button" data-setting-control>宪宪 v1</button>
|
|
148
|
+
<button id="settings-skin-yanyan-codex" type="button" data-setting-control>砚砚 v1</button>
|
|
149
|
+
<button id="settings-skin-ragdoll-v1" type="button" data-setting-control>布偶猫</button>
|
|
150
|
+
<button id="settings-skin-yarn-ball" type="button" data-setting-control>猫猫球</button>
|
|
151
|
+
</div>
|
|
152
|
+
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="48" max="192" step="1" data-setting-control><small>大</small></div></label>
|
|
153
|
+
</div>
|
|
154
|
+
<div id="settings-page-confirm" class="settings-page settings-confirm" hidden>
|
|
155
|
+
<strong id="settings-confirm-title"></strong><p id="settings-confirm-copy"></p>
|
|
156
|
+
<div><button id="settings-confirm-cancel" type="button">取消</button><button id="settings-confirm-apply" type="button" data-setting-control>确认更改</button></div>
|
|
157
|
+
</div>
|
|
158
|
+
<div id="settings-page-unavailable" class="settings-page settings-empty" hidden><p id="settings-unavailable-copy"></p><button id="settings-unavailable-retry" type="button">重试</button></div>
|
|
159
|
+
<div id="settings-page-disabled" class="settings-page settings-empty" hidden><p>桌面猫猫球已停用。可在 Cat Café 的“设置 › 插件”中重新启用。</p></div>
|
|
160
|
+
<p id="settings-notice" class="settings-notice" role="status" hidden></p>
|
|
161
|
+
<div class="settings-notice-actions"><button id="settings-retry" type="button" hidden>重试</button><button id="settings-reread" type="button" hidden>重新读取</button></div>
|
|
162
|
+
</section>
|
|
163
|
+
<p id="announcement" class="sr-only" role="status"></p>
|
|
164
|
+
<section id="decisions" class="floating" aria-label="待办" hidden>
|
|
165
|
+
<header><strong>待办</strong><button id="decision-reload" type="button">刷新</button></header>
|
|
166
|
+
<p id="decision-status" role="status">正在读取待办…</p>
|
|
167
|
+
<ul id="decision-list"></ul>
|
|
168
|
+
<button id="decision-more" type="button" hidden>查看更多</button>
|
|
169
|
+
<small>这里只查看原处事项。要决定时请打开对应卡片。</small>
|
|
170
|
+
</section>
|
|
171
|
+
</main>
|
|
30
172
|
<script type="module" src="./surface.mjs"></script>
|
|
31
173
|
</body></html>
|