forked from bai/curriculum-project-hub
feat(hub): 真实飞书读取 + 状态卡片 + 权限 gate + 部署脚本
FeishuRead(ADR-0003): feishuContextTool 从 stub 换成真实 lark im.v1.message.get(单条 trigger/reply/status_card)+ list(thread 回复); ADR-0003 chat 授权不变式不变(handler 已 gate)。 StatusCard: sendCard + buildRunStatusCard——完成/出错发 interactive card (status 文案 + model 选择器 + 取消按钮带 runId),替代纯文本。 Permission(ADR-0004, project-wise): triggerAgent 查 PermissionGrant 对 project 的 edit+ 能力(manage⊇edit 单调);sender open_id 当 principal (子类型学 OPEN,务实选择,permission.ts 标注);per-artifact OPEN; settings 组合规则 OPEN。无权限回 '无权限触发'。 Deploy: cph-hub.service systemd unit(ExecStartPre 跑 prisma migrate)+ install_service.sh(idempotent,seed env)+ deploy_platform.sh (rsync + npm ci + build + restart + healthz)。 client.ts 抽 createLarkClient/startFeishuListenerWithClient,tools 与 ws 共用同一 client。 tsc rc=0;prisma validate 通过;deploy 脚本 bash -n 通过。
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Feishu context reader — the real implementation behind `feishuContextTool`.
|
||||
*
|
||||
* ADR-0003: Hub stores only anchors, reads Feishu context on demand. The tool
|
||||
* (`feishu_read_context`) is already chat-authorized by `tools.ts` (chat must
|
||||
* match the project binding). Here we actually call lark's `im.v1.message`:
|
||||
*
|
||||
* - `trigger_message` / `reply`: `message.get` by message_id.
|
||||
* - `status_card`: the run's status card message — same `message.get` by id.
|
||||
* - `thread`: lark's thread replies. The SDK exposes `message.list` with a
|
||||
* `parent_message_id` filter; we map "thread" to that.
|
||||
*
|
||||
* The lark SDK's `im.v1.message` methods are dynamic at runtime (weak types);
|
||||
* we cast through a known request/response shape and return a compact JSON for
|
||||
* the model. All calls use the chat-authorized `client` from the runtime — no
|
||||
* separate credential path, so the ADR-0003 invariant holds end-to-end.
|
||||
*/
|
||||
import type { FeishuRuntime } from "./client.js";
|
||||
import type { FeishuContextArgs } from "../agent/tools.js";
|
||||
import type { ToolContext } from "../agent/tools.js";
|
||||
|
||||
/** Pragmatic shape of the lark message object we consume. */
|
||||
interface LarkMessage {
|
||||
message_id?: string;
|
||||
msg_type?: string;
|
||||
content?: string;
|
||||
create_time?: string;
|
||||
sender?: { id?: string; sender_type?: string };
|
||||
chat_id?: string;
|
||||
parent_message_id?: string;
|
||||
root_id?: string;
|
||||
}
|
||||
|
||||
interface ImV1Message {
|
||||
get: (p: { path: { message_id: string } }) => Promise<{ data?: { items?: LarkMessage[]; message?: LarkMessage } }>;
|
||||
list: (p: {
|
||||
params: { container_id_type: string; container_id: string; page_size?: number };
|
||||
}) => Promise<{ data?: { items?: LarkMessage[] } }>;
|
||||
}
|
||||
|
||||
function im(rt: FeishuRuntime): ImV1Message {
|
||||
return (rt.client as unknown as { im: { v1: { message: ImV1Message } } }).im.v1.message;
|
||||
}
|
||||
|
||||
/** Read on-demand Feishu context for the current run. Entry point for the tool. */
|
||||
export async function readFeishuContext(
|
||||
args: FeishuContextArgs,
|
||||
_ctx: ToolContext,
|
||||
rt: unknown,
|
||||
): Promise<string> {
|
||||
const runtime = rt as FeishuRuntime;
|
||||
const api = im(runtime);
|
||||
try {
|
||||
switch (args.anchor) {
|
||||
case "trigger_message":
|
||||
case "status_card":
|
||||
case "reply": {
|
||||
// All three are single-message reads by id.
|
||||
const res = await api.get({ path: { message_id: args.id } });
|
||||
const msg = res.data?.message ?? res.data?.items?.[0];
|
||||
if (msg === undefined) return JSON.stringify({ error: "message not found", id: args.id });
|
||||
return JSON.stringify(compact(msg));
|
||||
}
|
||||
case "thread": {
|
||||
// Thread = replies to a parent message. `container_id` is the parent's
|
||||
// message_id; container_id_type=message_id scopes the list to that thread.
|
||||
const res = await api.list({
|
||||
params: {
|
||||
container_id_type: "message_id",
|
||||
container_id: args.id,
|
||||
page_size: 50,
|
||||
},
|
||||
});
|
||||
const items = res.data?.items ?? [];
|
||||
return JSON.stringify(items.map(compact));
|
||||
}
|
||||
default:
|
||||
return JSON.stringify({ error: `unknown anchor type: ${args.anchor as string}` });
|
||||
}
|
||||
} catch (e) {
|
||||
return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
/** Project a lark message down to the fields the model actually needs. */
|
||||
function compact(m: LarkMessage): Record<string, unknown> {
|
||||
return {
|
||||
message_id: m.message_id,
|
||||
msg_type: m.msg_type,
|
||||
content: m.content,
|
||||
create_time: m.create_time,
|
||||
sender_id: m.sender?.id,
|
||||
parent_message_id: m.parent_message_id,
|
||||
root_id: m.root_id,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user