feat(feishu): add Feishu OAuth trigger for voice sessions
- Add POST /sessions/:sessionId/feishu/oauth-trigger endpoint (mirrors DingTalk) which emits oauth_prompt WS event so Flutter opens the Feishu authorization page automatically instead of asking the user to enter a bind code - Update SystemPromptBuilder: voice sessions now use the Feishu OAuth trigger endpoint; text sessions still use the code-based flow as fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
75f20075f6
commit
e2057bfe68
|
|
@ -38,8 +38,8 @@ export class SystemPromptBuilder {
|
||||||
*/
|
*/
|
||||||
build(context: SystemPromptContext): string {
|
build(context: SystemPromptContext): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
// Build the DingTalk OAuth trigger instruction once, reused in the prompt below.
|
// Build channel OAuth trigger instructions.
|
||||||
// Voice sessions (sessionId present): use Bash/wget to call the internal endpoint —
|
// Voice sessions (sessionId present): use Bash/wget to call the internal session endpoint —
|
||||||
// works for BOTH claude_api and claude_agent_sdk engines.
|
// works for BOTH claude_api and claude_agent_sdk engines.
|
||||||
// Text sessions (no sessionId): use the initiate_dingtalk_binding tool (claude_api only).
|
// Text sessions (no sessionId): use the initiate_dingtalk_binding tool (claude_api only).
|
||||||
const dingtalkOauthStep = context.sessionId
|
const dingtalkOauthStep = context.sessionId
|
||||||
|
|
@ -56,6 +56,27 @@ export class SystemPromptBuilder {
|
||||||
` Say: "请查看屏幕上弹出的授权卡片,点击「立即授权」按钮,在钉钉里完成一键授权就好了。"\n` +
|
` Say: "请查看屏幕上弹出的授权卡片,点击「立即授权」按钮,在钉钉里完成一键授权就好了。"\n` +
|
||||||
` Do NOT read out the URL — the card handles it automatically.`;
|
` Do NOT read out the URL — the card handles it automatically.`;
|
||||||
|
|
||||||
|
const feishuOauthStep = context.sessionId
|
||||||
|
? ` *** Use ONLY feishu endpoints — /channels/feishu/... ***\n` +
|
||||||
|
` Step 3a — Trigger Feishu OAuth (Bash/wget):\n` +
|
||||||
|
` wget -q -O- --post-data='{"instanceId":"<instanceId>","instanceName":"<name>"}' \\\n` +
|
||||||
|
` --header="Content-Type: application/json" \\\n` +
|
||||||
|
` http://localhost:3002/api/v1/agent/sessions/${context.sessionId}/feishu/oauth-trigger\n` +
|
||||||
|
` This automatically pushes a Feishu OAuth authorization page to the user's screen.\n` +
|
||||||
|
` Say: "请在弹出的飞书授权页面点击「同意授权」,完成后绑定会自动生效。"\n` +
|
||||||
|
` Do NOT read out the URL — the page opens automatically.\n` +
|
||||||
|
` Step 3b — Poll to confirm (ONLY feishu endpoint, every 5s up to 3 min):\n` +
|
||||||
|
` wget -q -O- http://localhost:3002/api/v1/agent/channels/feishu/status/<instanceId>\n` +
|
||||||
|
` If { "bound": true }: "太好了!飞书绑定成功了!"`
|
||||||
|
: ` *** Use ONLY feishu endpoints — /channels/feishu/... ***\n` +
|
||||||
|
` Get bind code (ONLY feishu endpoint):\n` +
|
||||||
|
` wget -q -O- --post-data="" http://localhost:3002/api/v1/agent/channels/feishu/bind/<instanceId>\n` +
|
||||||
|
` Parse JSON: { "code": "<CODE>", "expiresAt": <ms> }\n` +
|
||||||
|
` Say: "飞书绑定验证码是「<CODE>」,有效15分钟。请打开飞书,找到 iAgent 机器人,把这个验证码发给它,就完成绑定啦。"\n` +
|
||||||
|
` Poll to confirm (ONLY feishu endpoint, every 5s up to 3 min):\n` +
|
||||||
|
` wget -q -O- http://localhost:3002/api/v1/agent/channels/feishu/status/<instanceId>\n` +
|
||||||
|
` If { "bound": true }: "太好了!飞书绑定成功了!"`;
|
||||||
|
|
||||||
// Base instruction
|
// Base instruction
|
||||||
parts.push(
|
parts.push(
|
||||||
'You are iAgent, an AI-powered server cluster operations assistant built on IT0. ' +
|
'You are iAgent, an AI-powered server cluster operations assistant built on IT0. ' +
|
||||||
|
|
@ -88,14 +109,7 @@ export class SystemPromptBuilder {
|
||||||
' If { "bound": true }: "钉钉绑定成功!"\n' +
|
' If { "bound": true }: "钉钉绑定成功!"\n' +
|
||||||
' If still false: "授权还没完成,请确认是否点击了钉钉里的授权按钮。"\n\n' +
|
' If still false: "授权还没完成,请确认是否点击了钉钉里的授权按钮。"\n\n' +
|
||||||
' IF user chose 飞书 (Feishu) OR both:\n' +
|
' IF user chose 飞书 (Feishu) OR both:\n' +
|
||||||
' *** Use ONLY feishu endpoints — /channels/feishu/... ***\n' +
|
`${feishuOauthStep}\n\n` +
|
||||||
' Get bind code (ONLY feishu endpoint):\n' +
|
|
||||||
' wget -q -O- --post-data="" http://localhost:3002/api/v1/agent/channels/feishu/bind/<instanceId>\n' +
|
|
||||||
' Parse JSON: { "code": "<CODE>", "expiresAt": <ms> }\n' +
|
|
||||||
' Say: "飞书绑定验证码是「<CODE>」,有效15分钟。请打开飞书,找到 iAgent 机器人,把这个验证码发给它,就完成绑定啦。"\n' +
|
|
||||||
' Poll to confirm (ONLY feishu endpoint, every 5s up to 3 min):\n' +
|
|
||||||
' wget -q -O- http://localhost:3002/api/v1/agent/channels/feishu/status/<instanceId>\n' +
|
|
||||||
' If { "bound": true }: "太好了!飞书绑定成功了!"\n\n' +
|
|
||||||
' IF user chose skip / neither: "好的,之后想绑定随时可以在 IT0 App 里操作。"\n\n' +
|
' IF user chose skip / neither: "好的,之后想绑定随时可以在 IT0 App 里操作。"\n\n' +
|
||||||
' CRITICAL: dingtalk endpoints (/channels/dingtalk/) and feishu endpoints (/channels/feishu/) are completely separate. Never call a dingtalk endpoint for feishu or vice versa.\n\n' +
|
' CRITICAL: dingtalk endpoints (/channels/dingtalk/) and feishu endpoints (/channels/feishu/) are completely separate. Never call a dingtalk endpoint for feishu or vice versa.\n\n' +
|
||||||
' Check/unbind:\n' +
|
' Check/unbind:\n' +
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { SessionRepository } from '../../../infrastructure/repositories/session.
|
||||||
import { SystemPromptBuilder } from '../../../infrastructure/engines/claude-code-cli/system-prompt-builder';
|
import { SystemPromptBuilder } from '../../../infrastructure/engines/claude-code-cli/system-prompt-builder';
|
||||||
import { AgentStreamGateway } from '../../ws/agent-stream.gateway';
|
import { AgentStreamGateway } from '../../ws/agent-stream.gateway';
|
||||||
import { DingTalkRouterService } from '../../../infrastructure/dingtalk/dingtalk-router.service';
|
import { DingTalkRouterService } from '../../../infrastructure/dingtalk/dingtalk-router.service';
|
||||||
|
import { FeishuRouterService } from '../../../infrastructure/feishu/feishu-router.service';
|
||||||
import { AgentInstanceRepository } from '../../../infrastructure/repositories/agent-instance.repository';
|
import { AgentInstanceRepository } from '../../../infrastructure/repositories/agent-instance.repository';
|
||||||
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
|
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
|
||||||
import { AgentSession } from '../../../domain/entities/agent-session.entity';
|
import { AgentSession } from '../../../domain/entities/agent-session.entity';
|
||||||
|
|
@ -50,6 +51,7 @@ export class VoiceSessionController {
|
||||||
private readonly systemPromptBuilder: SystemPromptBuilder,
|
private readonly systemPromptBuilder: SystemPromptBuilder,
|
||||||
private readonly gateway: AgentStreamGateway,
|
private readonly gateway: AgentStreamGateway,
|
||||||
private readonly dingTalkRouter: DingTalkRouterService,
|
private readonly dingTalkRouter: DingTalkRouterService,
|
||||||
|
private readonly feishuRouter: FeishuRouterService,
|
||||||
private readonly instanceRepo: AgentInstanceRepository,
|
private readonly instanceRepo: AgentInstanceRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
@ -260,4 +262,48 @@ export class VoiceSessionController {
|
||||||
|
|
||||||
return { triggered: true, instanceId: body.instanceId, instanceName };
|
return { triggered: true, instanceId: body.instanceId, instanceName };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal Feishu OAuth trigger — called by Claude via Bash/wget.
|
||||||
|
*
|
||||||
|
* Mirrors the DingTalk oauth-trigger. Generates a Feishu OAuth URL and
|
||||||
|
* emits it as an `oauth_prompt` WS event so the Flutter app opens the
|
||||||
|
* authorization page automatically.
|
||||||
|
*/
|
||||||
|
@Post(':sessionId/feishu/oauth-trigger')
|
||||||
|
async triggerFeishuOAuth(
|
||||||
|
@Param('sessionId') sessionId: string,
|
||||||
|
@Body() body: { instanceId: string; instanceName?: string },
|
||||||
|
) {
|
||||||
|
if (!body.instanceId) {
|
||||||
|
throw new BadRequestException('instanceId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.feishuRouter.isEnabled()) {
|
||||||
|
return { triggered: false, reason: 'Feishu integration not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const inst = await this.instanceRepo.findById(body.instanceId);
|
||||||
|
if (!inst) {
|
||||||
|
throw new NotFoundException(`Instance ${body.instanceId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceName = body.instanceName || inst.name || body.instanceId;
|
||||||
|
const { oauthUrl } = this.feishuRouter.generateOAuthUrl(body.instanceId);
|
||||||
|
|
||||||
|
this.oauthPendingSessions.set(sessionId, Date.now() + OAUTH_GRACE_MS);
|
||||||
|
|
||||||
|
this.gateway.emitStreamEvent(sessionId, {
|
||||||
|
type: 'oauth_prompt',
|
||||||
|
url: oauthUrl,
|
||||||
|
instanceId: body.instanceId,
|
||||||
|
instanceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[VoiceSession ${sessionId}] Feishu OAuth triggered for instance=${body.instanceId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { triggered: true, instanceId: body.instanceId, instanceName };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue