feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口

补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
wikiboss
2026-09-23 18:01:47 +08:00
父节点 8ea9c25386
当前提交 62e0ae4435
共有 227 个文件被更改,包括 21855 次插入 和 5264 次删除

查看文件

@@ -1,5 +1,5 @@
import { DeepSeekService } from '../../../src/modules/ai/service/deepseek';
import { DeepSeekConfig } from '../../../src/comm/types/omni';
import { AiDeepSeekConfigEntity } from '../../../src/modules/ai/entity/deepseek-config';
describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
let service: DeepSeekService;
@@ -48,6 +48,81 @@ describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
expect(cfg.temperature).toBe(0.2);
expect(cfg.baseUrl).toBe('https://api.deepseek.com'); // 保留默认值
});
it('1.4 无 repository 时 setTenantConfig 仅写内存 Map 兜底', async () => {
expect((service as any).deepSeekConfigEntity).toBeUndefined();
await service.setTenantConfig(42, {
apiKey: 'sk-map-only-fallback-key',
model: 'deepseek-reasoner',
baseUrl: 'https://api.deepseek.com',
});
expect(service.isConfigured(42)).toBe(true);
expect(service.getTenantConfig(42).apiKey).toBe('sk-map-only-fallback-key');
expect(service.getTenantConfig(42).model).toBe('deepseek-reasoner');
// 不污染平台公共池
expect(service.isConfigured()).toBe(false);
});
it('1.5 有 mock repository 时 setTenantConfig 落库且 resolve 优先读库', async () => {
const store: AiDeepSeekConfigEntity[] = [];
const mockRepo = {
find: jest.fn(async () => [...store]),
findOne: jest.fn(async ({ where }: { where: { tenantId: number } }) => {
return store.find((r) => r.tenantId === where.tenantId) || null;
}),
save: jest.fn(async (row: AiDeepSeekConfigEntity) => {
const idx = store.findIndex((r) => r.tenantId === row.tenantId);
if (idx >= 0) {
store[idx] = row;
} else {
store.push(row);
}
return row;
}),
};
(service as any).deepSeekConfigEntity = mockRepo;
await service.setTenantConfig(1001, {
apiKey: 'sk-persisted-tenant-key',
model: 'deepseek-chat',
baseUrl: 'https://api.deepseek.com',
});
expect(mockRepo.save).toHaveBeenCalled();
expect(store).toHaveLength(1);
expect(store[0].tenantId).toBe(1001);
expect(store[0].apiKey).toBe('sk-persisted-tenant-key');
expect(store[0].enabled).toBe(true);
// 清空内存 Map,验证 resolveTenantConfig 从 DB 读回
(service as any).tenantConfigs.clear();
const fromDb = await service.resolveTenantConfig(1001);
expect(fromDb.apiKey).toBe('sk-persisted-tenant-key');
expect(service.getTenantConfig(1001).apiKey).toBe('sk-persisted-tenant-key');
});
it('1.6 DB 抛错时静默回退内存 Map,不中断业务', async () => {
const mockRepo = {
find: jest.fn(async () => {
throw new Error('db down');
}),
findOne: jest.fn(async () => {
throw new Error('db down');
}),
save: jest.fn(async () => {
throw new Error('db down');
}),
};
(service as any).deepSeekConfigEntity = mockRepo;
await expect(
service.setTenantConfig(9, { apiKey: 'sk-survive-db-outage' })
).resolves.toMatchObject({ apiKey: 'sk-survive-db-outage' });
expect(service.getTenantConfig(9).apiKey).toBe('sk-survive-db-outage');
});
});
describe('2. 提示词动态拼装 (Prompt Builders)', () => {
@@ -230,4 +305,73 @@ describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
expect(res.message).toContain('Authentication Fails');
});
});
describe('5. Token 用量仅进程内存,不得冒充账单/落库 (R29)', () => {
/** 内存用量标记:storage:'memory' 或 persisted:false 均可 */
function assertMemoryOnlyUsage(result: {
storage?: string;
persisted?: boolean;
totalTokens: number;
promptTokens: number;
completionTokens: number;
callCount: number;
}) {
const markedMemory =
result.storage === 'memory' || result.persisted === false;
expect(markedMemory).toBe(true);
// 不得伪装成已落库账单用量
expect(result.storage).not.toBe('db');
expect(result.storage).not.toBe('persisted');
expect(result.persisted).not.toBe(true);
}
it('5.1 无任何记录时 getTenantTokenUsage 返回全 0,并标明内存存储', () => {
const usage = service.getTenantTokenUsage(9001);
expect(usage.totalTokens).toBe(0);
expect(usage.promptTokens).toBe(0);
expect(usage.completionTokens).toBe(0);
expect(usage.callCount).toBe(0);
assertMemoryOnlyUsage(usage);
// 禁止造示例/占位用量
expect(usage.totalTokens).not.toBeGreaterThan(0);
});
it('5.2 recordTokenUsage 返回值须带 storage:memory(或 persisted:false)', () => {
const recorded = service.recordTokenUsage(9002, {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
});
expect(recorded.totalTokens).toBe(15);
expect(recorded.promptTokens).toBe(10);
expect(recorded.completionTokens).toBe(5);
expect(recorded.callCount).toBe(1);
assertMemoryOnlyUsage(recorded);
});
it('5.3 累计后 getTenantTokenUsage 仍标明内存,数值仅反映进程内账本', () => {
service.recordTokenUsage(9003, { totalTokens: 100 });
service.recordTokenUsage(9003, {
promptTokens: 20,
completionTokens: 30,
totalTokens: 50,
});
const usage = service.getTenantTokenUsage(9003);
expect(usage.totalTokens).toBe(150);
expect(usage.promptTokens).toBe(20);
expect(usage.completionTokens).toBe(30);
expect(usage.callCount).toBe(2);
assertMemoryOnlyUsage(usage);
});
it('5.4 不同租户用量隔离;未写入租户仍为 0 且标明内存', () => {
service.recordTokenUsage(9004, { totalTokens: 77 });
const other = service.getTenantTokenUsage(9005);
expect(other.totalTokens).toBe(0);
expect(other.callCount).toBe(0);
assertMemoryOnlyUsage(other);
assertMemoryOnlyUsage(service.getTenantTokenUsage(9004));
});
});
});