Skip to content

Commit 8c9ba17

Browse files
feat: 恢复 --channels 能力 (claude-code-best#297)
* feat: 恢复 --channels 能力 * docs: 添加 channels 注释
1 parent 8851c87 commit 8c9ba17

10 files changed

Lines changed: 319 additions & 479 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
2323
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
2424
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
25+
| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord 等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/channels) |
2526
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
2627
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
2728
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |

docs/features/channels.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Channels — 外部频道消息接入
2+
3+
> 启动参数:`--channels` / `--dangerously-load-development-channels`
4+
> 状态:已解除 feature flag 和 OAuth 限制,可直接使用
5+
6+
## 概述
7+
8+
Channel 是一个 MCP 服务器,它将外部事件推送到你运行中的 Claude Code 会话中,以便 Claude 可以在你不在终端时做出反应。详细使用说明请参考以下文档:
9+
10+
- **官方文档**[使用 channels 将事件推送到运行中的会话](https://code.claude.com/docs/zh-CN/channels)
11+
- **飞书插件**[claude-code-feishu-channel](https://github.com/whobot-ai/claude-code-feishu-channel) — 社区首个飞书 Channel 插件,支持双向消息、配对认证、群组聊天、文件附件
12+
13+
## 快速开始
14+
15+
```bash
16+
# 启用频道监听(plugin 格式)
17+
ccb --channels plugin:feishu@claude-code-feishu-channel
18+
19+
# 启用频道监听(server 格式)
20+
ccb --channels server:my-slack-bridge
21+
22+
# 同时启用多个频道
23+
ccb --channels plugin:feishu@claude-code-feishu-channel --channels server:discord-bot
24+
25+
# 开发模式(跳过 allowlist 检查,用于测试自定义 channel)
26+
ccb --dangerously-load-development-channels server:my-custom-channel
27+
```
28+
29+
## 支持的 Channel
30+
31+
| Channel | 说明 | 来源 |
32+
|---------|------|------|
33+
| **Telegram** | 官方 Telegram Bot 集成 | `/plugin install telegram@claude-plugins-official` |
34+
| **Discord** | 官方 Discord Bot 集成 | `/plugin install discord@claude-plugins-official` |
35+
| **iMessage** | macOS 原生消息 | `/plugin install imessage@claude-plugins-official` |
36+
| **飞书 (Feishu/Lark)** | 双向消息、群组聊天、文件附件 | `/plugin install feishu@claude-code-feishu-channel` |
37+
38+
## 相关文件
39+
40+
| 文件 | 职责 |
41+
|------|------|
42+
| `src/services/mcp/channelNotification.ts` | 频道 gate 逻辑、消息包装 |
43+
| `src/services/mcp/channelAllowlist.ts` | 频道开关(已默认开启) |
44+
| `src/services/mcp/useManageMCPConnections.ts` | MCP 连接管理中的频道注册 |
45+
| `src/components/LogoV2/ChannelsNotice.tsx` | 启动时频道状态提示 |
46+
| `src/main.tsx` | `--channels` 参数解析 |
47+
| `src/interactiveHelpers.tsx` | Dev channels 确认对话框 |
48+
49+
## 参考链接
50+
51+
- [官方 Channels 文档](https://code.claude.com/docs/zh-CN/channels) — 完整使用说明、安全性、Enterprise 控制
52+
- [飞书 Channel 插件](https://github.com/whobot-ai/claude-code-feishu-channel) — 安装配置教程、MCP 工具、Skill 命令参考

mint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
"group": "运行模式",
136136
"pages": [
137137
"docs/features/kairos",
138+
"docs/features/channels",
138139
"docs/features/voice-mode",
139140
"docs/features/bridge-mode",
140141
"docs/features/remote-control-self-hosting",

src/cli/print.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4948,7 +4948,7 @@ function handleChannelEnable(
49484948
function reregisterChannelHandlerAfterReconnect(
49494949
connection: MCPServerConnection,
49504950
): void {
4951-
if (!(feature('KAIROS') || feature('KAIROS_CHANNELS'))) return
4951+
// Channels always available — feature flag guard removed
49524952
if (connection.type !== 'connected') return
49534953

49544954
const gate = gateChannelServer(

src/components/LogoV2/ChannelsNotice.tsx

Lines changed: 3 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,27 @@ import {
1212
getHasDevChannels,
1313
} from '../../bootstrap/state.js'
1414
import { Box, Text } from '@anthropic/ink'
15-
import { isChannelsEnabled } from '../../services/mcp/channelAllowlist.js'
16-
import { getEffectiveChannelAllowlist } from '../../services/mcp/channelNotification.js'
1715
import { getMcpConfigsByScope } from '../../services/mcp/config.js'
18-
import {
19-
getClaudeAIOAuthTokens,
20-
getSubscriptionType,
21-
} from '../../utils/auth.js'
2216
import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js'
23-
import { getSettingsForSource } from '../../utils/settings/settings.js'
2417

2518
export function ChannelsNotice(): React.ReactNode {
2619
// Snapshot all reads at mount. This notice enters scrollback immediately
2720
// after the logo; any re-render past that point forces a full terminal
28-
// reset. getAllowedChannels (bootstrap state), getSettingsForSource
29-
// (session cache updated by background polling / /login), and
30-
// isChannelsEnabled (GrowthBook 5-min refresh) must be captured once
31-
// so a later re-render cannot flip branches.
32-
const [{ channels, disabled, noAuth, policyBlocked, list, unmatched }] =
21+
// reset.
22+
const [{ channels, list, unmatched }] =
3323
useState(() => {
3424
const ch = getAllowedChannels()
3525
if (ch.length === 0)
3626
return {
3727
channels: ch,
38-
disabled: false,
39-
noAuth: false,
40-
policyBlocked: false,
4128
list: '',
4229
unmatched: [] as Unmatched[],
4330
}
4431
const l = ch.map(formatEntry).join(', ')
45-
const sub = getSubscriptionType()
46-
const managed = sub === 'team' || sub === 'enterprise'
47-
const policy = getSettingsForSource('policySettings')
48-
const allowlist = getEffectiveChannelAllowlist(
49-
sub,
50-
policy?.allowedChannelPlugins,
51-
)
5232
return {
5333
channels: ch,
54-
disabled: !isChannelsEnabled(),
55-
noAuth: !getClaudeAIOAuthTokens()?.accessToken,
56-
policyBlocked: managed && policy?.channelsEnabled !== true,
5734
list: l,
58-
unmatched: findUnmatched(ch, allowlist),
35+
unmatched: findUnmatched(ch),
5936
}
6037
})
6138
if (channels.length === 0) return null
@@ -70,50 +47,6 @@ export function ChannelsNotice(): React.ReactNode {
7047
? '--dangerously-load-development-channels'
7148
: '--channels'
7249

73-
if (disabled) {
74-
return (
75-
<Box paddingLeft={2} flexDirection="column">
76-
<Text color="error">
77-
{flag} ignored ({list})
78-
</Text>
79-
<Text dimColor>Channels are not currently available</Text>
80-
</Box>
81-
)
82-
}
83-
84-
if (noAuth) {
85-
return (
86-
<Box paddingLeft={2} flexDirection="column">
87-
<Text color="error">
88-
{flag} ignored ({list})
89-
</Text>
90-
<Text dimColor>
91-
Channels require claude.ai authentication · run /login, then restart
92-
</Text>
93-
</Box>
94-
)
95-
}
96-
97-
if (policyBlocked) {
98-
return (
99-
<Box paddingLeft={2} flexDirection="column">
100-
<Text color="error">
101-
{flag} blocked by org policy ({list})
102-
</Text>
103-
<Text dimColor>Inbound messages will be silently dropped</Text>
104-
<Text dimColor>
105-
Have an administrator set channelsEnabled: true in managed settings to
106-
enable
107-
</Text>
108-
{unmatched.map(u => (
109-
<Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">
110-
{formatEntry(u.entry)} · {u.why}
111-
</Text>
112-
))}
113-
</Box>
114-
)
115-
}
116-
11750
// "Listening for" not "active" — at this point we only know the allowlist
11851
// was set. Server connection, capability declaration, and whether the name
11952
// even matches a configured MCP server are all still unknown.
@@ -144,7 +77,6 @@ type Unmatched = { entry: ChannelEntry; why: string }
14477

14578
function findUnmatched(
14679
entries: readonly ChannelEntry[],
147-
allowlist: ReturnType<typeof getEffectiveChannelAllowlist>,
14880
): Unmatched[] {
14981
// Server-kind: build one Set from all scopes up front. getMcpConfigsByScope
15082
// is not cached (project scope walks the dir tree); getMcpConfigByName would
@@ -163,46 +95,17 @@ function findUnmatched(
16395
Object.keys(loadInstalledPluginsV2().plugins),
16496
)
16597

166-
// Plugin-kind allowlist check: same {marketplace, plugin} test as the
167-
// gate at channelNotification.ts. entry.dev bypasses (dev flag opts out
168-
// of the allowlist). Org list replaces ledger when set (team/enterprise).
169-
// GrowthBook _CACHED_MAY_BE_STALE — cold cache yields [] so every plugin
170-
// entry warns; same tradeoff the gate already accepts.
171-
const { entries: allowed, source } = allowlist
172-
173-
// Independent ifs — a plugin entry that's both uninstalled AND
174-
// unlisted shows two lines. Server kind checks config + dev flag.
17598
const out: Unmatched[] = []
17699
for (const entry of entries) {
177100
if (entry.kind === 'server') {
178101
if (!configured.has(entry.name)) {
179102
out.push({ entry, why: 'no MCP server configured with that name' })
180103
}
181-
if (!entry.dev) {
182-
out.push({
183-
entry,
184-
why: 'server: entries need --dangerously-load-development-channels',
185-
})
186-
}
187104
continue
188105
}
189106
if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) {
190107
out.push({ entry, why: 'plugin not installed' })
191108
}
192-
if (
193-
!entry.dev &&
194-
!allowed.some(
195-
e => e.plugin === entry.name && e.marketplace === entry.marketplace,
196-
)
197-
) {
198-
out.push({
199-
entry,
200-
why:
201-
source === 'org'
202-
? "not on your org's approved channels list"
203-
: 'not on the approved channels allowlist',
204-
})
205-
}
206109
}
207110
return out
208111
}

src/interactiveHelpers.tsx

Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -332,59 +332,25 @@ export async function showSetupScreens(
332332
// dev channels to any --channels list already set in main.tsx. Org policy
333333
// is NOT bypassed — gateChannelServer() still runs; this flag only exists
334334
// to sidestep the --channels approved-server allowlist.
335-
if (feature('KAIROS') || feature('KAIROS_CHANNELS')) {
336-
// gateChannelServer and ChannelsNotice read tengu_harbor after this
337-
// function returns. A cold disk cache (fresh install, or first run after
338-
// the flag was added server-side) defaults to false and silently drops
339-
// channel notifications for the whole session — gh#37026.
340-
// checkGate_CACHED_OR_BLOCKING returns immediately if disk already says
341-
// true; only blocks on a cold/stale-false cache (awaits the same memoized
342-
// initializeGrowthBook promise fired earlier). Also warms the
343-
// isChannelsEnabled() check in the dev-channels dialog below.
344-
if (getAllowedChannels().length > 0 || (devChannels?.length ?? 0) > 0) {
345-
await checkGate_CACHED_OR_BLOCKING('tengu_harbor')
346-
}
347-
348-
if (devChannels && devChannels.length > 0) {
349-
const [{ isChannelsEnabled }, { getClaudeAIOAuthTokens }] =
350-
await Promise.all([
351-
import('./services/mcp/channelAllowlist.js'),
352-
import('./utils/auth.js'),
353-
])
354-
// Skip the dialog when channels are blocked (tengu_harbor off or no
355-
// OAuth) — accepting then immediately seeing "not available" in
356-
// ChannelsNotice is worse than no dialog. Append entries anyway so
357-
// ChannelsNotice renders the blocked branch with the dev entries
358-
// named. dev:true here is for the flag label in ChannelsNotice
359-
// (hasNonDev check); the allowlist bypass it also grants is moot
360-
// since the gate blocks upstream.
361-
if (!isChannelsEnabled() || !getClaudeAIOAuthTokens()?.accessToken) {
362-
setAllowedChannels([
363-
...getAllowedChannels(),
364-
...devChannels.map(c => ({ ...c, dev: true })),
365-
])
366-
setHasDevChannels(true)
367-
} else {
368-
const { DevChannelsDialog } = await import(
369-
'./components/DevChannelsDialog.js'
370-
)
371-
await showSetupDialog(root, done => (
372-
<DevChannelsDialog
373-
channels={devChannels}
374-
onAccept={() => {
375-
// Mark dev entries per-entry so the allowlist bypass doesn't leak
376-
// to --channels entries when both flags are passed.
377-
setAllowedChannels([
378-
...getAllowedChannels(),
379-
...devChannels.map(c => ({ ...c, dev: true })),
380-
])
381-
setHasDevChannels(true)
382-
void done()
383-
}}
384-
/>
385-
))
386-
}
387-
}
335+
if (devChannels && devChannels.length > 0) {
336+
const { DevChannelsDialog } = await import(
337+
'./components/DevChannelsDialog.js'
338+
)
339+
await showSetupDialog(root, done => (
340+
<DevChannelsDialog
341+
channels={devChannels}
342+
onAccept={() => {
343+
// Mark dev entries per-entry so the allowlist bypass doesn't leak
344+
// to --channels entries when both flags are passed.
345+
setAllowedChannels([
346+
...getAllowedChannels(),
347+
...devChannels.map(c => ({ ...c, dev: true })),
348+
])
349+
setHasDevChannels(true)
350+
void done()
351+
}}
352+
/>
353+
))
388354
}
389355

390356
// Show Chrome onboarding for first-time Claude in Chrome users

0 commit comments

Comments
 (0)