feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+117
View File
@@ -0,0 +1,117 @@
// Real customer workspace, synthetic responses only. No production requests.
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { installCustomerFixture, currentUser, households, id } from './fixtures/customer-mobile.mjs';
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
const { chromium } = require('playwright');
const { createServer } = await import(pathToFileURL(resolve('web/node_modules/vite/dist/node/index.js')));
const output = resolve('.design-evidence/consent-guidance');
await mkdir(output, { recursive: true });
const server = await createServer({ configFile: false, root: resolve('web'), oxc: { jsx: { runtime: 'automatic' } }, logLevel: 'error', server: { host: '127.0.0.1', port: 5340, strictPort: true }, plugins: [{
name: 'consent-guidance-harness',
configureServer(s) { s.middlewares.use('/__consent-test', (_q, r) => { r.setHeader('Content-Type', 'text/html'); r.end('<!doctype html><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><div id="root"></div><script type="module" src="/@id/__consent-module.tsx"></script>'); }); },
resolveId(name) { if (name === '__consent-module.tsx') return '\0__consent-module.tsx'; },
load(name) { if (name === '\0__consent-module.tsx') return `import React from 'react';import {createRoot} from 'react-dom/client';import {SaaSWorkspace} from '/src/pages/SaaSWorkspace.tsx';import '/src/styles.css';import '/src/cal-theme.css';function App(){const [section,setSection]=React.useState('conversations');return React.createElement(React.Fragment,null,React.createElement('nav',null,['conversations','stories','subjects','privacy','sharing'].map(value=>React.createElement('button',{key:value,onClick:()=>setSection(value)},'测试'+value))),React.createElement(SaaSWorkspace,{accessToken:'synthetic-consent-token',accounts:${JSON.stringify(currentUser.accounts)},section,onSectionChange:setSection,redesignedHome:true,showNavigation:false}));};createRoot(document.getElementById('root')).render(React.createElement(React.StrictMode,null,React.createElement(App)));`; },
}] });
await server.listen();
let browser;
try {
browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' });
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
await installCustomerFixture(context, { authenticated: false });
const page = await context.newPage(), errors = [], writes = [], grants = new Map();
const session = '11111111-1111-4111-8111-111111111111';
let failed = false, viewer = false, transcriptDenied = false, recordingState = 'CONSENT_REQUIRED', holdSubject = '', releaseSubject;
const response = (route, data, status = 200, code = 'OK') => route.fulfill({ status, json: { ok: status === 200, data, code, message: code, request_id: 'synthetic-consent' } });
page.on('pageerror', e => errors.push(e.message));
await page.route('**/api/v1/**', async route => {
const req = route.request(), path = new URL(req.url()).pathname;
if (path === '/api/v1/households') return response(route, { items: households.map(h => viewer ? { ...h, role: 'viewer', capabilities: ['household.read', 'subject.read', 'conversation.read'] } : h) });
if (/\/subjects\/[^/]+\/consents$/.test(path)) {
const subjectID = path.split('/')[4];
if (req.method() === 'POST') {
const body = req.postDataJSON(); writes.push({ subjectID, body });
const item = { ...body, id: id(999), subject_id: subjectID, actor_user_id: id(100), granted_at: new Date().toISOString(), effective: true, version: 1 };
grants.set(subjectID, [...(grants.get(subjectID) ?? []), item]); return response(route, item);
}
if (subjectID === holdSubject) await new Promise(resolve => { releaseSubject = resolve; });
return response(route, { items: grants.get(subjectID) ?? [] }, failed ? 503 : 200, failed ? 'UNAVAILABLE' : 'OK');
}
if (path.startsWith('/api/v1/memories/')) {
const op = path.split('/memories/')[1];
if (op === 'calls/query') return response(route, { items: [{ id: session, status: 'ENDED', transcript_state: 'PARTIAL', created_at: new Date().toISOString(), ended_at: new Date().toISOString(), duration_ms: 30000 }], next_cursor: '' });
if (op === 'calls/turns') return response(route, { items: [], next_cursor: '' }, transcriptDenied ? 403 : 200, transcriptDenied ? 'TRANSCRIPT_ACCESS_DENIED' : 'OK');
if (op === 'calls/recordings') return response(route, { items: [], state: recordingState, can_play: false, can_download: false, playback_ready: true, can_manage: !viewer, requester_id: id(100) });
if (op === 'query') return response(route, { items: [], mutations: [], next_cursor: '', can_write: false, can_manage_access: false });
if (op === 'sync/status') return response(route, { memory_revision: 0, viking_visible_revision: 0, pending_count: 0, blocked_count: 0, upload_pending_count: 0, extracting_count: 0, sync_state: 'EMPTY' });
throw new Error(`Unexpected memory operation ${op}`);
}
if (path.endsWith('/events') || path.endsWith('/events/history') || path.endsWith('/firmware') || path.endsWith('/commands') || path.endsWith('/home-overview')) return response(route, { items: [] });
return route.fallback();
});
const guidance = page.getByRole('region', { name: '当前功能的授权提示' });
await page.goto('http://127.0.0.1:5340/__consent-test');
await guidance.getByText(/目前无法保存新聊天的文字/).waitFor();
await page.screenshot({ path: resolve(output, 'owner-shortcuts.png'), fullPage: true });
await page.getByText('文字不完整或未保存', { exact: false }).first().waitFor();
assert.equal(await page.getByText('已保存部分文字', { exact: true }).count(), 0);
await page.getByRole('button', { name: /30 秒/ }).click();
await page.getByText(/如果聊天时未授权,文字不会保存/).waitFor();
await page.getByRole('region', { name: '本次聊天录音' }).getByRole('button', { name: '去授权保存原始录音' }).waitFor();
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click();
let dialog = page.getByRole('dialog');
assert(await dialog.getByRole('button', { name: '确认授权', exact: true }).isDisabled());
await page.waitForTimeout(300); // Capture the settled modal after its entrance animation.
await page.screenshot({ path: resolve(output, 'grant-dialog.png'), fullPage: true });
await dialog.getByRole('button', { name: /^取\s*消$/ }).click();
assert.equal(writes.length, 0);
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click();
dialog = page.getByRole('dialog');
await dialog.getByRole('checkbox').check();
await dialog.getByRole('button', { name: '确认授权', exact: true }).click();
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).waitFor({ state: 'hidden' });
assert.equal(writes.length, 1); assert.equal(writes[0].subjectID, id(400));
assert.equal(writes[0].body.type, 'transcript_storage'); assert.equal(writes[0].body.policy_version, '2026-09'); assert.match(writes[0].body.notice_digest, /^[0-9a-f]{64}$/);
assert.equal(grants.get(id(400)).length, 1);
await page.getByRole('button', { name: '测试stories', exact: true }).click();
await guidance.getByText(/无法在新聊天中使用长期记忆/).waitFor();
await guidance.getByText(/不会自动整理为故事候选或摘要/).waitFor();
await page.getByText(/当前账号只有查看权限/).waitFor();
await guidance.getByRole('button', { name: '去授权个性化记忆', exact: true }).click();
await page.getByRole('dialog').getByRole('button', { name: /^取\s*消$/ }).click();
await page.getByRole('button', { name: '测试conversations', exact: true }).click();
await page.getByRole('dialog').waitFor({ state: 'hidden' });
holdSubject = id(401);
await page.getByRole('combobox', { name: '选择长者', exact: true }).selectOption(id(401));
await page.getByText('正在核对数据授权…', { exact: true }).waitFor();
assert.equal(await page.getByRole('button', { name: '去授权保存文字记录', exact: true }).count(), 0);
while (!releaseSubject) await new Promise(resolve => setTimeout(resolve, 20));
holdSubject = ''; releaseSubject();
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).waitFor();
await guidance.getByRole('button', { name: '去授权保存文字记录', exact: true }).click();
await page.getByRole('dialog').getByText('陈建国', { exact: true }).waitFor();
await page.getByRole('dialog').getByRole('button', { name: /^取\s*消$/ }).click();
failed = true; await page.reload();
await page.getByText('暂时无法读取授权状态,请重试后核对。', { exact: false }).waitFor();
assert.equal(await page.getByText(/因为尚未授权“保存文字记录”/).count(), 0);
failed = false; await page.getByRole('button', { name: '重试授权状态', exact: true }).click();
await guidance.waitFor();
viewer = true; grants.clear(); recordingState = 'READY'; transcriptDenied = true;
await page.reload();
await guidance.getByText(/当前账号不能新增数据授权/).waitFor();
assert.equal(await guidance.getByRole('button', { name: /^去授权/ }).count(), 0);
await page.getByRole('button', { name: /30 秒/ }).click();
await page.getByText(/查看设备聊天文字需要/).waitFor();
await page.getByText(/当前账号没有播放权限/).waitFor();
await page.getByText(/当前账号没有下载权限/).waitFor();
assert.equal(await page.getByRole('button', { name: /^授权本账号/ }).count(), 0);
await page.screenshot({ path: resolve(output, 'desktop.png'), fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
await page.screenshot({ path: resolve(output, 'mobile.png'), fullPage: true });
assert((await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)) < 3);
assert.deepEqual(errors, []);
console.log('PASS: missing consent and historical gaps; explicit single-item grant/cancel; dependent features; subject isolation; fetch failure/retry; viewer restrictions; transcript/recording access instructions; mobile layout');
} finally { await browser?.close(); await server.close(); }