refactor(hub): Vercel AI SDK → Claude Code SDK

用 @anthropic-ai/claude-agent-sdk 的 query() 替换 Vercel AI SDK 的
streamText。SDK 自带 Read/Write/Bash/Glob/Grep 工具——read_file/
write_file/list_files/cph_check/cph_build 全部不需要了,Bash 跑
cph 直接。

UX 同时改成 workbuddy 风格:
- 收到 @bot → 表情回复(👍)确认收到,不发'已开始处理'
- agent 回复作为文本流式发送(text-as-card patch,markdown 渲染)
- 超长自动续发新消息,不截断
- 完成/出错不再额外发状态消息
- 去掉卡片(model选择器/取消按钮)

ADR-0017 更新:放弃 provider-agnostic,Claude Code SDK 是 Anthropic
专有。换来:compaction、工具审批、partial message 流式、成熟 agent
loop。

tsc rc=0。旧文件(workspace.ts/cph.ts/provider.ts/tools.ts)暂留,
下个 commit 清理。
This commit is contained in:
2026-07-08 01:36:59 +08:00
parent 1abcc495f4
commit 4bd5b03bb1
7 changed files with 1422 additions and 348 deletions
+74 -32
View File
@@ -48,41 +48,83 @@ export interface MessageReceiveEvent {
};
}
/** Send a text message to a chat. */
export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise<void> {
// SDK exposes im.v1 dynamically at runtime; the generated types are weak
// there, so cast through the known shape.
const client = rt.client as unknown as {
im: { v1: { message: { create: (p: unknown) => Promise<unknown> } } };
};
await client.im.v1.message.create({
params: { receive_id_type: "chat_id" },
data: {
receive_id: chatId,
msg_type: "text",
content: JSON.stringify({ text }),
},
});
}
/** Send an interactive card to a chat. `card` is the lark card schema object. */
export async function sendCard(
rt: FeishuRuntime,
chatId: string,
card: unknown,
): Promise<string | null> {
/** Send a text-as-card message (looks like text to the user, but is patchable
* because Feishu only allows patching interactive/card messages, not text). */
export async function sendTextMessage(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
const card = { elements: [{ tag: "div", text: { tag: "lark_md", content: text } }] };
const client = rt.client as unknown as {
im: { v1: { message: { create: (p: unknown) => Promise<{ data?: { message_id?: string } }> } } };
};
const res = await client.im.v1.message.create({
params: { receive_id_type: "chat_id" },
data: {
receive_id: chatId,
msg_type: "interactive",
content: JSON.stringify(card),
try {
const res = await client.im.v1.message.create({
params: { receive_id_type: "chat_id" },
data: { receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) },
});
return res.data?.message_id ?? null;
} catch {
return null;
}
}
/** Send a text message (fire-and-forget, message_id not needed). */
export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise<void> {
await sendTextMessage(rt, chatId, text);
}
/** Patch a text-as-card message's content in-place (streaming updates). */
export async function patchTextMessage(rt: FeishuRuntime, messageId: string, text: string): Promise<void> {
const card = { elements: [{ tag: "div", text: { tag: "lark_md", content: text } }] };
const client = rt.client as unknown as {
im: { v1: { message: { patch: (p: unknown) => Promise<unknown> } } };
};
try {
await client.im.v1.message.patch({
path: { message_id: messageId },
params: { msg_type: "interactive" },
data: { content: JSON.stringify(card) },
});
} catch (e) {
rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "patchTextMessage failed");
}
}
/** React to a message with an emoji (Feishu "表情回复"). */
export async function reactToMessage(rt: FeishuRuntime, messageId: string, emoji: string): Promise<void> {
try {
await rt.client.request({
method: "POST",
url: `/open-apis/im/v1/messages/${messageId}/reactions`,
data: { reaction_type: { emoji_type: emoji } },
});
} catch (e) {
rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "reactToMessage failed");
}
}
/** Build a streaming card for an in-progress run. Shows accumulated text + tool status. */
export function buildStreamingCard(opts: {
readonly status: string;
readonly model: string;
readonly text: string;
readonly toolName: string | null;
}): unknown {
const elements: unknown[] = [
{ tag: "div", text: { tag: "lark_md", content: `**model:** ${opts.model}` } },
];
if (opts.toolName !== null) {
elements.push({ tag: "div", text: { tag: "lark_md", content: `🔧 *${opts.toolName}...*` } });
}
if (opts.text !== "") {
elements.push({ tag: "div", text: { tag: "lark_md", content: opts.text } });
}
return {
config: { wide_screen_mode: true },
header: {
title: { tag: "plain_text", content: `@Claude · ${opts.status}` },
template: "blue",
},
});
return res.data?.message_id ?? null;
elements,
};
}
/** Build a run-status card: status text + model selector + cancel button. */
@@ -104,7 +146,7 @@ export function buildRunStatusCard(opts: {
tag: "action",
actions: [
{
tag: "select",
tag: "select_static",
placeholder: { tag: "plain_text", content: "切换 model" },
options: opts.models.map((m) => ({ text: { tag: "plain_text", content: m.label }, value: m.id })),
value: opts.model,