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
+101
View File
@@ -0,0 +1,101 @@
// Real portal + auth hook + API client, synthetic SMS/HTTP responses. Never sends an SMS.
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {mkdir,writeFile} from 'node:fs/promises';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
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/customer-sms-login');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:5322,strictPort:true}});
await server.listen();
let browser;
const checks=[],problems=[];
try {
browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_PATH??'C:/Program Files/Google/Chrome/Application/chrome.exe'});
const page=await browser.newPage({viewport:{width:1280,height:1080}});
page.on('pageerror',e=>problems.push(e.message));
await page.clock.install();
let signedIn=false, smsEnabled=true, expireMe=false, sendCount=0, bootstraps=0, lastRequestID, memoryWrites=0;
await page.route('**/api/v1/**',async route=>{
const r=route.request(), path=new URL(r.url()).pathname, body=r.method()==='POST'?r.postDataJSON():undefined;
let status=200,code='OK',data={};
if(path==='/api/v1/web/account/session') {
if(signedIn)data={authenticated:true,csrf_token:'account-csrf'};else{status=401;code='LOGIN_REQUIRED';}
} else if(path==='/api/v1/web/login/session') {bootstraps++;data={sms_enabled:smsEnabled,signup_enabled:true,csrf_token:'login-csrf'};}
else if (['/api/v1/web/login/captcha','/api/v1/web/auth/captcha'].includes(path)) { assert.equal(r.headers()['x-csrf-token'],path.includes('/auth/')?'setup-csrf':'login-csrf'); data={captcha_id:'captcha',image:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=',expires_at_ms:Date.now()+120000}; }
else if(path==='/api/v1/web/login/sms/send') {
assert.equal(r.headers()['x-csrf-token'],'login-csrf');assert.equal(r.headers().authorization,undefined);
assert.equal(body.captcha_id,'captcha');assert.equal(body.captcha_code,'654321');assert.equal(body.purpose,'LOGIN');assert.match(body.phone_e164,/^\+8613[0-9]{9}$/);
sendCount++;lastRequestID=r.headers()['x-request-id'];assert.match(lastRequestID,/^[a-f0-9-]{36}$/);
data={sms_challenge_id:`challenge-${sendCount}`,resend_after_ms:60000,expires_at_ms:Date.now()+300000};
} else if(path==='/api/v1/web/login/sms/verify') {
assert.equal(r.headers()['x-csrf-token'],'login-csrf');assert.equal(body.sms_challenge_id,`challenge-${sendCount}`);
if(body.code!=='654321'){status=401;code='SMS_CODE_INVALID';}else{signedIn=true;data={csrf_token:'account-csrf',data:{authenticated:true}};}
} else if(path==='/api/v1/me') {
assert.equal(r.headers().authorization,undefined);
if(!signedIn||expireMe){signedIn=false;status=401;code='LOGIN_REQUIRED';}
else data={identity:{issuer:'urn:platform:onboarding:sms',subject:'synthetic',display_name:'短信验收用户',preferred_username:'短信验收用户'},accounts:[]};
} else if(path==='/api/v1/memories/query') {
assert.equal(r.headers().authorization,undefined);assert.equal(r.headers()['x-csrf-token'],'account-csrf');memoryWrites++;data={items:[]};
} else if(path==='/api/v1/content-models'){status=401;code='AUTHENTICATION_REQUIRED';}
else if(path==='/api/v1/web/auth/logout'){signedIn=false;data={signed_out:true};}
else if(path==='/api/v1/web/account/password') data={available:false};
else {throw new Error(`Unexpected API: ${path}`);}
await route.fulfill({status,contentType:'application/json',body:JSON.stringify({ok:status===200,code,message:code==='SMS_CODE_INVALID'?'验证码错误或已失效':code,data})});
});
await page.goto('http://127.0.0.1:5322/?section=profile');
await page.getByRole('heading',{name:'手机号登录'}).waitFor();
await page.waitForFunction(()=>!document.querySelector('#customer-phone').disabled);
assert.equal(bootstraps,1);assert.equal(await page.locator('input[type=password]').count(),0);
await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'login-desktop.png'),fullPage:true});
checks.push('SMS is the default login; StrictMode creates one bootstrap request');
assert.equal(new URL(page.url()).pathname,'/login');
assert.equal(new URL(page.url()).searchParams.get('returnTo'),'/?section=profile');
assert.equal(await page.locator('.customer-layout,.maintenance-layout').count(),0);
checks.push('protected page redirects to standalone login with no business navigation');
const phone=page.getByLabel('手机号',{exact:true}), otp=page.getByLabel('短信验证码',{exact:true}), form=page.locator('.sms-login-form');
assert(await form.getByRole('button',{name:'获取验证码'}).isDisabled());
await phone.fill('13800000000');assert(await form.getByRole('button',{name:'获取验证码'}).isDisabled());await page.getByLabel('图片验证码',{exact:true}).fill('654321');await form.getByRole('button',{name:'获取验证码'}).click();
await page.getByText(/验证码已发送至 138\*\*\*\*0000/).waitFor();
assert.equal(sendCount,1);assert(await form.getByRole('button',{name:/秒后重发/}).isDisabled());
checks.push('send uses normalized phone, CSRF, unique request ID and a resend countdown');
await otp.fill('654321');await phone.fill('13900000000');
assert(await form.getByRole('button',{name:/^登\s*录$/}).isDisabled());
checks.push('changing phone clears the old challenge and code');
await page.clock.fastForward(61000);await page.getByLabel('图片验证码',{exact:true}).fill('654321');await form.getByRole('button',{name:'获取验证码'}).click();
await page.getByText(/验证码已发送至 139\*\*\*\*0000/).waitFor();
await otp.fill('000000');await form.getByRole('button',{name:/^登\s*录$/}).click();
await page.getByText('验证码错误或已失效',{exact:true}).waitFor();
assert.equal(await page.getByRole('heading',{name:'手机号登录'}).count(),1);
checks.push('incorrect OTP stays on the login form and does not trigger a global session failure');
await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'desktop.png'),fullPage:true});
await page.setViewportSize({width:375,height:920});await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'mobile.png'),fullPage:true});
assert(await page.evaluate(()=>document.documentElement.scrollWidth<=document.documentElement.clientWidth+2));
checks.push('375px mobile page has no horizontal overflow');
await otp.fill('654321');await form.getByRole('button',{name:/^登\s*录$/}).click();
await page.getByRole('button',{name:'打开账户菜单'}).waitFor();assert.equal(await page.getByRole('heading',{name:'手机号登录'}).count(),0);
assert.equal(new URL(page.url()).search,'?section=profile');
await page.reload();await page.getByRole('button',{name:'打开账户菜单'}).waitFor();
assert.equal(await page.evaluate(()=>Object.keys(sessionStorage).filter(k=>k.startsWith('oidc.user:')).length),0);
checks.push('successful SMS login restores the customer workspace after reload without an OIDC token');
await page.evaluate(async()=>{const api=await import('/src/api/client.ts');await api.authenticatedJSON('POST','/api/v1/memories/query','',undefined,{elder_id:'synthetic'});});
assert.equal(memoryWrites,1);checks.push('customer API writes send account CSRF with cookie authentication, without an empty Bearer header');
await page.evaluate(async()=>{const {platformApi}=await import('/src/api/client.ts');await platformApi.contentModels('synthetic','').catch(()=>{});});
await page.waitForTimeout(250);assert.equal(new URL(page.url()).pathname,'/');await page.getByRole('button',{name:'打开账户菜单'}).waitFor();
await page.evaluate(async()=>{const {userManager}=await import('/src/auth/oidc.ts');await userManager.events._silentRenewError.raise(new Error('old OIDC error'));});
await page.waitForTimeout(100);assert.equal(new URL(page.url()).pathname,'/');
checks.push('resource 401 and legacy renewal failure cannot discard a valid phone session');
expireMe=true;
await page.evaluate(async()=>{const {platformApi}=await import('/src/api/client.ts');await platformApi.currentUser('').catch(()=>{});});
await page.getByRole('heading',{name:'手机号登录'}).waitFor();assert.equal(await page.getByText('短信验收用户',{exact:true}).count(),0);
assert.equal(new URL(page.url()).searchParams.get('returnTo'),'/?section=profile');
checks.push('an expired customer session removes prior private account content');
smsEnabled=false;await page.reload();await page.getByText('短信登录暂未开放,请使用密码登录',{exact:true}).waitFor();
await phone.fill('13800000000');assert(await form.getByRole('button',{name:'获取验证码'}).isDisabled());
checks.push('an unconfigured SMS service is shown as unavailable, never as a successful send');
assert.deepEqual(problems,[]);
await writeFile(resolve(output,'result.json'),JSON.stringify({checks,problems,boundary:'Synthetic SMS and API responses; no real SMS sent.'},null,2));
console.log(JSON.stringify({checks,problems}));
} finally {await browser?.close();await server.close();}