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:
anguohui
2026-06-23 11:46:13 +08:00
parent a91f2cdd85
commit 999ac4e7d6

View File

@@ -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