@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.15",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clowder-ai/companion",
3
- "version": "0.1.0-alpha.14",
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.23",
31
- "@clowder-ai/plugin-sdk": "0.1.0-beta.19",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.15",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -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
- /** F246 remains the decision truth; linked F310 receipts are enrichment, not another item. */
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 || page.status !== 'available')
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.approvals?.length ?? 0) > 0 || (page.otherNeedsMe?.length ?? 0) > 0
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
- export function decisionRows(page) {
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: `审批 · ${state[item.resolution] ?? '状态待核'} · ${item.materializationState === 'outcome_unknown' ? '结果未知' : item.materializationState === 'failed' ? '执行失败' : item.linkedNeedsMe ? '已关联待办' : '原处卡片'}`,
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
+ }
@@ -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
- if (page.status !== 'available') throw new Error('Decision source unavailable');
287
+ const presentation = decisionPresentation(page);
288
+ const changedRead = more && presentation.renderRows && previousReadBasis !== null
289
+ && decisionReadBasis(page) !== previousReadBasis;
283
290
  showDecisionBadge(page);
284
- motion.setPendingDecision((page.approvals?.length ?? 0) + (page.otherNeedsMe?.length ?? 0) > 0);
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
- for (const row of decisionRows(page)) {
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.previewable) {
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
- const badge = decisionBadge(page);
312
- $('decision-reload').textContent = '刷新';
313
- $('decision-status').textContent = badge.state === 'empty'
314
- ? '现在没有待你处理的事项'
315
- : '已显示读取到的待办 · 暂无总数';
316
- $('decision-more').hidden = !page.page.hasMoreApprovals && !page.page.hasMoreNeedsMe;
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') $('decision-status').textContent = '待办暂不可读 · 请稍后刷新';
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;