Files
NianToB/electron/yinian/mock-control-plane.ts

303 lines
8.9 KiB
TypeScript

import type {
YinianAuthSession,
YinianConfigSnapshot,
YinianHotel,
YinianImageCaptcha,
YinianLoginWithPasswordInput,
YinianLoginWithSmsInput,
YinianLocalSkill,
YinianServerStatus,
YinianSessionState,
YinianSkillSyncResult,
YinianUser,
} from '../../shared/yinian';
import type { YinianControlPlane } from './control-plane';
import { getYinianStorage, type YinianStorage } from './storage';
const MOCK_USER: YinianUser = {
id: 'user_demo_manager',
name: '王管理员',
phone: '13800000000',
email: 'manager@yinian.local',
};
const MOCK_HOTELS: YinianHotel[] = [
{
id: 'workspace_hangzhou_ops',
name: '智念企业组织空间',
brand: '智念',
},
{
id: 'workspace_shanghai_growth',
name: '智念增长组织空间',
brand: '智念',
},
];
function createSession(user: YinianUser = MOCK_USER, currentHotelId = MOCK_HOTELS[0].id): YinianAuthSession {
return {
authenticated: true,
user,
hotels: MOCK_HOTELS,
currentHotelId,
accessTokenExpiresAt: Date.now() + 60 * 60 * 1000,
};
}
interface MockYinianControlPlaneOptions {
storage?: YinianStorage;
}
export class MockYinianControlPlane implements YinianControlPlane {
private readonly storage: YinianStorage;
private currentSession: YinianSessionState = { authenticated: false };
private localSkills: YinianLocalSkill[] = [];
constructor(options: MockYinianControlPlaneOptions = {}) {
this.storage = options.storage ?? getYinianStorage();
}
async getServerStatus(): Promise<YinianServerStatus> {
return {
mode: 'mock',
reachable: true,
checkedAt: Date.now(),
message: '当前使用本地演示数据',
};
}
async createImageCaptcha(randomStr = crypto.randomUUID()): Promise<YinianImageCaptcha> {
return {
randomStr,
image: '',
mimeType: 'image/svg+xml',
raw: { mock: true },
};
}
async restoreSession(): Promise<YinianSessionState> {
const persisted = await this.storage.getSession();
if (!persisted || persisted.mode !== 'mock') {
this.currentSession = { authenticated: false };
return this.currentSession;
}
this.currentSession = {
authenticated: true,
user: persisted.user,
hotels: persisted.hotels,
currentHotelId: persisted.currentHotelId,
accessTokenExpiresAt: persisted.accessTokenExpiresAt,
};
const registry = await this.storage.getSkillRegistry(persisted.currentHotelId);
this.localSkills = registry && 'skills' in registry ? registry.skills : [];
return this.currentSession;
}
async getSessionState(): Promise<YinianSessionState> {
if (!this.currentSession.authenticated) {
return this.restoreSession();
}
return this.currentSession;
}
async loginWithSms(input: YinianLoginWithSmsInput): Promise<YinianAuthSession> {
const phone = input.phone.trim();
if (!phone || !input.code.trim()) {
throw new Error('手机号和验证码不能为空');
}
this.currentSession = createSession({ ...MOCK_USER, phone });
await this.persistCurrentSession();
return this.currentSession;
}
async loginWithPassword(input: YinianLoginWithPasswordInput): Promise<YinianAuthSession> {
const account = input.account.trim();
if (!account || !input.password.trim()) {
throw new Error('账号和密码不能为空');
}
this.currentSession = createSession({
...MOCK_USER,
name: account === 'admin' ? 'NIANXX 实施' : MOCK_USER.name,
email: account.includes('@') ? account : MOCK_USER.email,
});
await this.persistCurrentSession();
return this.currentSession;
}
async logout(): Promise<YinianSessionState> {
if (this.currentSession.authenticated) {
await this.storage.clearConfig(this.currentSession.currentHotelId);
await this.storage.clearSkillRegistry(this.currentSession.currentHotelId);
}
await this.storage.clearSession();
this.currentSession = { authenticated: false };
this.localSkills = [];
return this.currentSession;
}
async switchHotel(hotelId: string): Promise<YinianAuthSession> {
if (!this.currentSession.authenticated) {
throw new Error('请先登录');
}
const hotel = MOCK_HOTELS.find((item) => item.id === hotelId);
if (!hotel) {
throw new Error('工作空间不存在或未开通');
}
this.currentSession = {
...this.currentSession,
currentHotelId: hotel.id,
};
await this.persistCurrentSession();
const registry = await this.storage.getSkillRegistry(hotel.id);
this.localSkills = registry && 'skills' in registry ? registry.skills : [];
return this.currentSession;
}
async getConfigSnapshot(): Promise<YinianConfigSnapshot> {
if (!this.currentSession.authenticated) {
throw new Error('请先登录');
}
const currentHotelId = this.currentSession.currentHotelId;
const hotel = this.currentSession.hotels.find((item) => item.id === currentHotelId)
?? this.currentSession.hotels[0];
const snapshot = {
serverTime: Date.now(),
user: this.currentSession.user,
hotel,
hotels: this.currentSession.hotels,
entitlements: [
{
skillId: 'data-check',
name: '数据检查助手',
version: '1.0.0',
enabled: true,
category: 'ota-monitoring',
triggers: ['manual', 'scheduled'],
lastRunAt: Date.now() - 48 * 60 * 1000,
},
{
skillId: 'workflow-check',
name: '流程检查助手',
version: '1.0.0',
enabled: true,
category: 'ops-automation',
triggers: ['manual', 'scheduled'],
lastRunAt: Date.now() - 55 * 60 * 1000,
},
{
skillId: 'daily-report',
name: '日报生成助手',
version: '0.1.0',
enabled: true,
category: 'reporting',
triggers: ['manual', 'scheduled'],
lastRunAt: Date.now() - 2 * 60 * 60 * 1000,
},
{
skillId: 'customer-reply-helper',
name: '客户回复助手',
version: '0.1.0',
enabled: true,
category: 'guest-comm',
triggers: ['manual', 'reply'],
},
],
notificationChannels: [
{
id: 'ch_wecom_ops',
kind: 'wecom',
label: '业务通知群',
recipient: '当前组织空间通知群',
enabled: true,
source: 'nianxx',
},
{
id: 'ch_desktop',
kind: 'desktop',
label: '本机桌面通知',
recipient: '当前设备',
enabled: true,
source: 'kernel',
},
],
featureFlags: {
yinianTodayPage: true,
autoUpdateSkills: true,
reviewReplyHelper: true,
},
uiPolicy: {
defaultPage: 'today',
showAdvancedSettings: false,
},
};
await this.storage.setConfig(snapshot);
return snapshot;
}
async syncSkills(): Promise<YinianSkillSyncResult> {
const config = await this.getConfigSnapshot();
const now = Date.now();
const previousRegistry = await this.storage.getSkillRegistry(config.hotel.id);
const previousSkills = previousRegistry && 'skills' in previousRegistry ? previousRegistry.skills : this.localSkills;
this.localSkills = config.entitlements.map((skill) => {
const previous = previousSkills.find((item) => item.skillId === skill.skillId);
return {
skillId: skill.skillId,
name: skill.name,
version: skill.version,
enabled: skill.enabled,
installedAt: previous?.installedAt ?? now,
lastSyncedAt: now,
status: previous?.version === skill.version ? 'skipped' : previous ? 'updated' : 'installed',
source: 'mock',
bundleSha256: `mock-${skill.skillId}-${skill.version}`,
};
});
await this.storage.setSkillRegistry({
hotelId: config.hotel.id,
updatedAt: now,
skills: this.localSkills,
});
return {
hotelId: config.hotel.id,
syncedAt: now,
skills: this.localSkills,
};
}
async listLocalSkills(): Promise<YinianLocalSkill[]> {
if (this.currentSession.authenticated) {
const registry = await this.storage.getSkillRegistry(this.currentSession.currentHotelId);
if (registry && 'skills' in registry) {
this.localSkills = registry.skills;
}
}
return this.localSkills;
}
async getSkillRegistry(hotelId?: string) {
return this.storage.getSkillRegistry(hotelId);
}
private async persistCurrentSession(): Promise<void> {
if (!this.currentSession.authenticated) return;
await this.storage.setSession({
mode: 'mock',
user: this.currentSession.user,
hotels: this.currentSession.hotels,
currentHotelId: this.currentSession.currentHotelId,
accessTokenExpiresAt: this.currentSession.accessTokenExpiresAt,
updatedAt: Date.now(),
});
}
}