dddk — Cookbook:企業寫自己的 skill
給內部開發者 / 第三方參考。
場景 1:客服 SaaS — 一鍵打開最近訂單
需求:用戶按 ctrl+k 打 /recent-orders,顯示最近 10 筆 + 點擊跳轉。
import type { ActionSkill } from '@perhapxin/dddk';
const recentOrders: ActionSkill = {
id: 'recent-orders',
type: 'action',
name: '最近訂單',
description: '看最近 10 筆',
handler: async (ctx) => {
const orders = await fetch('/api/orders?limit=10').then(r => r.json());
ctx.palette.replace(orders.map(o => ({
id: o.id,
name: `#${o.id} - ${o.customer} - $${o.total}`,
handler: () => ctx.navigate(`/orders/${o.id}`),
})));
},
};
場景 2:法律 SaaS — 對選取條款用客戶語言解釋
需求:用戶選取一段法律條款 → 長按 space 講「用簡單話解釋」→ 字幕條出簡化版本。
import type { PromptSkill } from '@perhapxin/dddk';
const simplifyClause: PromptSkill = {
id: 'simplify',
type: 'prompt',
name: '用白話解釋',
prompt: '把以下法律條款用一般人能懂的中文解釋。重點:實際上會怎麼影響我?\n\n{{selection}}',
};
註冊後,dddk 看到「對選取做事」+ skill /simplify 自動把 {{selection}} 替換。
場景 3:CRM — A2UI 表單建立 lead
import type { A2UISkill } from '@perhapxin/dddk';
const newLead: A2UISkill = {
id: 'new-lead',
type: 'a2ui',
name: '新增 lead',
build: async () => ({
version: 'v0.10',
updateComponents: {
surfaceId: 'new-lead',
components: [
{ id: 'root', component: 'Card', children: ['title', 'form'] },
{ id: 'title', component: 'Heading', text: '新增 Lead' },
{ id: 'form', component: 'Column', children: ['name', 'email', 'source', 'submit'] },
{ id: 'name', component: 'TextField', label: '姓名', bind: '/name', required: true },
{ id: 'email', component: 'TextField', label: 'Email', bind: '/email', required: true },
{ id: 'source', component: 'ChoicePicker', label: '來源', bind: '/source',
options: ['官網', '展會', '推薦', '其他'] },
{ id: 'submit', component: 'Button', text: '建立', action: 'submit' },
],
},
}),
onSubmit: async (data, ctx) => {
const lead = await fetch('/api/leads', {
method: 'POST',
body: JSON.stringify(data),
}).then(r => r.json());
ctx.subtitle.show({
text: `✓ Lead #${lead.id} 已建立`,
type: 'info',
autoHide: 2000,
});
return null; // 關閉 surface
},
};
場景 4:影視製作 — ScriptSkill 帶用戶逛新功能
import type { ScriptSkill } from '@perhapxin/dddk';
const tourScriptingMode: ScriptSkill = {
id: 'tour-scripting',
type: 'script',
name: '介紹劇本模式',
steps: [
{
page: '/project/123/script',
subtitle: '這是劇本模式。每場戲是獨立區塊。',
action: (t) => t.spotlight('.scene-list'),
},
{
subtitle: '按 + 加新場景,或拖拉換順序。',
action: (t) => t.highlight('.add-scene-btn', '#ff9900', '從這裡'),
waitForUser: true,
},
{
subtitle: '每場戲可以指定演員、場景、道具,按 ctrl+k 也找得到。',
action: (t) => t.border('.scene-card', '#00aaff'),
waitForUser: true,
},
{
page: '/project/123/storyboard',
subtitle: '完成劇本後,這裡可以畫故事板。完成。',
autoHide: 3000,
},
],
};
場景 5:會計 SaaS — 動態 ActionSkill 顯示異常
const findAnomalies: ActionSkill = {
id: 'anomalies',
type: 'action',
name: '本月帳目異常',
handler: async (ctx) => {
ctx.palette.replace([{ id: 'loading', name: '分析中...', handler: () => {} }]);
const issues = await ctx.llm(
'檢查最近 30 天交易,列出可疑項目(金額異常、重複、跳號等)。回 JSON array。',
// ctx.llm 內部從 ctx 取 sheet data
);
const parsed = JSON.parse(issues);
ctx.palette.replace(parsed.map(issue => ({
id: issue.id,
name: `⚠️ ${issue.desc}`,
description: issue.amount + ' on ' + issue.date,
handler: () => ctx.navigate(`/ledger/${issue.id}`),
})));
},
};
場景 6:翻譯 / 出版 — Skill 鏈結組合
const fullTranslateWorkflow: ScriptSkill = {
id: 'translate-book',
type: 'script',
name: '翻譯整本書',
steps: [
{
subtitle: '開始翻譯流程。先讓我看一下章節結構。',
action: async (t) => {
await t.runSkill('extract-chapters');
},
},
{
subtitle: '抽取詞彙表中...',
action: async (t) => {
await t.runSkill('build-glossary');
},
},
{
subtitle: '正式翻譯,使用詞彙表。',
action: async (t) => {
await t.runSkill('translate-with-glossary', { lang: 'zh-TW' });
},
},
{
subtitle: '✓ 完成!',
autoHide: 3000,
},
],
};
場景 7:ERP — Skill 走後端權限
const approveExpense: A2UISkill = {
id: 'approve-expense',
type: 'a2ui',
name: '審核費用',
build: async (ctx) => {
// 後端會檢查用戶身分
const pending = await fetch('/api/expenses/pending', {
headers: { 'Authorization': `Bearer ${ctx.storage.get('token')}` },
}).then(r => r.json());
if (pending.length === 0) {
ctx.subtitle.show({ text: '沒有待審核項目', type: 'info', autoHide: 2000 });
return null;
}
return {
version: 'v0.10',
updateComponents: {
surfaceId: 'approve',
components: [
{ id: 'root', component: 'Card', children: ['title', 'list'] },
{ id: 'title', component: 'Heading', text: `${pending.length} 筆待審` },
{ id: 'list', component: 'List', bind: '/items' },
],
},
updateDataModel: { data: { items: pending } },
};
},
};
共通模式
Skill 自動發現
企業 monorepo 通常把 skills 集中:
src/dddk-skills/
├── index.ts # export *
├── orders.ts
├── leads.ts
├── translate.ts
└── ...
// index.ts
export * from './orders';
export * from './leads';
// ...
// app.tsx
import * as skills from '@/dddk-skills';
<DddkProvider skills={Object.values(skills)}>
分權限隱藏
ActionSkill / A2UISkill 可以根據用戶角色決定 build 行為,或讓 host 直接過濾 skills list:
const allSkills = [adminSkill, userSkill, ...];
const visibleSkills = allSkills.filter(s => s.visible?.(currentUser) ?? true);
<DddkProvider skills={visibleSkills}>
A/B 測試 skills
host 自己決定載入哪些。dddk 不管。
不建議的模式
- ❌ 把 API key / secret 寫在 skill prompt 內 — 永遠走 host fetch
- ❌ skill 內 setTimeout 跑很久 — 改用 dock A2UI 給進度
- ❌ skill 內呼叫 alert / confirm — 用 subtitle 或 A2UI
- ❌ skill 內存 global state — 用 ctx.storage