@lark-apaas/coding-steering 0.1.75 → 0.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-steering",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "description": "Stack-specific steering content for miaoda-coding templates",
5
5
  "type": "module",
6
6
  "files": [
@@ -84,7 +84,7 @@ available-agents:
84
84
  | 需要数据存储、用户隔离、后端逻辑 | 全栈 |
85
85
  | 涉及文件上传与管理 | 全栈 + 文件服务 |
86
86
  | 服务端图像、音视频处理(易引入 native addon) | 先确认能否改用 client 处理、平台服务或纯 JS/WASM |
87
- | 以飞书多维表格为数据源 | 插件优先;复杂查询场景评估是否同步至本地库 |
87
+ | 以飞书多维表格为数据源 | 插件优先;数据量大或复杂查询场景评估同步至应用数据库(`+db-sync-create`) |
88
88
  | 需要飞书通知、群组、AI 能力 | 插件集成,标记插件实例需求 |
89
89
  | 需要定时或事件驱动的后台处理 | 全栈 + 自动化任务 |
90
90
 
@@ -95,6 +95,7 @@ available-agents:
95
95
  | 应用自身创建和维护的数据 | 本地数据库 |
96
96
  | 飞书多维表格为既有事实源 | 通过多维表格插件读写 |
97
97
  | 多维表格上的统计看板 | 优先使用插件聚合能力,避免全量拉取后计算 |
98
+ | 需要把多维表格当运行数据源单向读,且数据量大(要分片/多次分页/多表)或查询复杂/需持续同步 | 规划为全栈 + Base → 应用数据库单向同步(`lark-cli apps +db-sync-create`,streaming 持续增量 / batch 只导一次),运行时不走插件全量读 |
98
99
  | 需要跨表关联、全文搜索、高频读写 | 规划同步至本地数据库后查询 |
99
100
  | 外部系统 API | 明确接口、鉴权、网络可达性与失败策略 |
100
101
 
@@ -168,7 +168,9 @@ const structured = await capabilityClient
168
168
 
169
169
  ## 多维表格数据架构
170
170
 
171
- 应用需读取飞书多维表格数据时,按查询模式选架构(完整对照表见 `references/table.md`「数据架构选择」)。**快捷判断**:「仪表盘/dashboard/驾驶舱/看板/统计」→ 必须 `aggregateQuery`(禁止 searchRecords 全量拉取后内存计算);「列表/明细/详情」→ `searchRecords` 分页;「排行榜/TOP N」→ `searchRecords` + sort + pageSize=N;跨表关联或复杂计算 → 本地数据库 + 同步。
171
+ 应用需读取飞书多维表格数据时,按查询模式选架构(完整对照表见 `references/table.md`「数据架构选择」)。**快捷判断**:「仪表盘/dashboard/驾驶舱/看板/统计」→ 必须 `aggregateQuery`(禁止 searchRecords 全量拉取后内存计算);「列表/明细/详情」→ `searchRecords` 分页;「排行榜/TOP N」→ `searchRecords` + sort + pageSize=N;需要把 Base 当运行数据源单向读、且数据量大到要分片/多次分页或查询复杂/需持续同步 → 先用 `lark-cli apps +db-sync-create` 把 Base 表同步成应用数据库表(streaming 持续增量 / batch 只导一次),页面查应用库;这是全栈应用的方案,配置格式和 `--preview`→`--yes` 流程见 lark-apps-db skill;跨表关联或复杂计算 → 应用数据库 + 同步。
172
+
173
+ **多维表格字段结构以插件为准**。lark-cli 读到的文本是字符串:链接和 @文档都写成 `[文字](链接)`,@人写成 `<at user_id=…>@名字</at>`;单选是数组。插件返回的是结构化字段,不能照 lark-cli 的输出定类型和渲染,各字段读写格式见 `references/table.md`「BizType 读写格式对照表」。Text 字段读取传 `textFormat: 'original'`,返回 `{ text, originalValue?: TextSegment[] }`,段 `type` 为 text / url / mention。渲染用 `BitableText`:有 `link` 就渲染成链接(url 段和 @文档的 mention 段都有);mention 段没有 `link` 的是 @人,按 `text` 显示、不可点;其余按 `text`。不要按 `type` 分支(mention 不等于 @人)。共享类型、AGENTS.md、派 Worker 的说明按此写。
172
174
 
173
175
  ## 外部服务插件规则(category=external-service 必读)
174
176
 
@@ -115,6 +115,21 @@ type TextSegment = {
115
115
  };
116
116
  ```
117
117
 
118
+ ### lark-cli 输出与插件分段的对照
119
+
120
+ lark-cli(`+record-list` / `+record-search`)读到的文本字段是字符串,链接和 @ 写成标记文本;插件返回的是上面的 `TextSegment` 数组。对应关系:
121
+
122
+ | lark-cli 读到的 | 插件 `originalValue` 里的段 | 页面展示 |
123
+ | --- | --- | --- |
124
+ | 普通文字(可能含 `\n`) | `{ type: 'text', text }` | 文字,保留换行 |
125
+ | `[文字](https://…)`,普通超链接 | `{ type: 'url', text, link }` | `<a>` 可点击 |
126
+ | `[文字](https://…)`,@文档 | `{ type: 'mention', mentionType: 'Docx' / 'Wiki' / 'Bitable' 等, text, link, token }` | `<a>` 可点击 |
127
+ | `<at user_id=ou_…>@名字</at>`,@人 | `{ type: 'mention', mentionType: 'User', text: '@名字', token: 'ou_…' }`,没有 `link` | 文字,不可点击 |
128
+
129
+ - lark-cli 里普通超链接和 @文档写法一样,插件里分别是 url 段和 mention 段。渲染只看 `link`:有就渲染成链接,没有就按 `text` 显示。不要按 `type` 或 `mentionType` 分支。
130
+ - @人的 `ou_…` 按开放平台应用隔离:同一个人在 lark-cli 和插件里可能是不同的 id,不要跨来源匹配。
131
+ - 其他字段也不一样(如单选在 lark-cli 是数组、插件是字符串)。字段结构以上面的「BizType 读写格式对照表」为准,不要从 lark-cli 的输出推。
132
+
118
133
  兼容说明:`originalValue` 是可选的新增字段,不传 `textFormat` 时返回内容与原来完全一致,只读 `.text` 的存量代码不用改也能正常运行。要链接可点击、保留换行时,再按下文迁移:读取加 `textFormat: 'original'`,展示从 `.text` 换成 `BitableText`。写入同理:Text 字段继续传字符串的存量代码行为不变,分段数组是新增的可选写法。
119
134
 
120
135
  ### 只读字段
@@ -161,6 +176,7 @@ type TextSegment = {
161
176
  ❌ searchRecords 拉全量 → 内存 reduce/filter 做统计 → 必须用 aggregateQuery
162
177
  ❌ searchRecords 拉全量 → sort → slice 取 TOP N → 必须用 searchRecords + sort + pageSize
163
178
  ❌ searchRecords 拉全量 → find 找单条 → 必须用 getRecord
179
+ ❌ 页面每次加载分片拉全量 Base → 浏览器汇总 → 大数据量只读应同步到应用数据库(见下方「数据架构选择」)
164
180
  ```
165
181
 
166
182
  ## 数据架构选择(应用读取多维表格数据时)
@@ -174,9 +190,17 @@ type TextSegment = {
174
190
  | 统计聚合(计数/求和/平均) | **纯插件 + aggregateQuery** | 禁止 searchRecords 全量拉取后内存计算 |
175
191
  | 统计 + 明细下钻 | 纯插件 | 聚合用 `aggregateQuery`,下钻用 `searchRecords` + filter |
176
192
  | 排行榜 / TOP N | 纯插件 | `searchRecords` + sort + pageSize=N |
177
- | 复杂排序/多表关联/全文搜索 | 插件同步 + 本地数据库 | 定时/Webhook 同步到 postgres,复杂查询走数据库 |
193
+ | 需要把 Base 当运行数据源单向读(不在 Base 上写回),且数据量大(需分片/多次分页/多表)或查询复杂/需持续同步 | **全栈 + Base → 应用数据库单向同步** | 用 `lark-cli apps +db-sync-create` 把源表同步成应用库表:`mode=streaming` 持续增量、`mode=batch` 只导一次;页面和服务端查应用数据库,不要每次加载用插件全量拉 Base。配置格式、字段映射和 `--preview`→`--yes` 流程见 lark-apps-db skill |
194
+ | 复杂排序/多表关联/全文搜索 | **Base → 应用数据库单向同步** | 优先 `+db-sync-create`;同步表达不了映射时再自写定时/Webhook 同步,复杂查询走数据库 |
178
195
  | 高频写入 + 读取 | 本地数据库为主 | 多维表格仅作展示/备份 |
179
196
 
197
+ > 单向同步只解决 Base → 应用数据库,应用侧修改不回写 Base;需要回写仍走插件。多环境应用同步建新表时必须显式 `--environment dev`(online 禁 DDL)。
198
+
199
+ 运行时读取要求:
200
+
201
+ - 页面不要每次加载全量拉 Base:必须服务端分页或用 `aggregateQuery`,只把当前页/聚合结果传到浏览器。
202
+ - 首屏可以先展示缓存或上一次结果,但要标明数据时间;后台刷新失败时保留旧数据并提示,不要整页失败。
203
+
180
204
  ## 数据分析类Action补充说明
181
205
 
182
206
  `aggregateQuery` 用于分组聚合统计,支持 dimensions(分组维度)和 measures(聚合计算)。
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: tag-agent-task
3
+ description: "Use when 应用运行时需要把一件事派发给 Tag 智能体员工(AE)去做:服务端通过 PlatformHttpClientService 调平台集成接口 tag.create_agent_task 创建 Agent 任务,Agent 的飞书机器人在用户与 Agent 的飞书私聊(或新建群)里发出任务卡片,返回 task_id、chat_id 与飞书链接 url。覆盖请求/响应合同、给人看的任务卡片(title / summary / link_url)、client_token 幂等重试、错误码处理、NestJS service + controller 与前端调用示例。不用于 Agent 自己在开发期执行任务,也不用于插件(capabilityClient)或自动化触发器。触发词:派发任务, 派单, 交给 Agent, 交给智能体, 智能体员工, AE, Tag Agent, Tag 智能体, 让 Agent 去做, 创建 Agent 任务, create_agent_task, 在飞书中查看"
4
+ steering: true
5
+ steering-topic: tag_agent_task
6
+ match-template-name: nestjs-react-fullstack
7
+ available-request-source:
8
+ - workmate
9
+ - doubao_workmate
10
+ ---
11
+
12
+ # 派发任务给 Tag 智能体员工
13
+
14
+ 应用(运行态)把一段自然语言任务交给搭建本应用的 Tag 智能体员工(AE)执行:Agent 的飞书机器人在它和当前用户的**飞书私聊**里发一张任务卡片(传 `create_group` 时改为新建一个群),然后按这条任务开始做。调用**立即返回** `task_id`、`chat_id` 和飞书链接 `url`,不会等 Agent 做完;用户点链接在飞书里看进展、接着和 Agent 对话。
15
+
16
+ ## 使用边界
17
+
18
+ | 场景 | 做法 |
19
+ |------|------|
20
+ | 页面上「交给 Agent 处理」「派单给智能体」之类的按钮 / 业务流程 | 用本 skill:前端 → 应用后端接口 → `tag.create_agent_task` |
21
+ | 你(开发期 Agent)自己要做的事 | 直接做,不要生成调用本接口的代码 |
22
+ | 飞书消息 / 多维表格 / AI 生文等插件能力 | 走 plugin-guide |
23
+ | 定时 / 数据变更自动派单 | 触发器用 trigger-guide;但触发器入口**没有登录用户**,本接口会失败(见下文「操作人只能是当前登录用户」) |
24
+ | 查询任务进度、列表、取消 | **暂无接口**(GetAgentTask / ListAgentTasks / CancelAgentTask 后续提供),不要轮询、不要自造 |
25
+
26
+ ## 接口合同
27
+
28
+ 只能在 NestJS **服务端**调用。用 `@lark-apaas/fullstack-nestjs-core` 导出的 `PlatformHttpClientService`(`PlatformModule` 是全局模块,直接构造函数注入),通过 **`platformHttp.instance`** 发请求。不要用 `fetch` / `axios` / `rawInstance` 直连,不要手写鉴权或泳道 header;`instance` 会自动带当前请求的登录身份、SDK 请求日志与正常的请求上下文。
29
+
30
+ ```ts
31
+ const response = await this.platformHttp.instance.post(
32
+ '/api/v1/studio/innerapi/integration_apis/call',
33
+ { apiName: 'tag.create_agent_task', input }, // 不传 userID:操作人由平台按登录态识别
34
+ { timeout: 30000 },
35
+ );
36
+ ```
37
+
38
+ ```
39
+ POST /api/v1/studio/innerapi/integration_apis/call
40
+ {
41
+ "apiName": "tag.create_agent_task",
42
+ "input": {
43
+ "content": "<给 Agent 看的完整任务描述>",
44
+ "client_token": "<uuid,同一次用户操作的重试复用同一个>",
45
+ "title": "<应传:卡片标题,这一单是什么>",
46
+ "summary": "<应传:卡片里给人看的一两句话>",
47
+ "link_url": "<可选:https 业务页面链接>"
48
+ }
49
+ }
50
+ ```
51
+
52
+ | input 字段 | 必填 | 说明 |
53
+ |---|---|---|
54
+ | `content` | 是 | 交给 Agent 的完整任务描述,写法见下方「content 必须写完整」 |
55
+ | `client_token` | 是 | 幂等键,每次**用户操作**生成一个 UUID;超时 / 5xx / 限流后重试必须复用,避免重复建任务 |
56
+ | `title` | **应传**(接口选填) | 卡片标题(≤ 50 字,超长截断),说清这一单是什么,如「工单 OC-0014 待排查」;不传只显示通用的「来自 <应用名> 的任务」,用户在飞书里分不清是哪一单 |
57
+ | `summary` | **应传**(接口选填) | 卡片里给人看的一两句说明(≤ 300 字,超长截断),如「统计页每日完成数与待办列表对不上,请排查」;不传卡片就没有正文 |
58
+ | `link_url` | 否 | 卡片「查看详情」按钮跳转的业务页面(如本应用里这条记录的详情页),必须是 `https://` 绝对地址,否则报参数错误 |
59
+ | `create_group` | 否 | `{ name?, disable_reply_topic? }`:传了就新建一个群来跑(用户任群主),**只在界面上明确要「建群处理」时才传**;不传在私聊里跑 |
60
+
61
+ 成功输出:`{ task_id, url, chat_id }`:
62
+ - `url`:飞书链接,打开任务所在的会话并定位到任务卡片,展示给用户。
63
+ - `task_id`:任务卡片那条飞书消息的 ID(`om_` 开头),按不透明字符串保存。
64
+ - `chat_id`:任务所在的飞书会话(私聊或新建的群)。
65
+
66
+ ### 卡片给人看,content 给 Agent 看
67
+
68
+ - 平台**每次派单都会发卡片**,用户在飞书里看到的就是它:标题、`summary`、一行「来自妙搭应用「<应用名>」」、可选的「查看详情」按钮。卡片给人看,`content` 给 Agent 看,所以实现时**应当传 `title` 和 `summary`**(按这一单的业务数据生成),有对应业务页面就再传 `link_url`;只传 `content` 时卡片只有通用标题,用户分不清是哪一单。
69
+ - **不要自己拼飞书卡片 JSON**,也不要把 `content` 原样塞进 `summary`。
70
+ - `summary` 写人看得懂的一两句话(这单是什么、为什么派),**不要**放记录 ID、回写指引这类给 Agent 的内容;那些写进 `content`。
71
+ - Agent 只读 `content`,读不到卡片里的文字,所以 `content` 仍要按下一节写完整。
72
+
73
+ ### content 必须写完整
74
+
75
+ Agent 拿到的**只有** `content` 这段文字:看不到应用页面、不知道用户点的是哪条数据,也不会追问。实现时由代码把下面这些拼进 `content`,不要只传一句「处理这条记录」:
76
+
77
+ - **做什么、交付什么**:任务目标、期望产出、怎样算完成。
78
+ - **数据的定位标识**:基于哪条数据派单,就把它的 ID 带上,Agent 才能找到原数据、做完后回写。
79
+ - 多维表格记录:`app_token`、`table_id`、`record_id` 都要给。
80
+ - 应用自己的数据库:表名 + 主键 ID。
81
+ - 相关链接(记录链接、文档链接等)。
82
+ - **数据内容本身**:把关键字段的值写进去(标题、描述、负责人、截止时间等),不要指望 Agent 自己去查。
83
+ - **怎么回报进展**:应用如果提供了可供 Agent 调用的能力(如本应用的 MCP 工具、回写接口),写明工具名、要传的 ID 和字段,让 Agent 有进展或完成时调用它更新状态;没有就不写,不要编造工具名。本应用的 MCP 工具要在应用发布后 Agent 才能调用。
84
+
85
+ 推荐结构(在后端按记录 ID 查出数据后组装):
86
+
87
+ ```
88
+ 任务:<要 Agent 做的事>
89
+ 来源:多维表格记录 record_id=<recXXXX>(app_token=<...>,table_id=<...>)
90
+ 记录内容:
91
+ - 标题:<...>
92
+ - 描述:<...>
93
+ - 截止时间:<...>
94
+ 期望产出:<交付物与完成标准>
95
+ 进展回写:有进展或完成时,调用本应用 MCP 工具 `<工具名>`,传 record_id=<recXXXX> 和进展说明 / 结果。
96
+ 相关人员(要某人做事时必写):<姓名>(LarkID=<larkId>):<要他做的事>
97
+ ```
98
+
99
+ ### 响应解包
100
+
101
+ 与官方插件一致,响应体是 `{ status_code, error_msg?, data: { success, output } }`:
102
+
103
+ 1. HTTP 非 2xx:`post` 直接抛错(`HttpError`,带 `response?.status`;超时 / 网络错误时 `response` 为空)。
104
+ 2. `status_code !== '0'`:平台级错误(网关 / 鉴权),按服务端错误处理。
105
+ 3. `data.success !== true`:调用失败,`data.output` 是 `{ code, message, detail }`:Tag 业务错误时 `detail.code` 是 Tag 错误码(如 `100004003`)、`detail.retryable` 表示可否重试;平台访问 Tag 超时 / 不可用时 `code` 为 `Unavailable`(无 `detail.code`),按服务端错误重试。
106
+ 4. 否则 `data.output` 即 `{ task_id, url, chat_id }`。
107
+
108
+ ### 错误码
109
+
110
+ | Tag 错误码 | 含义 | 处理 |
111
+ |---|---|---|
112
+ | `100004001` | 参数错误 | 修正参数,**不重试**;返回 400 |
113
+ | `100004002` | 智能体不存在 | 不重试;提示目标智能体不存在 |
114
+ | `100004003` | 无权限:当前用户不能使用该智能体(不是它的主人 / 项目成员,或不在可协作范围内、智能体没开「允许团队外的人私聊」)/ 租户不匹配 | 不重试;给用户清晰提示(见示例),**不要返回 403**(前端 403 会被统一拦成「无操作权限,请联系管理员分配角色」,误导用户) |
115
+ | `100004006` | 限流(每个智能体 30 次/分钟) | 返回 429,提示稍后再试;用户重试时**复用同一个 client_token** |
116
+ | `100005900` | 飞书接口失败(机器人联系不到用户 / 建群失败) | 复用 client_token **只重试一次**;仍失败返回 503,提示「智能体暂时联系不到你,请稍后再试」 |
117
+ | `100006006`、其他 `100005xxx` / `100006xxx`、`output.code === 'Unavailable'`、超时 / 网络错误 | 服务端错误 | 退避重试,复用 client_token;仍失败返回 503 |
118
+ | 其他 | — | 不重试 |
119
+
120
+ ### 操作人只能是当前登录用户
121
+
122
+ 派单人(操作人)**永远是当前登录的用户**,由平台从登录态识别:`PlatformHttpClientService` 发请求时会自动带上当前请求的登录身份,平台只认这个。所以:
123
+
124
+ - 请求体**不要传 `userID`**,也不要想办法指定别的操作人;传了也不作数,平台仍按登录用户派单。不能「替别人派单」。
125
+ - 必须在**处理登录用户这次请求的调用链里**调用(Controller → Service 一路 `await` 下去)。应用关闭了登录、匿名访问、触发器 / Webhook / OpenAPI Key 入口、定时任务、脱离请求的后台队列都没有登录用户,调用会返回 `Unauthenticated`——此时直接给出明确提示(「需要登录后才能派发任务」),不要伪造或硬编码用户 ID。
126
+ - 当前登录用户就是这个智能体本身时(不能给自己派单)返回参数错误,按 `100004001` 同样处理即可。
127
+
128
+ ### 要让某个人做事:content 里写明此人的 LarkID
129
+
130
+ 功能是「让某个人去做某件事」(如指派给负责人、让审批人处理、通知某位同事跟进)时,操作人仍是当前登录用户(平台自动带上,不用传);**被指派的那个人**要写进 `content`,并带上他的 **LarkID**,只写姓名 Agent 找不到人:
131
+
132
+ - **LarkID** 是飞书全局用户 ID(纯数字字符串)。这里是 LarkID 的**唯一用途例外**:Tag Agent 认的就是它,不要换成 `employee_id`、`open_id` 或妙搭 userId。
133
+ - 手里是妙搭 userId:`AuthNPaasService.getBatchLarkIds([userId])`(见 user-identity)。
134
+ - 来自人员选择器 / 搜人:结果里的 `lark_id` 直接用;只有 `employee_id` 时先 `getBatchMiaodaUserIds` 转妙搭 userId,再 `getBatchLarkIds`。
135
+ - 不用写租户 ID:被指派人和操作人同租户,平台已经把租户带给 Tag。
136
+ - 按**字符串**原样拼进去,不要 `Number()` / `parseInt()`(大整数会丢精度,派错人)。
137
+ - 取不到此人的 LarkID(外部用户、转换返回 `null` 等)时,不要省略或编造,直接给用户明确提示,不发起派单。
138
+ - 写法:`相关人员:<姓名>(LarkID=<larkId>):<要他做的事>`;多个人逐行列出。
139
+
140
+ ## 规则
141
+
142
+ - 只在服务端调用;前端只调应用自己的后端接口(如 `POST /api/agent-tasks`)。
143
+ - 操作人就是当前登录用户:请求体不传 `userID`,前端也不能传入或指定操作人;接口加 `@NeedLogin()`。
144
+ - 后端接口校验前端传入的 `content` / 业务 ID:`content` 非空、去首尾空白、限制长度;`title` / `summary` 去首尾空白、非空;`clientToken` 校验为 UUID;`linkUrl` 只接受 `https://`。
145
+ - `client_token` 每次用户操作生成一次(前端 `crypto.randomUUID()`),失败重试复用,成功后再换新。
146
+ - 成功后提示「已在飞书里发起,点此查看」,链接用返回的 `url`(新窗口打开)。
147
+ - 不要轮询任务状态,不要把 `url` 当作完成标志;如需留痕,把 `task_id`、`chat_id`、`url` 存进业务表。
148
+ - 日志里不要打印完整 `content`(可能含业务敏感数据),记 `task_id` 与 logid 即可。
149
+
150
+ ## 示例代码
151
+
152
+ 完整的 NestJS 服务端(类型、Service 含退避重试与错误映射、Controller、Module)与前端调用示例见 [references/example.md](references/example.md)。写代码前先读它,按其中的响应解包与错误处理实现,不要自行简化掉重试或 client_token 复用。
153
+
154
+ 示例里前端直接传 `content` 只是演示调用链;基于业务数据(如多维表格记录)派单时,应改为前端只传记录 ID 等标识,后端查出数据后按「content 必须写完整」的推荐结构组装 `content`。
155
+
156
+ ## 自检
157
+
158
+ - [ ] 只有服务端调用 `integration_apis/call`,前端只调 `/api/agent-tasks`
159
+ - [ ] 用的是 `platformHttp.instance`(不是 `fetch` / `axios` / `rawInstance`),没有手写鉴权或泳道 header
160
+ - [ ] 请求体没有传 `userID`;在登录用户请求的调用链里调用,接口加了 `@NeedLogin()`,无登录用户时有明确提示
161
+ - [ ] 功能是让某个人做事时,`content` 里写了此人的 LarkID(字符串,不是 employee_id / open_id / 妙搭 userId),取不到时明确提示、不派单
162
+ - [ ] `content` 带上了数据的定位 ID(如多维表格 `record_id`)、关键字段内容、期望产出,以及应用有回写能力时的进展回写方式
163
+ - [ ] 传了 `title` 和 `summary`(按这一单的业务数据生成),有业务页面时传了 `link_url`;没有自己拼卡片 JSON;`summary` 里没有给 Agent 的 ID 和指令
164
+ - [ ] `client_token` 按用户操作生成、重试复用
165
+ - [ ] `100004003` 没有映射成 403;限流返回 429;`100005900` 只重试一次;只对服务端错误 / 网络错误退避重试
166
+ - [ ] 成功后展示「已在飞书里发起,点此查看」链接,没有轮询任务状态
@@ -0,0 +1,268 @@
1
+ # 派发任务给 Tag 智能体员工:完整示例
2
+
3
+ ## 服务端示例
4
+
5
+ `shared/api.interface.ts`:
6
+
7
+ ```typescript
8
+ export interface CreateAgentTaskRequest {
9
+ content: string; // 给 Agent 看
10
+ clientToken: string;
11
+ title: string; // 以下给人看(飞书卡片):这一单是什么
12
+ summary: string; // 一两句说明
13
+ linkUrl?: string;
14
+ }
15
+
16
+ export interface CreateAgentTaskResponse {
17
+ taskId: string; // 飞书消息 ID(om_),不透明字符串
18
+ chatId: string;
19
+ url: string; // 飞书链接
20
+ }
21
+ ```
22
+
23
+ `server/modules/agent-task/agent-task.service.ts`:
24
+
25
+ ```typescript
26
+ import {
27
+ BadRequestException,
28
+ HttpException,
29
+ HttpStatus,
30
+ Injectable,
31
+ Logger,
32
+ NotFoundException,
33
+ ServiceUnavailableException,
34
+ UnauthorizedException,
35
+ UnprocessableEntityException,
36
+ } from '@nestjs/common';
37
+ import { PlatformHttpClientService } from '@lark-apaas/fullstack-nestjs-core';
38
+ import type { CreateAgentTaskRequest, CreateAgentTaskResponse } from '@shared/api.interface';
39
+
40
+ interface IntegrationApiBody<T> {
41
+ status_code: string;
42
+ error_msg?: string;
43
+ data?: { success: boolean; output: T };
44
+ }
45
+
46
+ interface TagTaskOutput {
47
+ task_id: string;
48
+ url: string;
49
+ chat_id: string;
50
+ }
51
+
52
+ // 飞书接口失败(机器人联系不到用户 / 建群失败),Tag 建议只重试一次
53
+ const FEISHU_FAILED_CODE = '100005900';
54
+
55
+ const RETRY_DELAYS_MS = [1000, 3000];
56
+
57
+ @Injectable()
58
+ export class AgentTaskService {
59
+ private readonly logger = new Logger(AgentTaskService.name);
60
+
61
+ constructor(private readonly platformHttp: PlatformHttpClientService) {}
62
+
63
+ // loginUserId 只用来提前拦掉未登录的请求;操作人由平台按本次请求的登录态识别,请求体不传 userID。
64
+ // 必须在处理登录用户请求的调用链里 await 调用,不要丢进定时任务 / 后台队列。
65
+ async create(loginUserId: string | undefined, task: CreateAgentTaskRequest): Promise<CreateAgentTaskResponse> {
66
+ if (!loginUserId) {
67
+ throw new UnauthorizedException('需要登录后才能派发任务');
68
+ }
69
+ for (let attempt = 0; ; attempt++) {
70
+ try {
71
+ const output = await this.callOnce(task);
72
+ this.logger.log(`agent task created: task_id=${output.task_id}`);
73
+ return { taskId: String(output.task_id), chatId: output.chat_id, url: output.url };
74
+ } catch (err) {
75
+ const message = err instanceof Error ? err.message : String(err);
76
+ const code = err instanceof TagCallError ? err.tagCode : undefined;
77
+ const retryable = err instanceof TagCallError ? err.retryable : isNetworkError(err);
78
+ const maxRetries = code === FEISHU_FAILED_CODE ? 1 : RETRY_DELAYS_MS.length;
79
+ if (retryable && attempt < maxRetries) {
80
+ this.logger.warn(`create_agent_task retry #${attempt + 1}: ${message}`);
81
+ await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt]));
82
+ continue; // 复用同一个 clientToken
83
+ }
84
+ this.logger.error(`create_agent_task failed: ${message}`);
85
+ throw toHttpException(code, message, retryable);
86
+ }
87
+ }
88
+ }
89
+
90
+ private async callOnce(task: CreateAgentTaskRequest): Promise<TagTaskOutput> {
91
+ const response = await this.platformHttp.instance.post(
92
+ '/api/v1/studio/innerapi/integration_apis/call',
93
+ {
94
+ apiName: 'tag.create_agent_task',
95
+ input: {
96
+ // 给 Agent 看,要写完整;要某人做事时写明此人的 LarkID
97
+ content: task.content,
98
+ client_token: task.clientToken,
99
+ // 给人看的卡片:平台按这几个字段生成极简卡片,不要自己拼卡片 JSON
100
+ title: task.title,
101
+ summary: task.summary,
102
+ link_url: task.linkUrl,
103
+ // 只有界面上明确要「建群处理」时才加:create_group: { name: '<群名>' },
104
+ },
105
+ },
106
+ { timeout: 30000 },
107
+ );
108
+ const body = (await response.json()) as IntegrationApiBody<TagTaskOutput>;
109
+ if (body.status_code !== '0') {
110
+ // 平台级错误(网关 / 鉴权),按服务端错误重试
111
+ throw new TagCallError(`platform error ${body.status_code}: ${body.error_msg ?? ''}`, undefined, true);
112
+ }
113
+ if (body.data?.success !== true) {
114
+ const out = (body.data?.output ?? {}) as unknown as FailureOutput;
115
+ const tagCode = out.detail?.code !== undefined ? String(out.detail.code) : undefined;
116
+ // Tag 业务错误看 detail.retryable;平台访问 Tag 超时 / 不可用(code=Unavailable)按服务端错误重试
117
+ // 限流(100004006)不在服务端重试,交给用户稍后再发(复用同一个 client_token)
118
+ const retryable = tagCode
119
+ ? tagCode !== '100004006' && (out.detail?.retryable === true || isServerError(tagCode))
120
+ : out.code === 'Unavailable';
121
+ throw new TagCallError(out.message ?? JSON.stringify(out), tagCode, retryable);
122
+ }
123
+ return body.data.output;
124
+ }
125
+ }
126
+
127
+ interface FailureOutput {
128
+ code?: string;
129
+ message?: string;
130
+ detail?: { code?: string | number; message?: string; retryable?: boolean };
131
+ }
132
+
133
+ class TagCallError extends Error {
134
+ constructor(message: string, readonly tagCode: string | undefined, readonly retryable: boolean) {
135
+ super(message);
136
+ }
137
+ }
138
+
139
+ function isServerError(code?: string): boolean {
140
+ return !!code && (code.startsWith('100005') || code.startsWith('100006'));
141
+ }
142
+
143
+ function isNetworkError(err: unknown): boolean {
144
+ // HttpError 没有 response = 超时 / 网络错误;有 response 且 5xx 也视为可重试
145
+ const status = (err as { response?: { status?: number } })?.response?.status;
146
+ return (err as { name?: string })?.name === 'HttpError' && (status === undefined || status >= 500);
147
+ }
148
+
149
+ function toHttpException(code: string | undefined, message: string, retryable: boolean): HttpException {
150
+ switch (code) {
151
+ case '100004001':
152
+ return new BadRequestException('任务参数不合法,请检查任务内容(也不能给智能体自己派单)');
153
+ case '100004002':
154
+ return new NotFoundException('目标智能体不存在');
155
+ case '100004003':
156
+ return new UnprocessableEntityException('你的账号暂时无法使用该智能体,请联系智能体的主人开通协作权限');
157
+ case '100004006':
158
+ return new HttpException('派发太频繁,请稍后再试', HttpStatus.TOO_MANY_REQUESTS);
159
+ case FEISHU_FAILED_CODE:
160
+ return new ServiceUnavailableException('智能体暂时联系不到你,请稍后再试');
161
+ default:
162
+ return retryable
163
+ ? new ServiceUnavailableException('智能体服务暂时不可用,请稍后重试')
164
+ : new BadRequestException(`派发任务失败:${message}`);
165
+ }
166
+ }
167
+ ```
168
+
169
+ `server/modules/agent-task/agent-task.controller.ts`:
170
+
171
+ ```typescript
172
+ import { Body, Controller, Post, Req } from '@nestjs/common';
173
+ import type { Request } from 'express';
174
+ import { NeedLogin } from '@lark-apaas/fullstack-nestjs-core';
175
+ import { IsNotEmpty, IsOptional, IsString, IsUrl, IsUUID, MaxLength } from 'class-validator';
176
+ import { Transform } from 'class-transformer';
177
+ import type { CreateAgentTaskRequest, CreateAgentTaskResponse } from '@shared/api.interface';
178
+ import { AgentTaskService } from './agent-task.service';
179
+
180
+ class CreateAgentTaskDto implements CreateAgentTaskRequest {
181
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
182
+ @IsString()
183
+ @IsNotEmpty()
184
+ @MaxLength(5000) // 业务上限示例;平台拒绝时会返回 100004001
185
+ content!: string;
186
+
187
+ @IsUUID()
188
+ clientToken!: string;
189
+
190
+ // 以下只用于给人看的飞书卡片(接口选填,但应当传,否则卡片只有通用标题);
191
+ // title 超 50 字、summary 超 300 字由平台截断,这里不拦
192
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
193
+ @IsString()
194
+ @IsNotEmpty()
195
+ title!: string;
196
+
197
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
198
+ @IsString()
199
+ @IsNotEmpty()
200
+ summary!: string;
201
+
202
+ @IsOptional()
203
+ @IsUrl({ protocols: ['https'], require_protocol: true })
204
+ linkUrl?: string;
205
+ }
206
+
207
+ @Controller('api/agent-tasks')
208
+ export class AgentTaskController {
209
+ constructor(private readonly agentTaskService: AgentTaskService) {}
210
+
211
+ @NeedLogin()
212
+ @Post()
213
+ async create(@Req() req: Request, @Body() dto: CreateAgentTaskDto): Promise<CreateAgentTaskResponse> {
214
+ return this.agentTaskService.create(req.userContext.userId, dto);
215
+ }
216
+ }
217
+ ```
218
+
219
+ `agent-task.module.ts` 里 `controllers: [AgentTaskController]`、`providers: [AgentTaskService]`,并在 `app.module.ts` 注册。`PlatformHttpClientService` 来自全局 `PlatformModule`,不需要再 import 或自建 provider。
220
+
221
+ ## 前端示例(实际按 coding-guide 用 shadcn Button / toast,这里只示意调用与 token 复用)
222
+
223
+ ```tsx
224
+ import { useRef, useState } from 'react';
225
+ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBackend';
226
+ import type { CreateAgentTaskResponse } from '@shared/api.interface';
227
+
228
+ // content 给 Agent 看(要写完整);title / summary 给人看(飞书卡片),按这一单的业务数据生成
229
+ export function DispatchToAgentButton({ content, title, summary }: { content: string; title: string; summary: string }) {
230
+ const tokenRef = useRef<string | null>(null);
231
+ const [result, setResult] = useState<CreateAgentTaskResponse | null>(null);
232
+ const [error, setError] = useState('');
233
+ const [loading, setLoading] = useState(false);
234
+
235
+ const dispatch = async () => {
236
+ // 同一次操作失败后重试复用 token;成功后清空,下次操作生成新的
237
+ tokenRef.current ??= crypto.randomUUID();
238
+ setLoading(true);
239
+ setError('');
240
+ try {
241
+ const res = await axiosForBackend.post<CreateAgentTaskResponse>('/api/agent-tasks', {
242
+ content,
243
+ title,
244
+ summary,
245
+ // 示例:卡片「查看详情」回到当前页面;实际优先用这条记录的详情页地址。只接受 https,非 https 时不传,避免整单被拒
246
+ linkUrl: window.location.protocol === 'https:' ? window.location.href : undefined,
247
+ clientToken: tokenRef.current,
248
+ });
249
+ setResult(res.data);
250
+ tokenRef.current = null;
251
+ } catch (e: any) {
252
+ setError(e?.response?.data?.error?.message ?? '派发失败,请稍后重试');
253
+ } finally {
254
+ setLoading(false);
255
+ }
256
+ };
257
+
258
+ return (
259
+ <div className="flex items-center gap-3">
260
+ <button disabled={loading} onClick={dispatch}>交给 Agent 处理</button>
261
+ {result && (
262
+ <a href={result.url} target="_blank" rel="noreferrer">已在飞书里发起,点此查看</a>
263
+ )}
264
+ {error && <span className="text-destructive">{error}</span>}
265
+ </div>
266
+ );
267
+ }
268
+ ```
@@ -69,6 +69,8 @@ match-template-name: vite-react
69
69
  - 构建搜索过滤条件(filter / conditions)时
70
70
  - 使用多维表格相关 UI 组件(人员展示、超链接、单/多选下拉 等;vite-react 用 shadcn-ui 拼装)时
71
71
 
72
+ **多维表格字段结构以插件为准**。lark-cli 读到的文本是字符串:链接和 @文档都写成 `[文字](链接)`,@人写成 `<at user_id=…>@名字</at>`;单选是数组。插件返回的是结构化字段,不能照 lark-cli 的输出定类型和渲染,各字段读写格式见 `references/table.md`「BizType 读写格式对照表」。Text 字段读取传 `textFormat: 'original'`,返回 `{ text, originalValue?: TextSegment[] }`,段 `type` 为 text / url / mention。渲染用 `BitableText`:有 `link` 就渲染成链接(url 段和 @文档的 mention 段都有);mention 段没有 `link` 的是 @人,按 `text` 显示、不可点;其余按 `text`。不要按 `type` 分支(mention 不等于 @人)。类型定义、AGENTS.md、派 Worker 的说明按此写。
73
+
72
74
  ## Plugin 链式调用(Plugin Chain)
73
75
 
74
76
  很多业务场景需要多个插件串联完成,**禁止用正则/字符串解析替代 AI 插件做结构化输出处理**(包括提取和生成场景)。
@@ -111,6 +111,21 @@ type TextSegment = {
111
111
  };
112
112
  ```
