303 lines
8.9 KiB
TypeScript
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(),
|
|
});
|
|
}
|
|
}
|