forked from EduCraft/curriculum-project-hub
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:
+74
-32
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user