113
113
 
114
+ ### lark-cli 输出与插件分段的对照
115
+
116
+ lark-cli(`+record-list` / `+record-search`)读到的文本字段是字符串,链接和 @ 写成标记文本;插件返回的是上面的 `TextSegment` 数组。对应关系:
117
+
118
+ | lark-cli 读到的 | 插件 `originalValue` 里的段 | 页面展示 |
119
+ | --- | --- | --- |
120
+ | 普通文字(可能含 `\n`) | `{ type: 'text', text }` | 文字,保留换行 |
121
+ | `[文字](https://…)`,普通超链接 | `{ type: 'url', text, link }` | `<a>` 可点击 |
122
+ | `[文字](https://…)`,@文档 | `{ type: 'mention', mentionType: 'Docx' / 'Wiki' / 'Bitable' 等, text, link, token }` | `<a>` 可点击 |
123
+ | `<at user_id=ou_…>@名字</at>`,@人 | `{ type: 'mention', mentionType: 'User', text: '@名字', token: 'ou_…' }`,没有 `link` | 文字,不可点击 |
124
+
125
+ - lark-cli 里普通超链接和 @文档写法一样,插件里分别是 url 段和 mention 段。渲染只看 `link`:有就渲染成链接,没有就按 `text` 显示。不要按 `type` 或 `mentionType` 分支。
126
+ - @人的 `ou_…` 按开放平台应用隔离:同一个人在 lark-cli 和插件里可能是不同的 id,不要跨来源匹配。
127
+ - 其他字段也不一样(如单选在 lark-cli 是数组、插件是字符串)。字段结构以上面的「BizType 读写格式对照表」为准,不要从 lark-cli 的输出推。
128
+
114
129
  兼容说明:`originalValue` 是可选的新增字段,不传 `textFormat` 时返回内容与原来完全一致,只读 `.text` 的存量代码不用改也能正常运行。要链接可点击、保留换行时,再按下文迁移:读取加 `textFormat: 'original'`,展示从 `.text` 换成 `BitableText`。写入同理:Text 字段继续传字符串的存量代码行为不变,分段数组是新增的可选写法。
115
130
 
116
131
  ### 只读字段