From 999ac4e7d6e130d35bb43c55b9343aef3d2e54a2 Mon Sep 17 00:00:00 2001 From: anguohui Date: Tue, 23 Jun 2026 11:46:13 +0800 Subject: [PATCH] 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. --- .../references/lark-apps-plugin-call.md | 201 ++---------------- 1 file changed, 14 insertions(+), 187 deletions(-) diff --git a/skills/lark-apps/references/lark-apps-plugin-call.md b/skills/lark-apps/references/lark-apps-plugin-call.md index c6e444370..2287f8b62 100644 --- a/skills/lark-apps/references/lark-apps-plugin-call.md +++ b/skills/lark-apps/references/lark-apps-plugin-call.md @@ -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 -.callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable -``` +技术栈 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, ['content']); - if (delta) { - fullContent += delta; - setContent(fullContent); - } -} -``` - -#### normalizeStream(必须) - -`callStream()` 可能返回 `AsyncIterable` 或 `{ output: AsyncIterable }`,必须归一化: - -```typescript -type AnyRecord = Record; - -function isAsyncIterable(value: unknown): value is AsyncIterable { - return !!value && typeof (value as AnyRecord)[Symbol.asyncIterator] === 'function'; -} - -function normalizeStream(resultOrStream: unknown): AsyncIterable { - 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; - } - 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, ['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) { - try { - return await this.capabilityService - .load('') - .call('', input); - } catch (error) { - this.logger.error('pluginInstance call failed', { - pluginInstanceId: '', - actionKey: '', - 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