Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions catalog/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@
"voice"
],
"versions": [
{
"version": "0.1.0-alpha.22",
"contractVersion": "0.1.0",
"manifestPath": "plugin.yaml",
"artifact": {
"kind": "npm",
"packageName": "@clowder-ai/companion",
"version": "0.1.0-alpha.22",
"tarballUrl": "https://registry.npmjs.org/@clowder-ai/companion/-/companion-0.1.0-alpha.22.tgz",
"integrity": "sha512-7le73ak9F8fdFWzPG/11oN3HVfTaJT1dd193GyX39LRT0SjnJi3wvLj0SMFKjsGc6MNQbz5kQHCTB/pG2JUPdw==",
"shasum": "c2ad9ef5d763dec7dbe87e9e484e1da0708555f1",
"provenance": {
"repository": "https://github.com/zts212653/clowder-ai-plugins",
"sourceDirectory": "packages/companion"
}
}
},
{
"version": "0.1.0-alpha.21",
"contractVersion": "0.1.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/companion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clowder-ai/companion",
"version": "0.1.0-alpha.21",
"version": "0.1.0-alpha.22",
"description": "The desktop voice body for your Clowder companion",
"license": "MIT",
"type": "module",
Expand All @@ -27,8 +27,8 @@
"prepack": "pnpm build"
},
"devDependencies": {
"@clowder-ai/plugin-contract": "0.1.0-beta.25",
"@clowder-ai/plugin-sdk": "0.1.0-beta.21",
"@clowder-ai/plugin-contract": "0.1.0-beta.26",
"@clowder-ai/plugin-sdk": "0.1.0-beta.22",
"playwright": "1.61.1"
},
"repository": {
Expand Down
3 changes: 3 additions & 0 deletions packages/companion/scripts/preview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ export function fixture(skin, failure, pending = true) {
];
window.__pushTranscript = row => transcriptRows.push(row);
let phase = 'idle', allowed = true;
let meeting = {kind:'running',sharing:false,paused:false,sourceLabel:'Zoom · Cat Café 产品周会(核心桌面协作与会议转写状态验证)'};
window.__setMeeting = value => { meeting = value; };
let companionSettings = {dutyCatProfileId:'fable-5',skin:${JSON.stringify(skin)},ballSize:72,behaviorEnabled:true,proactivePolicy:'quiet-badge',personaTone:'温暖、简短、不啰嗦',householdReadsAllowed:true};
const identity = () => ({kind:'state',phase,displayName:'宪宪',skin:companionSettings.skin,documentsAllowed:allowed,toolsReady:phase==='talking',behaviorEnabled:companionSettings.behaviorEnabled,nativeActivity:'none',
audio:{supportedModes:['duplex','receive_only'],activeMode:phase==='talking'?(activeMode??'duplex'):null},
meeting,
liveTransport:{kind:'gpt_live_v3',verifiedModel:null},nativeWork:{scopeId:phase==='idle'?null:'0123456789abcdef',revision:0,active:[],recent:[]},
duty:{catId:'preview-cat',displayName:'宪宪'},carrier:{catId:'preview-cat',displayName:'宪宪'}});
let activeMode = null; const emit = event => listeners.forEach(fn=>fn(event));
Expand Down
9 changes: 7 additions & 2 deletions packages/companion/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
</section>
<section id="actions" class="floating call-bar" aria-label="当前通话" hidden>
<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>
<p id="call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="call-meeting-label" class="meeting-label"></span><span id="call-meeting-source" class="meeting-source" hidden></span></p>
<p id="call-work" class="call-work" hidden></p>
<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>
<div class="call-recent" aria-label="最新消息">
Expand All @@ -67,6 +68,7 @@
<section id="menu" class="floating name-card" aria-label="猫猫名片" hidden>
<div id="menu-call-context" class="call-context" hidden>
<span class="status-dot" aria-hidden="true"></span><strong id="menu-call-state"></strong><span id="menu-call-mic-state"></span>
<small id="menu-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="menu-call-meeting-label" class="meeting-label"></span><span id="menu-call-meeting-source" class="meeting-source" hidden></span></small>
<small id="menu-call-work" hidden></small>
<div id="menu-call-actions" class="call-context-actions menu-call-actions">
<button id="menu-mic" data-action="mute" aria-label="静音"><svg class="icon" aria-hidden="true"><use id="menu-mic-icon" href="#i-mic"/></svg></button>
Expand Down Expand Up @@ -111,8 +113,9 @@
<section id="chat" class="floating" aria-label="和猫猫写几句" hidden>
<div id="chat-call-context" class="call-context" hidden>
<span class="status-dot" aria-hidden="true"></span><strong id="chat-call-state"></strong><span id="chat-call-mic-state"></span>
<small id="chat-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="chat-call-meeting-label" class="meeting-label"></span><span id="chat-call-meeting-source" class="meeting-source" hidden></span></small>
<small id="chat-call-work" hidden></small>
<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>
<div class="call-context-actions call-context-inline-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>
<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>
</div>
<header><strong id="chat-name">和猫猫写几句</strong><button data-action="dismiss" aria-label="收起文字">收起</button></header>
Expand All @@ -124,6 +127,7 @@
<section id="transcript" class="floating call-transcript" data-expanded="false" aria-label="这次通话的字幕" hidden>
<div id="transcript-call-context" class="call-context transcript-call-context">
<span class="status-dot" aria-hidden="true"></span><strong id="transcript-call-state"></strong><span id="transcript-call-mic-state"></span>
<small id="transcript-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="transcript-call-meeting-label" class="meeting-label"></span><span id="transcript-call-meeting-source" class="meeting-source" hidden></span></small>
<small id="transcript-call-work" hidden></small>
<div class="call-context-actions">
<button id="transcript-return" class="transcript-return" type="button" data-action="transcript-return"><span aria-hidden="true">‹</span><span>返回通话</span></button>
Expand All @@ -142,8 +146,9 @@
<section id="settings" class="floating settings-panel" aria-label="猫猫球设置" hidden>
<div id="settings-call-context" class="call-context" hidden>
<span class="status-dot" aria-hidden="true"></span><strong id="settings-call-state"></strong><span id="settings-call-mic-state"></span>
<small id="settings-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="settings-call-meeting-label" class="meeting-label"></span><span id="settings-call-meeting-source" class="meeting-source" hidden></span></small>
<small id="settings-call-work" hidden></small>
<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>
<div class="call-context-actions call-context-inline-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>
<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>
</div>
<header class="settings-head"><button id="settings-back" type="button" aria-label="返回名片">‹</button><strong id="settings-title">设置</strong></header>
Expand Down
55 changes: 55 additions & 0 deletions packages/companion/src/meeting-state.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export function meetingPresentation(meeting) {
if (!meeting || meeting.kind === 'idle') return null;
if (meeting.kind === 'running') {
if (meeting.paused) return { label: '会议记录已暂停', sourceLabel: meeting.sourceLabel, tone: 'quiet' };
return meeting.sharing
? { label: '听会中', sourceLabel: meeting.sourceLabel, tone: 'active' }
: { label: '会议记录中 · 未接入通话', sourceLabel: meeting.sourceLabel, tone: 'active' };
}
if (meeting.kind === 'stopped') return { label: '会议记录已停止', sourceLabel: null, tone: 'quiet' };
if (meeting.kind === 'unconfirmed') return { label: '会议记录状态未确认', sourceLabel: null, tone: 'warning' };
if (meeting.kind === 'needs_source') return { label: '需要确认会议所在的 App', sourceLabel: null, tone: 'warning' };
return null;
}

const meetingPrefixes = ['call', 'menu-call', 'chat-call', 'transcript-call', 'settings-call'];
export function renderMeetingStatus(document, presentation) {
for (const prefix of meetingPrefixes) {
const row = document.getElementById(`${prefix}-meeting`);
const source = document.getElementById(`${prefix}-meeting-source`);
row.hidden = presentation === null;
row.dataset.tone = presentation?.tone ?? '';
document.getElementById(`${prefix}-meeting-label`).textContent = presentation?.label ?? '';
source.textContent = presentation?.sourceLabel ? ` · ${presentation.sourceLabel}` : '';
source.hidden = !presentation?.sourceLabel;
}
}

/** Keeps the stopped acknowledgement transient without hiding uncertain or active capture truth. */
export class MeetingStatus {
constructor({ now = () => Date.now() } = {}) {
this.now = now;
this.previousKind = null;
this.stoppedUntil = 0;
}

update(meeting) {
const kind = meeting?.kind ?? 'idle';
if (kind === 'idle') {
this.previousKind = 'idle';
this.stoppedUntil = 0;
return null;
}
if (kind === 'stopped') {
if (this.previousKind !== 'stopped') {
this.stoppedUntil = this.previousKind !== null && this.previousKind !== 'idle'
? this.now() + 10_000 : 0;
}
this.previousKind = 'stopped';
return this.now() < this.stoppedUntil ? meetingPresentation(meeting) : null;
}
this.previousKind = kind;
this.stoppedUntil = 0;
return meetingPresentation(meeting);
}
}
8 changes: 8 additions & 0 deletions packages/companion/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ button:disabled { opacity: .45; cursor: default; }
.call-status-line span:not(.status-dot) { color: var(--desktop-muted); font-size: 12px; }
.status-dot { width: 8px; height: 8px; flex: none; border-radius: 999px; background: var(--desktop-success); }
[data-connection-state="recovering"] .status-dot { background: var(--desktop-warning); }
.call-meeting { min-width: 0; margin: 3px 0 0 15px; display: flex; align-items: center; gap: 5px; color: var(--desktop-muted); font-size: 12px; line-height: 1.35; }
.call-context > .call-meeting { grid-column: 1 / -1; margin: 0; }
.meeting-status-dot { width: 7px; height: 7px; flex: none; border-radius: 999px; background: var(--desktop-success); }
.call-meeting[data-tone="quiet"] .meeting-status-dot { background: var(--desktop-muted); }
.call-meeting[data-tone="warning"] .meeting-status-dot { background: var(--desktop-warning); }
.meeting-label { flex: none; white-space: nowrap; }
.meeting-source { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.call-work { margin: 3px 0 0 15px; color: var(--desktop-muted); font-size: 12px; }
.call-share-context { display: flex; align-items: center; gap: 7px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--desktop-line); }
.call-share-context > span, .call-share-line > span { display: grid; gap: 1px; min-width: 0; }
Expand Down Expand Up @@ -132,6 +139,7 @@ button:disabled { opacity: .45; cursor: default; }
.call-context > small { grid-column: 2 / 4; }
.call-share-line { grid-column: 1 / -1; display: flex; align-items: center; gap: 7px; padding-top: 6px; border-top: 1px solid var(--desktop-line); min-width: 0; }
.call-context-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
.call-context-inline-actions { grid-column: 4; grid-row: 1; }
.call-context-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 9px; white-space: nowrap; }
#menu .call-context { margin: -8px -8px 5px; }
.menu-footer { flex: none; display: flex; align-items: center; gap: 2px; margin: 4px -8px -8px; border-top: 1px solid var(--desktop-line); padding: 4px; }
Expand Down
3 changes: 3 additions & 0 deletions packages/companion/src/surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { currentCompanionIdentity } from './companion-identity.mjs';
import { FoldedChatUnread } from './chat-unread.mjs';
import { COMPACT_BALL_SIZE, compactSizeAction } from './compact-size.mjs';
import { WorkOverviewView } from './work-overview.mjs';
import { MeetingStatus, renderMeetingStatus } from './meeting-state.mjs';
const $ = id => document.getElementById(id);
const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
const status = text => {
Expand All @@ -35,6 +36,7 @@ const callTranscript = new CallTranscriptView($('call-transcript-log'), {
});
const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
const callBubble = new RecentBubble([$('call-bubble-first'), $('call-bubble-second')]);
const meetingStatus = new MeetingStatus();
const foldedChatUnread = new FoldedChatUnread(count => {
$('call-chat-unread').hidden = count === 0;
$('call-chat-unread').textContent = count ? String(count) : '';
Expand Down Expand Up @@ -214,6 +216,7 @@ if (!window.clowderCompanion) {
$('menu-hide-label').textContent = active ? '结束通话并隐藏' : '隐藏';
const identity = value.identity;
const companionIdentity = currentCompanionIdentity(identity);
renderMeetingStatus(document, meetingStatus.update(active ? identity?.meeting : null));
const nativeSnapshot = normalizeNativeWork(identity?.nativeWork);
const workLabel = nativeWorkLabel(nativeSnapshot, identity?.nativeActivity);
const workState = workLabel ? `${identity.displayName}${workLabel}` : '';
Expand Down
1 change: 1 addition & 0 deletions packages/companion/test/conversation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const CALL_B = '22222222-2222-4222-8222-222222222222';
const state = (phase = 'ready') => ({ kind: 'state', phase, displayName: '宪宪', skin: 'xianxian-codex', documentsAllowed: true, toolsReady: true,
behaviorEnabled: true,
nativeActivity: 'none', liveTransport: { kind: 'gpt_live_v3', verifiedModel: null },
meeting: { kind: 'idle', sharing: false, paused: false, sourceLabel: null },
nativeWork: { scopeId: phase === 'idle' ? null : '0123456789abcdef', revision: 0, active: [], recent: [] },
duty: { catId: 'opus5', displayName: '宪宪' }, carrier: { catId: 'codex-astra', displayName: '砚砚' } });
const deferred = () => { let resolve; const promise = new Promise(r => { resolve = r; }); return { promise, resolve }; };
Expand Down
Loading
Loading