Skip to content

feat(wecom): add user feedback support for WeChat Work AI Bot#2078

Merged
RockChinQ merged 6 commits intolangbot-app:masterfrom
6mvp6:feat/wecom-feedback-support
Mar 30, 2026
Merged

feat(wecom): add user feedback support for WeChat Work AI Bot#2078
RockChinQ merged 6 commits intolangbot-app:masterfrom
6mvp6:feat/wecom-feedback-support

Conversation

@6mvp6
Copy link
Copy Markdown
Contributor

@6mvp6 6mvp6 commented Mar 24, 2026

功能描述

实现企业微信智能机器人用户反馈功能(点赞/点踩)支持,允许在 Web 监控页面查看和管理用户反馈数据。

修改内容

后端修改

  • WecomBotEvent 新增 feedback_idstream_id 字段
  • api.py 实现反馈事件处理逻辑,新增 on_feedback 装饰器
  • 新增 StreamSessionManager._feedback_index 用于 feedback_id 反向查找
  • 新增 MonitoringFeedback 数据库实体
  • 新增 dbm025_feedback_stats.py 数据库迁移脚本
  • 新增 FeedbackMonitor 辅助类用于记录反馈
  • 更新所有平台适配器支持 ap 参数
  • botmgr.py 新增 set_bot_info 方法传递机器人上下文信息

前端修改

  • 新增 FeedbackCard.tsx 反馈统计卡片组件
  • 新增 FeedbackList.tsx 反馈列表组件
  • 新增 useFeedbackData.ts 反馈数据获取 Hook
  • 监控页面新增"用户反馈"标签页
  • 新增国际化翻译(中文/英文)

其他修改

  • Dockerfile 新增阿里云镜像加速
  • docker-compose.yaml 网络配置优化
  • .gitignore 新增 Docker 数据目录和备份目录忽略规则

待完善功能

  1. 取消反馈处理 (feedback_type = 3)

    • 当前会记录但前端会错误显示为 dislike
    • 建议后续实现:收到 type=3 时更新或删除对应记录
  2. 重复反馈去重

    • 同一 feedback_id 多次操作会产生多条记录
    • 建议后续实现:使用 UPSERT 机制确保每个 feedback_id 只保留最新状态

测试

  • 企业微信机器人点赞/点踩功能正常
  • 前端监控页面反馈列表显示正常
  • 数据库迁移成功
  • 反馈统计数据计算正确

参考文档

Copilot AI review requested due to automatic review settings March 24, 2026 11:38
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. eh: Feature enhance: 新功能添加 / add new features IM: wecom 企业微信 适配器相关 / WeCom and WeComCS adapter related javascript Pull requests that update Javascript code m: Platform 机器人管理相关 / Bots management labels Mar 24, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end “user feedback (like/dislike)” support for the WeChat Work (WeCom) AI Bot, persisting feedback into the monitoring database and exposing it in the Web monitoring UI for viewing/exporting.

Changes:

  • Backend: capture WeCom feedback events, persist them as monitoring_feedback, and expose list/stats/export APIs.
  • Frontend: add a “User Feedback” monitoring tab with stats cards + feedback list, plus i18n strings.
  • Platform/runtime wiring: pass application context (ap) into adapters and provide bot/pipeline context to feedback recording.

Reviewed changes

