mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 10:08:02 +08:00
refactor: slim down plugin-call to decisions only, delegate code patterns to tech-stack skill
Remove all code pattern content (capabilityClient imports, normalizeStream, NestJS injection, streaming examples, chunk field table) from lark-apps-plugin-call.md. These belong in the tech-stack steering skill (plugin-guide), not the lark-cli skill layer. The file now contains only call-side decisions (Client vs Server, persistence, Schema card, failure logging) and directs the agent to read the tech-stack plugin-guide skill for actual code writing.
This commit is contained in:
@@ -1,14 +1,8 @@
|
||||
# 插件实例调用代码编写指南
|
||||
# 插件实例调用指南
|
||||
|
||||
创建/更新插件实例后,根据本文件生成调用代码。
|
||||
创建/更新插件实例后,根据本文件做调用决策,再读技术栈 Skill 写代码。
|
||||
|
||||
本文件分两部分:**调用决策**(选择调用侧、是否持久化)和**代码模式**(具体写法)。
|
||||
|
||||
---
|
||||
|
||||
## 第一部分:调用决策
|
||||
|
||||
### 调用前获取权威依据
|
||||
## 调用前获取权威依据
|
||||
|
||||
**必须**先读取以下文件获取 actions 信息:
|
||||
|
||||
@@ -75,185 +69,18 @@ output.fields: [...]
|
||||
|
||||
---
|
||||
|
||||
## 第二部分:代码模式
|
||||
## 生成调用代码
|
||||
|
||||
> 以下内容与妙搭技术栈(`@lark-apaas/client-toolkit`、NestJS)绑定。如仓库本地有技术栈 Skill(如 `.agent/skills/plugin-coding-guide/SKILL.md`),优先读仓库本地版本。
|
||||
完成上述决策后,**读取项目的技术栈 Skill** 获取具体代码模式(import 路径、call/callStream 写法、normalizeStream、NestJS 注入等)。
|
||||
|
||||
### call / callStream 函数签名
|
||||
技术栈 Skill 按项目类型不同:
|
||||
- **Design / Modern 应用**(纯前端)→ 读 `plugin-guide` Skill(仅 `capabilityClient`)
|
||||
- **全栈应用**(NestJS + React)→ 读 `plugin-guide` Skill(`capabilityClient` + `CapabilityService`)
|
||||
|
||||
```typescript
|
||||
.call(actionKey: string, input: object) // 非流式,返回 Promise<output>
|
||||
.callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
|
||||
```
|
||||
技术栈 Skill 的位置取决于运行环境:
|
||||
- **妙搭平台 Agent**:自动加载 steering skill(`steering-topic: plugin_guide`)
|
||||
- **本地 Code Agent**:读仓库内 `.agent/skills/plugin-guide/SKILL.md`(如存在)
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:把参数 JSON.stringify 后当 actionKey
|
||||
plugin.call(JSON.stringify({ text: '...' }));
|
||||
// ❌ 错误:漏掉 actionKey,直接传参数
|
||||
plugin.call({ text: '...' });
|
||||
// ✅ 正确:第一个参数是 actionKey 字符串,第二个是 input 对象
|
||||
plugin.call('textGenerate', { text: '...' });
|
||||
```
|
||||
|
||||
**唯一导入方式**(严禁其他路径):
|
||||
```typescript
|
||||
// ❌ 严禁
|
||||
import { capabilityClient } from '@lark-apaas/client-capability';
|
||||
// ✅ 唯一指定
|
||||
import { capabilityClient } from '@lark-apaas/client-toolkit';
|
||||
```
|
||||
|
||||
### Client 侧调用
|
||||
|
||||
#### 非流式(outputMode = "unary")
|
||||
|
||||
```typescript
|
||||
import { capabilityClient } from '@lark-apaas/client-toolkit';
|
||||
import { logger } from "@lark-apaas/client-toolkit/logger";
|
||||
|
||||
const result = await capabilityClient
|
||||
.load('task_text_summary')
|
||||
.call('textGenerate', { task_content: '...' });
|
||||
|
||||
logger.info(result);
|
||||
```
|
||||
|
||||
#### 流式(outputMode = "stream")
|
||||
|
||||
```typescript
|
||||
const streamResult = capabilityClient
|
||||
.load('task_text_summary')
|
||||
.callStream('textGenerate', { task_content: '...' });
|
||||
|
||||
const stream = normalizeStream(streamResult);
|
||||
let fullContent = '';
|
||||
for await (const chunk of stream) {
|
||||
const delta = readFirstStringField(chunk as Record<string, unknown>, ['content']);
|
||||
if (delta) {
|
||||
fullContent += delta;
|
||||
setContent(fullContent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### normalizeStream(必须)
|
||||
|
||||
`callStream()` 可能返回 `AsyncIterable<chunk>` 或 `{ output: AsyncIterable<chunk> }`,必须归一化:
|
||||
|
||||
```typescript
|
||||
type AnyRecord = Record<string, unknown>;
|
||||
|
||||
function isAsyncIterable(value: unknown): value is AsyncIterable<AnyRecord> {
|
||||
return !!value && typeof (value as AnyRecord)[Symbol.asyncIterator] === 'function';
|
||||
}
|
||||
|
||||
function normalizeStream(resultOrStream: unknown): AsyncIterable<AnyRecord> {
|
||||
if (isAsyncIterable(resultOrStream)) return resultOrStream;
|
||||
if (
|
||||
resultOrStream && typeof resultOrStream === 'object' &&
|
||||
'output' in (resultOrStream as AnyRecord) &&
|
||||
isAsyncIterable((resultOrStream as AnyRecord).output)
|
||||
) {
|
||||
return (resultOrStream as AnyRecord).output as AsyncIterable<AnyRecord>;
|
||||
}
|
||||
throw new Error('Invalid callStream result: cannot find AsyncIterable stream');
|
||||
}
|
||||
|
||||
function readFirstStringField(chunk: AnyRecord, keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = chunk[key];
|
||||
if (typeof value === 'string') return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
#### 流式 chunk 字段速查
|
||||
|
||||
chunk 是扁平对象,字段名与 outputSchema 一致。**禁止** `chunk.data?.text`、`chunk.choices[0]` 等非 capabilityClient 格式。
|
||||
|
||||
| 插件 | chunk 字段 | 正确写法 | 错误写法 |
|
||||
|------|-----------|---------|---------|
|
||||
| ai-text-generate | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
|
||||
| ai-translate | `translation` | `chunk.translation` | ~~`chunk.content`~~ |
|
||||
| ai-text-summary | `summary` | `chunk.summary` | ~~`chunk.content`~~ |
|
||||
| ai-image-understanding | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
|
||||
| ai-image-compare | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
|
||||
| ai-search-summary | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
|
||||
|
||||
> 同一应用多页面调用同一插件时,所有页面必须使用一致的 chunk 字段名。
|
||||
|
||||
#### 多插件并行流式(推荐)
|
||||
|
||||
需求涉及多种独立输出(标题、正文、图片等)时,拆分为多个插件并行调用:
|
||||
|
||||
```tsx
|
||||
const handleGenerate = async (keywords: string) => {
|
||||
// 封面图(非流式,异步不阻塞)
|
||||
capabilityClient.load('cover_generator')
|
||||
.call<{ images: string[] }>('textToImage', { keywords })
|
||||
.then(res => res?.images?.[0] && setCoverUrl(res.images[0]))
|
||||
.catch(err => logger.warn('封面生成失败', err));
|
||||
|
||||
// 标题(非流式)
|
||||
const titleResult = await capabilityClient
|
||||
.load('title_generator')
|
||||
.call<{ content: string }>('textGenerate', { keywords });
|
||||
setTitle(titleResult?.content || '');
|
||||
|
||||
// 正文(流式)
|
||||
const contentStream = capabilityClient
|
||||
.load('content_generator')
|
||||
.callStream<{ content: string }>('textGenerate', { keywords });
|
||||
const stream = normalizeStream(contentStream);
|
||||
let fullContent = '';
|
||||
for await (const chunk of stream) {
|
||||
const delta = readFirstStringField(chunk as Record<string, unknown>, ['content']);
|
||||
if (delta) { fullContent += delta; setContent(fullContent); }
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Server 侧调用(仅全栈应用)
|
||||
|
||||
#### NestJS 注入
|
||||
|
||||
```typescript
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
|
||||
|
||||
@Injectable()
|
||||
export class XxxService {
|
||||
private readonly logger = new Logger(XxxService.name);
|
||||
constructor(@Inject() private readonly capabilityService: CapabilityService) {}
|
||||
|
||||
async callPlugin(input: Record<string, unknown>) {
|
||||
try {
|
||||
return await this.capabilityService
|
||||
.load('<pluginInstanceId>')
|
||||
.call('<actionKey>', input);
|
||||
} catch (error) {
|
||||
this.logger.error('pluginInstance call failed', {
|
||||
pluginInstanceId: '<id>',
|
||||
actionKey: '<key>',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Server 侧编排原则
|
||||
|
||||
- PluginInstance 调用属于外部依赖 / side-effect
|
||||
- 除非业务要求强一致性,默认不阻塞主业务流程
|
||||
- 推荐异步触发 + catch 兜底:
|
||||
|
||||
```typescript
|
||||
this.somePluginSideEffect(input).catch(error => {
|
||||
this.logger.warn('PluginInstance side-effect failed, ignored', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
});
|
||||
```
|
||||
根据技术栈 Skill 中的指引,基于 manifest 的 `actions[].outputMode` 选择调用方式:
|
||||
- `unary` → `capabilityClient.load(id).call(actionKey, input)`
|
||||
- `stream` → `capabilityClient.load(id).callStream(actionKey, input)` + normalizeStream
|
||||
|
||||
Reference in New Issue
Block a user