feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户