Copilot reviewed 26 out of 28 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
web/src/i18n/locales/zh-Hans.ts Adds Simplified Chinese strings for the feedback tab/components.
web/src/i18n/locales/en-US.ts Adds English strings for the feedback tab/components.
web/src/app/home/monitoring/types/monitoring.ts Introduces FeedbackRecord/FeedbackStats types and optional feedback fields in MonitoringData.
web/src/app/home/monitoring/page.tsx Adds a “feedback” tab and wires it to the new feedback hook + UI components.
web/src/app/home/monitoring/hooks/useMonitoringData.ts Adjusts imports (currently includes unused additions).
web/src/app/home/monitoring/hooks/useFeedbackData.ts New hook to fetch feedback list + stats from backend monitoring endpoints.
web/src/app/home/monitoring/components/FeedbackList.tsx New UI component to render expandable feedback records and context fields.
web/src/app/home/monitoring/components/FeedbackCard.tsx New UI component for feedback stats cards (totals + satisfaction rate).
src/langbot/pkg/utils/constants.py Bumps required_database_version to 25 for the new migration.
src/langbot/pkg/platform/sources/wecombot.py Registers feedback handler to persist WeCom feedback into monitoring.
src/langbot/pkg/platform/sources/wecom.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/sources/wechatpad.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/sources/slack.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/sources/qqofficial.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/sources/officialaccount.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/sources/line.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/sources/dingtalk.py Updates adapter ctor signature to accept ap/**kwargs.
src/langbot/pkg/platform/botmgr.py Passes ap into adapters and sets bot/pipeline context via set_bot_info.
src/langbot/pkg/pipeline/monitoring_helper.py Adds FeedbackMonitor helper to call monitoring service feedback recorder.
src/langbot/pkg/persistence/migrations/dbm025_feedback_stats.py New migration creating monitoring_feedback table + indexes.
src/langbot/pkg/entity/persistence/monitoring.py Adds MonitoringFeedback SQLAlchemy model.
src/langbot/pkg/api/http/service/monitoring.py Adds feedback persistence + stats/list/export methods.
src/langbot/pkg/api/http/controller/groups/monitoring.py Adds feedback stats/list routes and extends export to support feedback.
src/langbot/libs/wecom_ai_bot_api/wecombotevent.py Adds feedback_id and stream_id properties on WecomBotEvent.
src/langbot/libs/wecom_ai_bot_api/api.py Adds feedback ID generation/indexing, feedback event handling, and on_feedback decorator.
docker/docker-compose.yaml Minor network section formatting adjustment.
Dockerfile Switches apt/pip indexes to CN mirrors and adjusts uv sync index usage.
.gitignore Ignores docker deployment data/override and backup artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +51
create_table_sql = '''
CREATE TABLE monitoring_feedback (
id VARCHAR(255) PRIMARY KEY,
timestamp DATETIME NOT NULL,
feedback_id VARCHAR(255) NOT NULL UNIQUE,
feedback_type INTEGER NOT NULL,
feedback_content TEXT,
inaccurate_reasons TEXT,
bot_id VARCHAR(255),
bot_name VARCHAR(255),
pipeline_id VARCHAR(255),
pipeline_name VARCHAR(255),
session_id VARCHAR(255),
message_id VARCHAR(255),
stream_id VARCHAR(255),
user_id VARCHAR(255),
platform VARCHAR(255)
)
'''
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration uses timestamp DATETIME NOT NULL in a single CREATE TABLE statement intended for both SQLite and PostgreSQL. PostgreSQL does not support the DATETIME type (it uses TIMESTAMP), so this migration is likely to fail on Postgres deployments. Please branch the DDL by self.ap.persistence_mgr.db.name (as other migrations do) and use the correct column types per DB.

Suggested change
create_table_sql = '''
CREATE TABLE monitoring_feedback (
id VARCHAR(255) PRIMARY KEY,
timestamp DATETIME NOT NULL,
feedback_id VARCHAR(255) NOT NULL UNIQUE,
feedback_type INTEGER NOT NULL,
feedback_content TEXT,
inaccurate_reasons TEXT,
bot_id VARCHAR(255),
bot_name VARCHAR(255),
pipeline_id VARCHAR(255),
pipeline_name VARCHAR(255),
session_id VARCHAR(255),
message_id VARCHAR(255),
stream_id VARCHAR(255),
user_id VARCHAR(255),
platform VARCHAR(255)
)
'''
if self.ap.persistence_mgr.db.name == 'postgresql':
create_table_sql = '''
CREATE TABLE monitoring_feedback (
id VARCHAR(255) PRIMARY KEY,
timestamp TIMESTAMP NOT NULL,
feedback_id VARCHAR(255) NOT NULL UNIQUE,
feedback_type INTEGER NOT NULL,
feedback_content TEXT,
inaccurate_reasons TEXT,
bot_id VARCHAR(255),
bot_name VARCHAR(255),
pipeline_id VARCHAR(255),
pipeline_name VARCHAR(255),
session_id VARCHAR(255),
message_id VARCHAR(255),
stream_id VARCHAR(255),
user_id VARCHAR(255),
platform VARCHAR(255)
)
'''
else:
create_table_sql = '''
CREATE TABLE monitoring_feedback (
id VARCHAR(255) PRIMARY KEY,
timestamp DATETIME NOT NULL,
feedback_id VARCHAR(255) NOT NULL UNIQUE,
feedback_type INTEGER NOT NULL,
feedback_content TEXT,
inaccurate_reasons TEXT,
bot_id VARCHAR(255),
bot_name VARCHAR(255),
pipeline_id VARCHAR(255),
pipeline_name VARCHAR(255),
session_id VARCHAR(255),
message_id VARCHAR(255),
stream_id VARCHAR(255),
user_id VARCHAR(255),
platform VARCHAR(255)
)
'''

Copilot uses AI. Check for mistakes.
Comment on lines +697 to +703
{loading && (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}

{!loading && (
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feedback tab uses the monitoring loading state to show/hide feedback UI. This means feedback can be hidden while monitoring data loads, and it won’t show a spinner when only feedback is loading. Use feedbackLoading (and/or both states) to control the feedback tab rendering.

Suggested change
{loading && (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
{!loading && (
{feedbackLoading && (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
{!feedbackLoading && (

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +126
@@ -10,8 +10,11 @@
import { ExportDropdown } from './components/ExportDropdown';
import { useMonitoringFilters } from './hooks/useMonitoringFilters';
import { useMonitoringData } from './hooks/useMonitoringData';
import { useFeedbackData } from './hooks/useFeedbackData';
import { MessageDetailsCard } from './components/MessageDetailsCard';
import { MessageContentRenderer } from './components/MessageContentRenderer';
import { FeedbackStatsCards } from './components/FeedbackCard';
import { FeedbackList } from './components/FeedbackList';
import { MessageDetails } from './types/monitoring';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
@@ -68,6 +71,60 @@
useMonitoringFilters();
const { data, loading, refetch } = useMonitoringData(filterState);

// Get time range for feedback data
const feedbackTimeRange = useMemo(() => {
const now = new Date();
let startTime: Date | null = null;

switch (filterState.timeRange) {
case 'lastHour':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
break;
case 'last6Hours':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
break;
case 'last24Hours':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case 'last7Days':
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'last30Days':
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case 'custom':
if (filterState.customDateRange) {
startTime = filterState.customDateRange.from;
}
break;
}

const endTime =
filterState.timeRange === 'custom' && filterState.customDateRange
? filterState.customDateRange.to
: now;

return {
startTime: startTime?.toISOString(),
endTime: endTime.toISOString(),
};
}, [filterState.timeRange, filterState.customDateRange]);

// Feedback data hook
const {
feedback: feedbackList,
stats: feedbackStats,
total: feedbackTotal,
loading: feedbackLoading,
refetch: refetchFeedback,
} = useFeedbackData({
botIds: filterState.selectedBots.length > 0 ? filterState.selectedBots : undefined,
pipelineIds: filterState.selectedPipelines.length > 0 ? filterState.selectedPipelines : undefined,
startTime: feedbackTimeRange.startTime,
endTime: feedbackTimeRange.endTime,
limit: 50,
});
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are unused React imports/vars that will trip next/typescript lint rules: useCallback is imported but not used, and useFeedbackData destructures total and refetch into feedbackTotal/refetchFeedback which are never used. Remove them or use them (e.g., show total / add a refresh action).

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +156
const transformedFeedback: FeedbackRecord[] = result.feedback.map((item) => ({
id: item.id,
timestamp: new Date(item.timestamp),
feedbackId: item.feedback_id,
feedbackType: item.feedback_type === 1 ? 'like' : 'dislike',
feedbackContent: item.feedback_content,
inaccurateReasons: item.inaccurate_reasons
? JSON.parse(item.inaccurate_reasons)
: undefined,
botId: item.bot_id,
botName: item.bot_name,
pipelineId: item.pipeline_id,
pipelineName: item.pipeline_name,
sessionId: item.session_id,
messageId: item.message_id,
streamId: item.stream_id,
userId: item.user_id,
platform: item.platform,
}));

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feedback_type values other than 1 are currently mapped to 'dislike'. For WeCom feedback events, type=3 (cancel) will be misrepresented as a dislike in the UI. Consider explicitly handling known values (1/2/3) and either filtering out cancels or mapping them to a third state.

Suggested change
const transformedFeedback: FeedbackRecord[] = result.feedback.map((item) => ({
id: item.id,
timestamp: new Date(item.timestamp),
feedbackId: item.feedback_id,
feedbackType: item.feedback_type === 1 ? 'like' : 'dislike',
feedbackContent: item.feedback_content,
inaccurateReasons: item.inaccurate_reasons
? JSON.parse(item.inaccurate_reasons)
: undefined,
botId: item.bot_id,
botName: item.bot_name,
pipelineId: item.pipeline_id,
pipelineName: item.pipeline_name,
sessionId: item.session_id,
messageId: item.message_id,
streamId: item.stream_id,
userId: item.user_id,
platform: item.platform,
}));
const transformedFeedback: FeedbackRecord[] = result.feedback.reduce<FeedbackRecord[]>((acc, item) => {
let feedbackType: FeedbackRecord['feedbackType'] | undefined;
if (item.feedback_type === 1) {
feedbackType = 'like';
} else if (item.feedback_type === 2) {
feedbackType = 'dislike';
} else {
// Ignore cancel (e.g. type=3) or any unknown feedback types
return acc;
}
acc.push({
id: item.id,
timestamp: new Date(item.timestamp),
feedbackId: item.feedback_id,
feedbackType,
feedbackContent: item.feedback_content,
inaccurateReasons: item.inaccurate_reasons
? JSON.parse(item.inaccurate_reasons)
: undefined,
botId: item.bot_id,
botName: item.bot_name,
pipelineId: item.pipeline_id,
pipelineName: item.pipeline_name,
sessionId: item.session_id,
messageId: item.message_id,
streamId: item.stream_id,
userId: item.user_id,
platform: item.platform,
});
return acc;
}, []);

Copilot uses AI. Check for mistakes.
Comment on lines +282 to +292
try:
pipeline_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline.name).where(
persistence_pipeline.LegacyPipeline.uuid == bot_entity.use_pipeline_uuid
)
)
pipeline_row = pipeline_result.first()
if pipeline_row:
pipeline_name = pipeline_row[0]
except Exception:
pass
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds one DB query per bot to resolve pipeline_name (SELECT LegacyPipeline.name ... WHERE uuid = ...). If there are many bots, load_bots_from_db() will become an N+1 query pattern. Consider preloading pipeline names in a single query (e.g., fetch all needed pipeline UUIDs into a map) or joining when loading bots.

Suggested change
try:
pipeline_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline.name).where(
persistence_pipeline.LegacyPipeline.uuid == bot_entity.use_pipeline_uuid
)
)
pipeline_row = pipeline_result.first()
if pipeline_row:
pipeline_name = pipeline_row[0]
except Exception:
pass
# Lazily initialize a simple in-memory cache for pipeline names
if not hasattr(self, '_pipeline_name_cache'):
self._pipeline_name_cache: dict[str, str] = {}
pipeline_uuid = bot_entity.use_pipeline_uuid
if pipeline_uuid in self._pipeline_name_cache:
pipeline_name = self._pipeline_name_cache[pipeline_uuid]
else:
try:
pipeline_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline.name).where(
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
)
)
pipeline_row = pipeline_result.first()
if pipeline_row:
pipeline_name = pipeline_row[0]
# Cache the result (including empty string) to avoid repeat queries
self._pipeline_name_cache[pipeline_uuid] = pipeline_name
except Exception:
# On error, cache empty string to avoid repeated failing queries
self._pipeline_name_cache[pipeline_uuid] = ''

Copilot uses AI. Check for mistakes.
end_time = parse_iso_datetime(end_time_str)

# Parse feedback type
feedback_type = int(feedback_type_str) if feedback_type_str else None
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feedbackType parsing uses int(feedback_type_str) directly. If the client passes a non-integer value (or an empty string), this will raise ValueError and return a 500. Please validate/guard the conversion and return a 400 for invalid feedbackType values.

Suggested change
feedback_type = int(feedback_type_str) if feedback_type_str else None
feedback_type = None
if feedback_type_str:
try:
feedback_type = int(feedback_type_str)
except ValueError:
quart.abort(400, description="Invalid feedbackType, must be an integer")

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 12
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
FilterState,
MonitoringData,
ModelCall,
LLMCall,
EmbeddingCall,
FeedbackRecord,
FeedbackStats,
} from '../types/monitoring';
import { backendClient } from '@/app/infra/http';
import { backendClient, httpClient } from '@/app/infra/http';

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FeedbackRecord, FeedbackStats, and httpClient are imported but never used in this hook. With the repo’s next/typescript ESLint config, unused imports are typically lint errors—please remove them or use them.

Copilot uses AI. Check for mistakes.

useEffect(() => {
refetch();
}, [paramsStr]);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useEffect calls refetch() but the dependency array is [paramsStr] only. With react-hooks/exhaustive-deps enabled (as in next/core-web-vitals), this will be flagged and can also lead to stale closures if refetch changes independently. Either include refetch in the deps (and ensure it’s stable), or add an explicit eslint disable comment with a justification like in useMonitoringData.

Suggested change
}, [paramsStr]);
}, [paramsStr, refetch]);

Copilot uses AI. Check for mistakes.
Comment on lines +236 to +242
)
self.listeners = {}
object.__setattr__(self, '_ws_mode', ws_mode)
object.__setattr__(self, 'ap', ap)

# Register feedback handler for monitoring
self._register_feedback_handler()
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ws_mode is referenced when setting _ws_mode, but it is not defined anywhere in __init__ (will raise NameError during adapter initialization). Define ws_mode (e.g., based on enable_webhook) or set _ws_mode from the already-computed _ws_mode = not self.config.get('enable-webhook', False) logic used elsewhere in this class.

Copilot uses AI. Check for mistakes.
Comment on lines 73 to +113
def __init__(self, logger: EventLogger, ttl: int = 60) -> None:
self.logger = logger

self.ttl = ttl # 超时时间(秒),超过该时间未被访问的会话会被清理由 cleanup
self._sessions: dict[str, StreamSession] = {} # stream_id -> StreamSession 映射
self._msg_index: dict[str, str] = {} # msgid -> stream_id 映射,便于流水线根据消息 ID 找到会话
self._feedback_index: dict[str, str] = {} # feedback_id -> stream_id 映射

def get_stream_id_by_msg(self, msg_id: str) -> Optional[str]:
if not msg_id:
return None
return self._msg_index.get(msg_id)

def get_session(self, stream_id: str) -> Optional[StreamSession]:
return self._sessions.get(stream_id)

def get_session_by_feedback_id(self, feedback_id: str) -> Optional[StreamSession]:
"""根据 feedback_id 查找会话。

Args:
feedback_id: 企业微信反馈事件中的反馈 ID。

Returns:
Optional[StreamSession]: 找到的会话实例,未找到返回 None。
"""
if not feedback_id:
return None
stream_id = self._feedback_index.get(feedback_id)
if stream_id:
return self._sessions.get(stream_id)
return None

def register_feedback_id(self, stream_id: str, feedback_id: str) -> None:
"""注册 feedback_id 与 stream_id 的映射。

Args:
stream_id: 企业微信流式会话 ID。
feedback_id: 反馈 ID。
"""
if feedback_id and stream_id:
self._feedback_index[feedback_id] = stream_id
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StreamSessionManager.cleanup() removes expired sessions from _sessions and _msg_index, but it never removes entries from the newly added _feedback_index. This can grow unbounded over time and will leave stale feedback_id -> stream_id mappings after the session is purged.

Copilot uses AI. Check for mistakes.
This commit implements user feedback functionality (like/dislike) for
WeChat Work AI Bot conversations, including:

Backend changes:
- Add feedback_id and stream_id fields to WecomBotEvent
- Implement feedback event handling in WecomBotClient (api.py)
- Add StreamSessionManager._feedback_index for feedback_id lookup
- Add on_feedback decorator for custom feedback handlers
- Create MonitoringFeedback entity for database persistence
- Add dbm025 migration for monitoring_feedback table
- Implement FeedbackMonitor helper class
- Update all platform adapters with ap parameter support
- Update botmgr to pass bot_info for monitoring context

Frontend changes:
- Add FeedbackCard and FeedbackList components
- Add useFeedbackData hook for feedback data fetching
- Add feedback tab to monitoring page
- Add feedback types and interfaces
- Add i18n translations (zh-Hans, en-US)

Other changes:
- Update Dockerfile with Chinese mirror for faster builds
- Update docker-compose.yaml with network configuration
- Update .gitignore for docker data and backup files

Note: Known issues that need future improvement:
- feedback_type=3 (cancel) is recorded but not properly handled
- Duplicate feedback records are not deduplicated
@RockChinQ RockChinQ force-pushed the feat/wecom-feedback-support branch from cc96d59 to 6bb7329 Compare March 29, 2026 16:09
@RockChinQ RockChinQ merged commit 6e37aae into langbot-app:master Mar 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

eh: Feature enhance: 新功能添加 / add new features IM: wecom 企业微信 适配器相关 / WeCom and WeComCS adapter related javascript Pull requests that update Javascript code m: Platform 机器人管理相关 / Bots management size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants