Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export async function createServices(config: ReturnType<typeof loadConfig>) {
const feishuAppId = feishuIntegration?.appId ?? process.env['FEISHU_APP_ID'];
const feishuAppSecret = feishuIntegration?.appSecret ?? process.env['FEISHU_APP_SECRET'];
if (feishuAppId && feishuAppSecret) {
const presets = feishuIntegration?.mcp?.presets ?? ['preset.default'];
const presets = feishuIntegration?.mcp?.presets ?? ['preset.default', 'preset.calendar.default', 'preset.task.default', 'preset.im.default', 'preset.base.batch'];
let larkMcpBin: string;
try {
const esmRequire = createRequire(import.meta.url);
Expand Down
39 changes: 36 additions & 3 deletions packages/org-manager/src/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ export class APIServer {
const teams = this.orgService.listTeams(orgId);
const humans = this.orgService.listHumanUsers(orgId);
const todayToolCalls = this.getToolCallsTodayFromAgents();
info.usage = { teams: teams.length, toolCallsToday: todayToolCalls, users: humans.length };
const agentCount = this.orgService.getAgentManager().listAgents().length;
info.usage = { agents: agentCount, teams: teams.length, toolCallsToday: todayToolCalls, users: humans.length };
} catch { /* non-critical */ }
return info;
}
Expand Down Expand Up @@ -1655,11 +1656,15 @@ export class APIServer {
const secretary = agentManager.getAgent(secretaryInfo.id);
const senderName = payload['senderName'] as string ?? senderId ?? 'feishu_user';

// Track active conversation so approvals are routed to this chat
this.feishuNotifier?.setActiveConversationChat(chatId);

// Streaming state for real-time card updates
const toolCalls: Array<{ name: string; status: 'running' | 'done' | 'error'; durationMs?: number }> = [];
let lastCardUpdatePhase: AgentCardPhase = 'thinking';
let cardUpdatePending = false;
let cardUpdateTimer: ReturnType<typeof setTimeout> | null = null;
let streamingText = '';

// Throttled card update — avoid excessive API calls (max once per 2s)
const CARD_UPDATE_INTERVAL_MS = 2000;
Expand All @@ -1674,6 +1679,7 @@ export class APIServer {
agentName,
phase: lastCardUpdatePhase,
toolCalls: toolCalls.length > 0 ? [...toolCalls] : undefined,
content: streamingText || undefined,
});
await this.feishuNotifier!.updateCard(statusCardId!, card);
} catch (e) {
Expand All @@ -1683,7 +1689,7 @@ export class APIServer {
};

// Stream event handler — updates card state in real-time
const handleStreamEvent = (event: { type: string; tool?: string; phase?: string; success?: boolean; durationMs?: number; agentEvent?: string }) => {
const handleStreamEvent = (event: { type: string; tool?: string; phase?: string; success?: boolean; durationMs?: number; agentEvent?: string; text?: string }) => {
if (event.type === 'agent_tool') {
if (event.phase === 'start' && event.tool) {
toolCalls.push({ name: event.tool, status: 'running' });
Expand All @@ -1697,6 +1703,10 @@ export class APIServer {
}
scheduleCardUpdate();
}
} else if (event.type === 'text_delta' && event.text) {
streamingText += event.text;
lastCardUpdatePhase = 'tool_calling';
scheduleCardUpdate();
}
};

Expand All @@ -1714,6 +1724,27 @@ export class APIServer {

const elapsedMs = Date.now() - startTime;

// Handle merged messages — user sent while agent was already processing
if (reply === '[merged]' || reply === '[Stream cancelled]') {
log.info('Feishu message was merged into active processing', { chatId });
if (messageId && this.feishuNotifier) {
if (processingReactionId) {
await this.feishuNotifier.deleteReaction(messageId, processingReactionId);
}
await this.feishuNotifier.addReaction(messageId, 'OnIt');
}
if (statusCardId && this.feishuNotifier) {
const mergedCard = buildAgentResponseCard({
agentName,
phase: 'done',
content: '已收到,已合并到当前正在处理的对话中。',
elapsedMs,
});
await this.feishuNotifier.updateCard(statusCardId, mergedCard).catch(() => {});
}
return;
}

// Strip thinking/reasoning blocks — only show the clean response to user
const { clean: cleanReply } = extractThinkBlocks(reply ?? '');
const displayContent = stripInternalBlocks(cleanReply);
Expand Down Expand Up @@ -1788,6 +1819,8 @@ export class APIServer {
} else {
await this.feishuNotifier?.sendTextToChat(chatId, `处理消息时出错: ${errMsg}`);
}
} finally {
this.feishuNotifier?.setActiveConversationChat(null);
}
}

Expand Down Expand Up @@ -7678,7 +7711,7 @@ EXPLANATION_END`;
if (path === '/api/license' && req.method === 'GET') {
const raw = this.licenseService
? this.licenseService.getInfo()
: { plan: 'free', features: [], limits: { maxTeams: 5, maxToolCallsPerDay: 5000, maxUsers: 1 } };
: { plan: 'free', features: [], limits: { maxAgents: 20, maxTeams: 5, maxToolCallsPerDay: 5000, maxUsers: 1 } };
this.json(res, 200, await this.buildLicenseResponse(raw, req));
return;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/org-manager/src/billing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface OrgPlan {

const DEFAULT_PLANS: Record<PlanTier, OrgPlan['limits']> = {
free: {
maxAgents: -1,
maxAgents: 20,
maxTokensPerMonth: -1,
maxToolCallsPerDay: 5000,
maxMessagesPerDay: -1,
Expand Down
14 changes: 13 additions & 1 deletion packages/org-manager/src/feishu-notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export class FeishuNotifier {
private wsConnected = false;
/** Map messageId → approvalId for tracking replies as comments */
private approvalMessageMap = new Map<string, string>();
/** Active Feishu chat being served — approvals are sent here too */
private activeConversationChatId: string | null = null;

constructor(opts: {
eventBus: EventBus;
Expand Down Expand Up @@ -883,9 +885,14 @@ export class FeishuNotifier {
matchedTargets.push(...rule.targets);
}

// For approvals during active conversation, send to the user's chat directly
const isApprovalType = ['approval_requested', 'approval_approved', 'approval_rejected'].includes(eventType);
if (isApprovalType && this.activeConversationChatId) {
matchedTargets.push({ type: 'chat', channelId: this.activeConversationChatId });
}

// Fallback: use notifyChatId or notifyOpenId from simplified config
if (matchedTargets.length === 0 && (this.config.notifyChatId || this.config.notifyOpenId)) {
const isApprovalType = ['approval_requested', 'approval_approved', 'approval_rejected'].includes(eventType);
const shouldForward = isApprovalType
? this.config.notifyOnApproval !== false
: this.config.notifyOnNotification === true;
Expand Down Expand Up @@ -949,6 +956,11 @@ export class FeishuNotifier {
}
}

/** Set the active Feishu conversation chatId (approvals will also be sent here). */
setActiveConversationChat(chatId: string | null): void {
this.activeConversationChatId = chatId;
}

/** Send a text message to a specific Feishu chat. */
async sendTextToChat(chatId: string, text: string): Promise<void> {
if (!this.apiClient) return;
Expand Down
2 changes: 1 addition & 1 deletion packages/org-manager/src/org-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export class OrganizationService {
name,
ownerId,
plan: 'free',
maxAgents: -1,
maxAgents: 20,
createdAt: new Date().toISOString(),
};

Expand Down
2 changes: 1 addition & 1 deletion packages/org-manager/test/license-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function defaultLicenseJson() {
plan: 'free',
features: [],
limits: {
maxAgents: -1,
maxAgents: 20,
maxTokensPerMonth: -1,
maxToolCallsPerDay: 5000,
maxMessagesPerDay: -1,
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/types/license.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
export type PlanTier = 'free' | 'enterprise';

export interface PlanLimits {
maxAgents: number;
maxTeams: number;
maxToolCallsPerDay: number;
maxUsers: number;
}

export const PLAN_LIMITS: Record<PlanTier, PlanLimits> = {
free: {
maxAgents: 20,
maxTeams: 5,
maxToolCallsPerDay: 5000,
maxUsers: 1,
},
enterprise: {
maxAgents: -1,
maxTeams: -1,
maxToolCallsPerDay: -1,
maxUsers: -1,
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/test/shared-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ describe('CognitiveDepth', () => {

describe('PLAN_LIMITS', () => {
it('defines free tier limits', () => {
expect(PLAN_LIMITS.free.maxAgents).toBe(20);
expect(PLAN_LIMITS.free.maxTeams).toBe(5);
expect(PLAN_LIMITS.free.maxToolCallsPerDay).toBe(5000);
expect(PLAN_LIMITS.free.maxUsers).toBe(1);
});

it('defines enterprise tier as unlimited', () => {
expect(PLAN_LIMITS.enterprise.maxAgents).toBe(-1);
expect(PLAN_LIMITS.enterprise.maxTeams).toBe(-1);
expect(PLAN_LIMITS.enterprise.maxToolCallsPerDay).toBe(-1);
expect(PLAN_LIMITS.enterprise.maxUsers).toBe(-1);
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/src/sqlite-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ export class SqliteOrgRepo {
`INSERT OR IGNORE INTO organizations (id, name, owner_id, plan, max_agents, settings, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, '{}', ?, ?)`
)
.run(data.id, data.name, data.ownerId, data.plan ?? 'free', data.maxAgents ?? 5, ts, ts);
.run(data.id, data.name, data.ownerId, data.plan ?? 'free', data.maxAgents ?? 20, ts, ts);
return this.findOrgById(data.id)!;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/web-ui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1590,8 +1590,8 @@ export const api = {
request<{ avatarUrl: string }>('/avatars/upload', { method: 'POST', body: JSON.stringify({ image, type, id }) }),
},
license: {
get: () => request<{ plan: string; licenseKey?: string; validUntil?: string; isTrial?: boolean; isOffline?: boolean; features: string[]; limits: { maxTeams: number; maxToolCallsPerDay: number; maxUsers: number }; usage?: { teams: number; toolCallsToday: number; users: number }; instanceId: string; hubUserId?: string; username?: string; orgId?: string; orgName?: string; maxSeats?: number; usedSeats?: number }>('/license'),
refresh: () => request<{ plan: string; licenseKey?: string; validUntil?: string; isTrial?: boolean; isOffline?: boolean; features: string[]; limits: { maxTeams: number; maxToolCallsPerDay: number; maxUsers: number }; usage?: { teams: number; toolCallsToday: number; users: number }; instanceId: string; hubUserId?: string; username?: string; orgId?: string; orgName?: string; maxSeats?: number; usedSeats?: number }>('/license/refresh', { method: 'POST' }),
get: () => request<{ plan: string; licenseKey?: string; validUntil?: string; isTrial?: boolean; isOffline?: boolean; features: string[]; limits: { maxAgents: number; maxTeams: number; maxToolCallsPerDay: number; maxUsers: number }; usage?: { agents: number; teams: number; toolCallsToday: number; users: number }; instanceId: string; hubUserId?: string; username?: string; orgId?: string; orgName?: string; maxSeats?: number; usedSeats?: number }>('/license'),
refresh: () => request<{ plan: string; licenseKey?: string; validUntil?: string; isTrial?: boolean; isOffline?: boolean; features: string[]; limits: { maxAgents: number; maxTeams: number; maxToolCallsPerDay: number; maxUsers: number }; usage?: { agents: number; teams: number; toolCallsToday: number; users: number }; instanceId: string; hubUserId?: string; username?: string; orgId?: string; orgName?: string; maxSeats?: number; usedSeats?: number }>('/license/refresh', { method: 'POST' }),
activate: (licenseKey: string) =>
request<{ success: boolean; error?: string }>('/license/activate', { method: 'POST', body: JSON.stringify({ licenseKey }) }),
trial: () =>
Expand Down
1 change: 1 addition & 0 deletions packages/web-ui/src/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@
"offline": "Offline",
"details": "License Details",
"manage": "License Management",
"limitAgents": "AI Agents",
"limitTeams": "AI Agent Teams",
"limitToolCalls": "AI Tool Calls",
"limitUsers": "Human Users",
Expand Down
1 change: 1 addition & 0 deletions packages/web-ui/src/locales/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@
"offline": "离线",
"details": "许可证详情",
"manage": "许可证管理",
"limitAgents": "智能体数",
"limitTeams": "AI Agent 团队数",
"limitToolCalls": "AI Tool 调用次数",
"limitUsers": "人类员工数",
Expand Down
10 changes: 6 additions & 4 deletions packages/web-ui/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3395,8 +3395,8 @@ function OrgLicenseSection() {
// ── License state ──
const [licenseInfo, setLicenseInfo] = useState<{
plan: string; licenseKey?: string; validUntil?: string; isTrial?: boolean; isOffline?: boolean;
features: string[]; limits: { maxTeams: number; maxToolCallsPerDay: number; maxUsers: number };
usage?: { teams: number; toolCallsToday: number; users: number };
features: string[]; limits: { maxAgents: number; maxTeams: number; maxToolCallsPerDay: number; maxUsers: number };
usage?: { agents: number; teams: number; toolCallsToday: number; users: number };
instanceId: string; hubUserId?: string; username?: string;
orgId?: string; orgName?: string; maxSeats?: number; usedSeats?: number;
} | null>(null);
Expand Down Expand Up @@ -3973,7 +3973,8 @@ function LicensePlanCard({ isEnterprise, licenseInfo, daysRemaining, effectiveVa
</div>
)}
</div>
<div className="mt-4 grid grid-cols-3 gap-px rounded-lg overflow-hidden border border-border-default/50">
<div className="mt-4 grid grid-cols-4 gap-px rounded-lg overflow-hidden border border-border-default/50">
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-sm font-semibold text-fg-primary tabular-nums">{licenseInfo?.usage?.agents ?? 0} <span className="text-fg-tertiary font-normal">/ ∞</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitAgents')}</div></div>
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-sm font-semibold text-fg-primary tabular-nums">{licenseInfo?.usage?.teams ?? 0} <span className="text-fg-tertiary font-normal">/ ∞</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitTeams')}</div></div>
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-sm font-semibold text-fg-primary tabular-nums">{licenseInfo?.usage?.toolCallsToday ?? 0} <span className="text-fg-tertiary font-normal">/ ∞</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitToolCalls')}{t('license.perDay')}</div></div>
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-sm font-semibold text-fg-primary tabular-nums">{displayUsers} <span className="text-fg-tertiary font-normal">/ {effectiveMaxSeats ?? '∞'}</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitUsers')}</div></div>
Expand Down Expand Up @@ -4001,7 +4002,8 @@ function LicensePlanCard({ isEnterprise, licenseInfo, daysRemaining, effectiveVa
<div className="rounded-lg border border-border-default">
<div className="px-5 py-4">
<div className="flex items-center gap-2.5"><span className="text-base font-semibold text-fg-primary">{t('license.planFree')}</span><span className="text-[11px] text-fg-tertiary">{t('license.freeFeaturesDesc')}</span></div>
<div className="mt-3 grid grid-cols-3 gap-px rounded-lg overflow-hidden border border-border-default/50">
<div className="mt-3 grid grid-cols-4 gap-px rounded-lg overflow-hidden border border-border-default/50">
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-lg font-semibold text-fg-primary tabular-nums">{licenseInfo?.usage?.agents ?? 0} <span className="text-fg-tertiary font-normal text-xs">/ {licenseInfo?.limits?.maxAgents ?? 20}</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitAgents')}</div></div>
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-lg font-semibold text-fg-primary tabular-nums">{licenseInfo?.usage?.teams ?? 0} <span className="text-fg-tertiary font-normal text-xs">/ {licenseInfo?.limits?.maxTeams ?? 5}</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitTeams')}</div></div>
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-lg font-semibold text-fg-primary tabular-nums">{licenseInfo?.usage?.toolCallsToday ?? 0} <span className="text-fg-tertiary font-normal text-xs">/ {(licenseInfo?.limits?.maxToolCallsPerDay ?? 5000).toLocaleString()}</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitToolCalls')}{t('license.perDay')}</div></div>
<div className="bg-surface-primary/40 px-3 py-2.5 text-center"><div className="text-lg font-semibold text-fg-primary tabular-nums">{displayUsers} <span className="text-fg-tertiary font-normal text-xs">/ {licenseInfo?.limits?.maxUsers ?? 1}</span></div><div className="text-[10px] text-fg-tertiary mt-0.5">{t('license.limitUsers')}</div></div>
Expand Down
Loading