feat: prepare Zhinian desktop client for pilot release
This commit is contained in:
302
electron/yinian/mock-control-plane.ts
Normal file
302
electron/yinian/mock-control-plane.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user