@clowder-ai/companion 0.1.0-alpha.14 → 0.1.0-alpha.15
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/manifest.json +1 -1
- package/package.json +3 -3
- package/plugin.yaml +1 -1
- package/renderer/client.mjs +1 -0
- package/renderer/decision-view.mjs +107 -11
- package/renderer/surface.mjs +45 -14
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clowder-ai/companion",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.15",
|
|
4
4
|
"description": "The desktop voice body for your Clowder companion",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"prepack": "pnpm build"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@clowder-ai/plugin-contract": "0.1.0-beta.
|
|
31
|
-
"@clowder-ai/plugin-sdk": "0.1.0-beta.
|
|
30
|
+
"@clowder-ai/plugin-contract": "0.1.0-beta.24",
|
|
31
|
+
"@clowder-ai/plugin-sdk": "0.1.0-beta.20",
|
|
32
32
|
"playwright": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
package/plugin.yaml
CHANGED
package/renderer/client.mjs
CHANGED
|
@@ -37,6 +37,7 @@ export function createCompanionClient(bridge) {
|
|
|
37
37
|
screenClose: () => invoke({ kind: 'screen.close' }, 'ok'),
|
|
38
38
|
openConversation: () => invoke({ kind: 'conversation.open' }, 'navigation'),
|
|
39
39
|
readDecisions: (offset = 0, limit = 20) => invoke({ kind: 'decisions.read', offset, limit }, 'decisions'),
|
|
40
|
+
openDecision: (variantRef, target) => invoke({ kind: 'decision.open', variantRef, target }, 'navigation'),
|
|
40
41
|
inspectF221: (proposalId) => invoke({ kind: 'f221.inspect', proposalId }, 'decision-trial'),
|
|
41
42
|
resize: (expanded) => invoke({ kind: 'view.resize', expanded }, 'ok'),
|
|
42
43
|
layout: (panel, width, height) => invoke({ kind: 'view.layout', panel, width, height }, 'layout'),
|
|
@@ -1,28 +1,124 @@
|
|
|
1
|
-
|
|
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. */
|
|
2
17
|
export function decisionBadge(page) {
|
|
3
|
-
if (!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')
|
|
4
36
|
return { visible: true, label: '?', title: '待办暂不可读', state: 'unavailable' };
|
|
5
|
-
const hasKnownRows = (page
|
|
37
|
+
const hasKnownRows = knownRows(page) > 0
|
|
6
38
|
|| page.approvalCount > 0 || page.needsMeCount > 0 || page.otherNeedsMeCount > 0;
|
|
7
39
|
if (hasKnownRows) return { visible: true, label: '有待办', title: '有待办', state: 'available' };
|
|
8
40
|
return { visible: false, label: '0', title: '现在没有待办', state: 'empty' };
|
|
9
41
|
}
|
|
10
42
|
|
|
11
|
-
|
|
12
|
-
if (!page || page.status !== 'available') return [];
|
|
43
|
+
function approvalMeta(approval) {
|
|
13
44
|
const state = {
|
|
14
45
|
open: '待你决定', accepted: '已接纳 · 待核结果', rejected: '已拒绝',
|
|
15
46
|
closed_without_decision: '已关闭',
|
|
16
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 [];
|
|
17
75
|
return [
|
|
18
|
-
...page.approvals.map(item => ({
|
|
76
|
+
...(page.approvals ?? []).map(item => ({
|
|
19
77
|
title: item.summary || '待决策事项',
|
|
20
|
-
meta:
|
|
78
|
+
meta: approvalMeta(item),
|
|
21
79
|
proposalId: item.proposalId,
|
|
22
|
-
previewable: item.sourceFeatureId === 'F221'
|
|
23
|
-
/^[A-Za-z0-9_-]{1,200}$/.test(item.proposalId)
|
|
24
|
-
item.resolution === 'open' && item.materializationState === 'not_started',
|
|
80
|
+
previewable: item.sourceFeatureId === 'F221'
|
|
81
|
+
&& /^[A-Za-z0-9_-]{1,200}$/.test(item.proposalId)
|
|
82
|
+
&& item.resolution === 'open' && item.materializationState === 'not_started',
|
|
25
83
|
})),
|
|
26
|
-
...page.otherNeedsMe.map(item => ({ title: item.summary || '待处理事项', meta: '其他待处理 · 请向猫猫询问详情' })),
|
|
84
|
+
...(page.otherNeedsMe ?? []).map(item => ({ title: item.summary || '待处理事项', meta: '其他待处理 · 请向猫猫询问详情' })),
|
|
27
85
|
];
|
|
28
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/surface.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { PetMotion } from './pet-motion.mjs';
|
|
|
11
11
|
import { LivingBody } from './living-body.mjs';
|
|
12
12
|
import { detectDockedEdge } from './living-edge.mjs';
|
|
13
13
|
import { bindPetControls } from './pet-controls.mjs';
|
|
14
|
-
import { decisionBadge, decisionRows } from './decision-view.mjs';
|
|
14
|
+
import { decisionBadge, decisionPresentation, decisionRows } from './decision-view.mjs';
|
|
15
15
|
import { nativeWorkLabel, normalizeNativeWork } from './native-work-motion.mjs';
|
|
16
16
|
import { NativeWindowTravel } from './native-window-travel.mjs';
|
|
17
17
|
import { currentCompanionIdentity } from './companion-identity.mjs';
|
|
@@ -44,6 +44,10 @@ let didMove = false, decisionLoading = false, decisionOffset = 0, threadTitle, s
|
|
|
44
44
|
const ambientPanel = active => active ? 'actions' : 'none';
|
|
45
45
|
const setTexts = (ids, text) => { for (const id of ids) $(id).textContent = text; };
|
|
46
46
|
const setHidden = (ids, hidden) => { for (const id of ids) $(id).hidden = hidden; };
|
|
47
|
+
const decisionReadBasis = page => page?.version === 1
|
|
48
|
+
? [page.status, page.totalCount ?? 'unknown', ...['approvals', 'needsMe']
|
|
49
|
+
.flatMap(key => [page.sources?.[key]?.status, page.sources?.[key]?.coverage])].join('|')
|
|
50
|
+
: page ? 'legacy' : null;
|
|
47
51
|
if (!window.clowderCompanion) {
|
|
48
52
|
$('actions').hidden = false; $('status').hidden = false;
|
|
49
53
|
status('请从 Clowder 的聊聊入口打开猫猫球');
|
|
@@ -278,20 +282,39 @@ if (!window.clowderCompanion) {
|
|
|
278
282
|
decisionLoading = true;
|
|
279
283
|
if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待办…';
|
|
280
284
|
try {
|
|
285
|
+
const previousReadBasis = decisionReadBasis(latestDecisionPage);
|
|
281
286
|
const page = await client.readDecisions(more ? decisionOffset : 0, 10);
|
|
282
|
-
|
|
287
|
+
const presentation = decisionPresentation(page);
|
|
288
|
+
const changedRead = more && presentation.renderRows && previousReadBasis !== null
|
|
289
|
+
&& decisionReadBasis(page) !== previousReadBasis;
|
|
283
290
|
showDecisionBadge(page);
|
|
284
|
-
motion.setPendingDecision(
|
|
291
|
+
motion.setPendingDecision(presentation.pending);
|
|
285
292
|
if (controls.panel !== 'decisions') return;
|
|
286
293
|
const list = $('decision-list');
|
|
287
|
-
if (!more) { list.replaceChildren(); list.scrollTop = 0; }
|
|
288
|
-
|
|
294
|
+
if (!more || changedRead) { list.replaceChildren(); list.scrollTop = 0; }
|
|
295
|
+
const retainedRows = more && !presentation.renderRows && list.children.length > 0;
|
|
296
|
+
const knownVariantRefs = new Set(Array.from(list.children)
|
|
297
|
+
.map(item => item.dataset.variantRef).filter(Boolean));
|
|
298
|
+
for (const row of presentation.renderRows && !changedRead ? decisionRows(page) : []) {
|
|
299
|
+
if (row.variantRef && knownVariantRefs.has(row.variantRef)) continue;
|
|
289
300
|
const item = document.createElement('li');
|
|
301
|
+
if (row.variantRef) { item.dataset.variantRef = row.variantRef; knownVariantRefs.add(row.variantRef); }
|
|
290
302
|
const title = document.createElement('strong');
|
|
291
303
|
const meta = document.createElement('span');
|
|
292
304
|
title.textContent = row.title; meta.textContent = row.meta;
|
|
293
305
|
item.append(title, meta); list.append(item);
|
|
294
|
-
if (row.
|
|
306
|
+
if (row.navigation) {
|
|
307
|
+
const button = document.createElement('button');
|
|
308
|
+
button.type = 'button'; button.textContent = row.navigation.label;
|
|
309
|
+
button.onclick = async () => {
|
|
310
|
+
try {
|
|
311
|
+
const result = await client.openDecision(row.navigation.variantRef, row.navigation.target);
|
|
312
|
+
$('decision-status').textContent = result.delivery === 'requested'
|
|
313
|
+
? '已请求打开对应事项' : '打开对应事项尚未确认 · 请从 Cat Café 查看';
|
|
314
|
+
} catch { $('decision-status').textContent = '暂时无法打开对应事项 · 请稍后重试'; }
|
|
315
|
+
};
|
|
316
|
+
item.append(button);
|
|
317
|
+
} else if (row.previewable) {
|
|
295
318
|
const button = document.createElement('button');
|
|
296
319
|
button.type = 'button'; button.textContent = '查看确认演练';
|
|
297
320
|
button.onclick = async () => {
|
|
@@ -307,17 +330,25 @@ if (!window.clowderCompanion) {
|
|
|
307
330
|
item.append(button);
|
|
308
331
|
}
|
|
309
332
|
}
|
|
310
|
-
decisionOffset = page.page.offset + page.page.limit;
|
|
311
|
-
|
|
312
|
-
$('decision-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
: '
|
|
316
|
-
|
|
333
|
+
decisionOffset = changedRead ? 0 : page.page.offset + page.page.limit;
|
|
334
|
+
$('decision-reload').textContent = changedRead ? '刷新' : presentation.reloadLabel;
|
|
335
|
+
$('decision-status').textContent = changedRead && page.totalCount !== 0
|
|
336
|
+
? '待办已变化 · 请刷新查看最新列表'
|
|
337
|
+
: retainedRows
|
|
338
|
+
? `${presentation.badge.state === 'authentication' ? '待办需要登录' : '待办暂不可读'} · 以下为上次读到的内容`
|
|
339
|
+
: presentation.message;
|
|
340
|
+
$('decision-more').hidden = changedRead || !presentation.hasMore;
|
|
317
341
|
} catch {
|
|
318
342
|
showDecisionBadge(undefined);
|
|
319
343
|
motion.setPendingDecision(null);
|
|
320
|
-
if (controls.panel === 'decisions')
|
|
344
|
+
if (controls.panel === 'decisions') {
|
|
345
|
+
const retained = $('decision-list').children.length > 0;
|
|
346
|
+
$('decision-status').textContent = retained
|
|
347
|
+
? '待办暂不可读 · 以下为上次读到的内容'
|
|
348
|
+
: '待办暂不可读 · 请稍后刷新';
|
|
349
|
+
$('decision-reload').textContent = '重试';
|
|
350
|
+
$('decision-more').hidden = true;
|
|
351
|
+
}
|
|
321
352
|
} finally { decisionLoading = false; }
|
|
322
353
|
}
|
|
323
354
|
$('share-badge').onclick = toggleScreen;
|