@clowder-ai/companion 0.1.0-alpha.14 → 0.1.0-alpha.19

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.19",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -40,7 +40,7 @@
40
40
  "bridgeVersion": "1.3.0",
41
41
  "surface": {
42
42
  "entrypoint": "renderer/index.html",
43
- "integrity": "sha256-UH8VFfr3q9a5gI2pPPXinKzCTjaGI8ZdvrJngy6i+M0="
43
+ "integrity": "sha256-jaXXDHzwGVDNnFoOhrj5nvqAXMlf02Jw8CeC7Xay538="
44
44
  },
45
45
  "presentation": {
46
46
  "width": 300,
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.19",
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.19",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -40,7 +40,7 @@
40
40
  "bridgeVersion": "1.3.0",
41
41
  "surface": {
42
42
  "entrypoint": "renderer/index.html",
43
- "integrity": "sha256-UH8VFfr3q9a5gI2pPPXinKzCTjaGI8ZdvrJngy6i+M0="
43
+ "integrity": "sha256-jaXXDHzwGVDNnFoOhrj5nvqAXMlf02Jw8CeC7Xay538="
44
44
  },
45
45
  "presentation": {
46
46
  "width": 300,
@@ -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
+ }
@@ -103,9 +103,10 @@
103
103
  </section>
104
104
  <section id="transcript" class="floating call-transcript" data-expanded="false" aria-label="这次通话的字幕" hidden>
105
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><span class="view-tag">字幕</span>
106
+ <span class="status-dot" aria-hidden="true"></span><strong id="transcript-call-state"></strong><span id="transcript-call-mic-state"></span>
107
107
  <small id="transcript-call-work" hidden></small>
108
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>
109
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>
110
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>
111
112
  <button data-action="stop" class="destructive compact">挂断</button>
@@ -139,8 +139,14 @@ button:disabled { opacity: .45; cursor: default; }
139
139
  #chat small { color: var(--cafe-text-muted); font-size: 10px; padding: 0 14px 10px; }
140
140
  .call-transcript { width: 420px; height: 210px; display: flex; flex-direction: column; overflow: hidden; }
141
141
  .call-transcript[data-expanded="true"] { height: 500px; }
142
- .transcript-call-context { flex: none; grid-template-columns: auto auto auto auto 1fr auto; padding: 7px 10px; }
143
- .view-tag { border-radius: 999px; background: var(--desktop-rail); color: var(--desktop-body); padding: 2px 7px; font-size: 11px; }
142
+ .transcript-call-context { flex: none; grid-template-columns: auto minmax(0, 1fr) auto; gap: 5px; padding: 7px 8px; }
143
+ .transcript-call-context #transcript-call-state { min-width: 0; overflow-wrap: anywhere; }
144
+ .transcript-call-context #transcript-call-mic-state { grid-column: 2 / 4; grid-row: 2; min-width: 0; white-space: normal; overflow-wrap: anywhere; font-size: 11px; }
145
+ .transcript-call-context .call-context-actions { grid-column: 3; grid-row: 1; gap: 2px; }
146
+ .transcript-call-context .call-context-actions button { padding: 6px; }
147
+ .transcript-call-context .destructive.compact { min-width: 64px; padding-inline: 9px; }
148
+ .transcript-return { display: inline-flex; align-items: center; gap: 2px; border: 1px solid var(--desktop-line) !important; background: var(--desktop-work) !important; color: var(--desktop-body); padding: 3px 6px !important; font-size: 11px; font-weight: 500 !important; white-space: nowrap; }
149
+ .transcript-return:hover:not(:disabled) { background: var(--desktop-rail) !important; }
144
150
  #call-transcript-log { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 8px 12px 5px; }
145
151
  .call-transcript-row { margin: 0 0 7px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.45; }
146
152
  .call-transcript-row.voice.user { color: var(--desktop-muted); font-size: 13px; font-weight: 400; }
@@ -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 的聊聊入口打开猫猫球');
@@ -77,6 +81,7 @@ if (!window.clowderCompanion) {
77
81
  if (kind === 'share') toggleScreen();
78
82
  if (kind === 'documents') void conversation.documents(conversation.identity?.documentsAllowed === false);
79
83
  if (kind === 'hide') void client.hide().catch(error => status(explainError(error)));
84
+ if (kind === 'transcript-return') void controls.show('actions');
80
85
  if (kind === 'transcript-expand') {
81
86
  const expanded = $('transcript').dataset.expanded !== 'true';
82
87
  $('transcript').dataset.expanded = String(expanded);
@@ -278,20 +283,39 @@ if (!window.clowderCompanion) {
278
283
  decisionLoading = true;
279
284
  if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待办…';
280
285
  try {
286
+ const previousReadBasis = decisionReadBasis(latestDecisionPage);
281
287
  const page = await client.readDecisions(more ? decisionOffset : 0, 10);
282
- if (page.status !== 'available') throw new Error('Decision source unavailable');
288
+ const presentation = decisionPresentation(page);
289
+ const changedRead = more && presentation.renderRows && previousReadBasis !== null
290
+ && decisionReadBasis(page) !== previousReadBasis;
283
291
  showDecisionBadge(page);
284
- motion.setPendingDecision((page.approvals?.length ?? 0) + (page.otherNeedsMe?.length ?? 0) > 0);
292
+ motion.setPendingDecision(presentation.pending);
285
293
  if (controls.panel !== 'decisions') return;
286
294
  const list = $('decision-list');
287
- if (!more) { list.replaceChildren(); list.scrollTop = 0; }
288
- for (const row of decisionRows(page)) {
295
+ if (!more || changedRead) { list.replaceChildren(); list.scrollTop = 0; }
296
+ const retainedRows = more && !presentation.renderRows && list.children.length > 0;
297
+ const knownVariantRefs = new Set(Array.from(list.children)
298
+ .map(item => item.dataset.variantRef).filter(Boolean));
299
+ for (const row of presentation.renderRows && !changedRead ? decisionRows(page) : []) {
300
+ if (row.variantRef && knownVariantRefs.has(row.variantRef)) continue;
289
301
  const item = document.createElement('li');
302
+ if (row.variantRef) { item.dataset.variantRef = row.variantRef; knownVariantRefs.add(row.variantRef); }
290
303
  const title = document.createElement('strong');
291
304
  const meta = document.createElement('span');
292
305
  title.textContent = row.title; meta.textContent = row.meta;
293
306
  item.append(title, meta); list.append(item);
294
- if (row.previewable) {
307
+ if (row.navigation) {
308
+ const button = document.createElement('button');
309
+ button.type = 'button'; button.textContent = row.navigation.label;
310
+ button.onclick = async () => {
311
+ try {
312
+ const result = await client.openDecision(row.navigation.variantRef, row.navigation.target);
313
+ $('decision-status').textContent = result.delivery === 'requested'
314
+ ? '已请求打开对应事项' : '打开对应事项尚未确认 · 请从 Cat Café 查看';
315
+ } catch { $('decision-status').textContent = '暂时无法打开对应事项 · 请稍后重试'; }
316
+ };
317
+ item.append(button);
318
+ } else if (row.previewable) {
295
319
  const button = document.createElement('button');
296
320
  button.type = 'button'; button.textContent = '查看确认演练';
297
321
  button.onclick = async () => {
@@ -307,17 +331,25 @@ if (!window.clowderCompanion) {
307
331
  item.append(button);
308
332
  }
309
333
  }
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;
334
+ decisionOffset = changedRead ? 0 : page.page.offset + page.page.limit;
335
+ $('decision-reload').textContent = changedRead ? '刷新' : presentation.reloadLabel;
336
+ $('decision-status').textContent = changedRead && page.totalCount !== 0
337
+ ? '待办已变化 · 请刷新查看最新列表'
338
+ : retainedRows
339
+ ? `${presentation.badge.state === 'authentication' ? '待办需要登录' : '待办暂不可读'} · 以下为上次读到的内容`
340
+ : presentation.message;
341
+ $('decision-more').hidden = changedRead || !presentation.hasMore;
317
342
  } catch {
318
343
  showDecisionBadge(undefined);
319
344
  motion.setPendingDecision(null);
320
- if (controls.panel === 'decisions') $('decision-status').textContent = '待办暂不可读 · 请稍后刷新';
345
+ if (controls.panel === 'decisions') {
346
+ const retained = $('decision-list').children.length > 0;
347
+ $('decision-status').textContent = retained
348
+ ? '待办暂不可读 · 以下为上次读到的内容'
349
+ : '待办暂不可读 · 请稍后刷新';
350
+ $('decision-reload').textContent = '重试';
351
+ $('decision-more').hidden = true;
352
+ }
321
353
  } finally { decisionLoading = false; }
322
354
  }
323
355
  $('share-badge').onclick = toggleScreen;