@jack200714/mafw 4.5.1 → 4.5.2
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/README.md +5 -2
- package/bin/mafw.js +16 -14
- package/package.json +1 -7
- package/.opencode/mafw/.backup-v61/parametric/base-skill-manifest.yaml +0 -2
- package/.opencode/mafw/STATUS.md +0 -6
- package/.opencode/mafw/ledger.md +0 -20
- package/.opencode/mafw/memory/.harmonic_index.json +0 -28
- package/.opencode/mafw/memory/.review_queue.json +0 -1
- package/.opencode/mafw/memory/tier3/parametric.json +0 -13
- package/.opencode/mafw/memory/tier3/test-goal.json +0 -19
- package/.opencode/mafw/memory-index.json +0 -6
- package/.opencode/mafw/parametric/base-skill-manifest.yaml +0 -2
- package/.opencode/mafw/waves.json +0 -4
- package/.opencode/opencode.json +0 -3
- package/.opencode/plugins/mafw-plugin.ts +0 -8
- package/.opencode/skills/banner-design/SKILL.md +0 -196
- package/.opencode/skills/banner-design/references/banner-sizes-and-styles.md +0 -118
- package/.opencode/skills/brand/SKILL.md +0 -97
- package/.opencode/skills/brand/references/approval-checklist.md +0 -169
- package/.opencode/skills/brand/references/asset-organization.md +0 -157
- package/.opencode/skills/brand/references/brand-guideline-template.md +0 -140
- package/.opencode/skills/brand/references/color-palette-management.md +0 -186
- package/.opencode/skills/brand/references/consistency-checklist.md +0 -94
- package/.opencode/skills/brand/references/logo-usage-rules.md +0 -185
- package/.opencode/skills/brand/references/messaging-framework.md +0 -85
- package/.opencode/skills/brand/references/typography-specifications.md +0 -214
- package/.opencode/skills/brand/references/update.md +0 -118
- package/.opencode/skills/brand/references/visual-identity.md +0 -96
- package/.opencode/skills/brand/references/voice-framework.md +0 -88
- package/.opencode/skills/brand/scripts/extract-colors.cjs +0 -341
- package/.opencode/skills/brand/scripts/inject-brand-context.cjs +0 -349
- package/.opencode/skills/brand/scripts/sync-brand-to-tokens.cjs +0 -248
- package/.opencode/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +0 -52
- package/.opencode/skills/brand/scripts/validate-asset.cjs +0 -387
- package/.opencode/skills/brand/templates/brand-guidelines-starter.md +0 -275
- package/.opencode/skills/design/SKILL.md +0 -313
- package/.opencode/skills/design/data/cip/deliverables.csv +0 -51
- package/.opencode/skills/design/data/cip/industries.csv +0 -21
- package/.opencode/skills/design/data/cip/mockup-contexts.csv +0 -21
- package/.opencode/skills/design/data/cip/styles.csv +0 -21
- package/.opencode/skills/design/data/icon/styles.csv +0 -16
- package/.opencode/skills/design/data/logo/colors.csv +0 -56
- package/.opencode/skills/design/data/logo/industries.csv +0 -56
- package/.opencode/skills/design/data/logo/styles.csv +0 -56
- package/.opencode/skills/design/references/banner-sizes-and-styles.md +0 -118
- package/.opencode/skills/design/references/cip-deliverable-guide.md +0 -95
- package/.opencode/skills/design/references/cip-design.md +0 -121
- package/.opencode/skills/design/references/cip-prompt-engineering.md +0 -84
- package/.opencode/skills/design/references/cip-style-guide.md +0 -68
- package/.opencode/skills/design/references/design-routing.md +0 -207
- package/.opencode/skills/design/references/icon-design.md +0 -122
- package/.opencode/skills/design/references/logo-color-psychology.md +0 -101
- package/.opencode/skills/design/references/logo-design.md +0 -92
- package/.opencode/skills/design/references/logo-prompt-engineering.md +0 -158
- package/.opencode/skills/design/references/logo-style-guide.md +0 -109
- package/.opencode/skills/design/references/slides-copywriting-formulas.md +0 -84
- package/.opencode/skills/design/references/slides-create.md +0 -4
- package/.opencode/skills/design/references/slides-html-template.md +0 -295
- package/.opencode/skills/design/references/slides-layout-patterns.md +0 -137
- package/.opencode/skills/design/references/slides-strategies.md +0 -94
- package/.opencode/skills/design/references/slides.md +0 -42
- package/.opencode/skills/design/references/social-photos-design.md +0 -329
- package/.opencode/skills/design/scripts/cip/core.py +0 -215
- package/.opencode/skills/design/scripts/cip/generate.py +0 -484
- package/.opencode/skills/design/scripts/cip/render-html.py +0 -424
- package/.opencode/skills/design/scripts/cip/search.py +0 -127
- package/.opencode/skills/design/scripts/icon/generate.py +0 -487
- package/.opencode/skills/design/scripts/logo/core.py +0 -175
- package/.opencode/skills/design/scripts/logo/generate.py +0 -362
- package/.opencode/skills/design/scripts/logo/search.py +0 -114
- package/.opencode/skills/design-system/SKILL.md +0 -244
- package/.opencode/skills/design-system/data/slide-backgrounds.csv +0 -11
- package/.opencode/skills/design-system/data/slide-charts.csv +0 -26
- package/.opencode/skills/design-system/data/slide-color-logic.csv +0 -14
- package/.opencode/skills/design-system/data/slide-copy.csv +0 -26
- package/.opencode/skills/design-system/data/slide-layout-logic.csv +0 -16
- package/.opencode/skills/design-system/data/slide-layouts.csv +0 -26
- package/.opencode/skills/design-system/data/slide-strategies.csv +0 -16
- package/.opencode/skills/design-system/data/slide-typography.csv +0 -15
- package/.opencode/skills/design-system/references/component-specs.md +0 -236
- package/.opencode/skills/design-system/references/component-tokens.md +0 -214
- package/.opencode/skills/design-system/references/primitive-tokens.md +0 -203
- package/.opencode/skills/design-system/references/semantic-tokens.md +0 -215
- package/.opencode/skills/design-system/references/states-and-variants.md +0 -241
- package/.opencode/skills/design-system/references/tailwind-integration.md +0 -251
- package/.opencode/skills/design-system/references/token-architecture.md +0 -224
- package/.opencode/skills/design-system/scripts/embed-tokens.cjs +0 -99
- package/.opencode/skills/design-system/scripts/fetch-background.py +0 -317
- package/.opencode/skills/design-system/scripts/generate-slide.py +0 -770
- package/.opencode/skills/design-system/scripts/generate-tokens.cjs +0 -205
- package/.opencode/skills/design-system/scripts/html-token-validator.py +0 -327
- package/.opencode/skills/design-system/scripts/search-slides.py +0 -218
- package/.opencode/skills/design-system/scripts/slide-token-validator.py +0 -35
- package/.opencode/skills/design-system/scripts/slide_search_core.py +0 -453
- package/.opencode/skills/design-system/scripts/tests/test_validate_tokens.py +0 -48
- package/.opencode/skills/design-system/scripts/validate-tokens.cjs +0 -246
- package/.opencode/skills/design-system/templates/design-tokens-starter.json +0 -143
- package/.opencode/skills/frontend-design/LICENSE.txt +0 -21
- package/.opencode/skills/frontend-design/SKILL.md +0 -115
- package/.opencode/skills/mafw-automation-interview/SKILL.md +0 -43
- package/.opencode/skills/mafw-cli-scanner/SKILL.md +0 -37
- package/.opencode/skills/mafw-compression-verifier/SKILL.md +0 -52
- package/.opencode/skills/mafw-desktop-inspector/SKILL.md +0 -170
- package/.opencode/skills/mafw-execute/SKILL.md +0 -62
- package/.opencode/skills/mafw-gateway-restart/SKILL.md +0 -87
- package/.opencode/skills/mafw-github-scanner/SKILL.md +0 -38
- package/.opencode/skills/mafw-goal/SKILL.md +0 -76
- package/.opencode/skills/mafw-interview/SKILL.md +0 -68
- package/.opencode/skills/mafw-memory-extractor/SKILL.md +0 -65
- package/.opencode/skills/mafw-plan/SKILL.md +0 -58
- package/.opencode/skills/mafw-review/SKILL.md +0 -85
- package/.opencode/skills/runtime-plugin-authoring/SKILL.md +0 -791
- package/.opencode/skills/slides/SKILL.md +0 -40
- package/.opencode/skills/slides/references/copywriting-formulas.md +0 -84
- package/.opencode/skills/slides/references/create.md +0 -4
- package/.opencode/skills/slides/references/html-template.md +0 -295
- package/.opencode/skills/slides/references/layout-patterns.md +0 -137
- package/.opencode/skills/slides/references/slide-strategies.md +0 -94
- package/.opencode/skills/ui-styling/LICENSE.txt +0 -202
- package/.opencode/skills/ui-styling/SKILL.md +0 -324
- package/.opencode/skills/ui-styling/references/canvas-design-system.md +0 -320
- package/.opencode/skills/ui-styling/references/shadcn-accessibility.md +0 -471
- package/.opencode/skills/ui-styling/references/shadcn-components.md +0 -424
- package/.opencode/skills/ui-styling/references/shadcn-theming.md +0 -373
- package/.opencode/skills/ui-styling/references/tailwind-customization.md +0 -483
- package/.opencode/skills/ui-styling/references/tailwind-responsive.md +0 -382
- package/.opencode/skills/ui-styling/references/tailwind-utilities.md +0 -455
- package/.opencode/skills/ui-styling/scripts/requirements.txt +0 -17
- package/.opencode/skills/ui-styling/scripts/shadcn_add.py +0 -308
- package/.opencode/skills/ui-styling/scripts/tailwind_config_gen.py +0 -473
- package/.opencode/skills/ui-styling/scripts/tests/coverage-ui.json +0 -1
- package/.opencode/skills/ui-styling/scripts/tests/requirements.txt +0 -3
- package/.opencode/skills/ui-styling/scripts/tests/test_shadcn_add.py +0 -266
- package/.opencode/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +0 -394
- package/.opencode/skills/ui-ux-pro-max/SKILL.md +0 -388
- package/.opencode/skills/ui-ux-pro-max/data/app-interface.csv +0 -31
- package/.opencode/skills/ui-ux-pro-max/data/charts.csv +0 -26
- package/.opencode/skills/ui-ux-pro-max/data/colors.csv +0 -193
- package/.opencode/skills/ui-ux-pro-max/data/google-fonts.csv +0 -1924
- package/.opencode/skills/ui-ux-pro-max/data/icons.csv +0 -106
- package/.opencode/skills/ui-ux-pro-max/data/landing.csv +0 -35
- package/.opencode/skills/ui-ux-pro-max/data/motion.csv +0 -17
- package/.opencode/skills/ui-ux-pro-max/data/products.csv +0 -193
- package/.opencode/skills/ui-ux-pro-max/data/react-performance.csv +0 -45
- package/.opencode/skills/ui-ux-pro-max/data/stacks/angular.csv +0 -51
- package/.opencode/skills/ui-ux-pro-max/data/stacks/astro.csv +0 -54
- package/.opencode/skills/ui-ux-pro-max/data/stacks/avalonia.csv +0 -57
- package/.opencode/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
- package/.opencode/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
- package/.opencode/skills/ui-ux-pro-max/data/stacks/javafx.csv +0 -76
- package/.opencode/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
- package/.opencode/skills/ui-ux-pro-max/data/stacks/laravel.csv +0 -51
- package/.opencode/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
- package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -71
- package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
- package/.opencode/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
- package/.opencode/skills/ui-ux-pro-max/data/stacks/react.csv +0 -54
- package/.opencode/skills/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
- package/.opencode/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
- package/.opencode/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
- package/.opencode/skills/ui-ux-pro-max/data/stacks/threejs.csv +0 -54
- package/.opencode/skills/ui-ux-pro-max/data/stacks/uno.csv +0 -60
- package/.opencode/skills/ui-ux-pro-max/data/stacks/uwp.csv +0 -56
- package/.opencode/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -50
- package/.opencode/skills/ui-ux-pro-max/data/stacks/winui.csv +0 -60
- package/.opencode/skills/ui-ux-pro-max/data/stacks/wpf.csv +0 -57
- package/.opencode/skills/ui-ux-pro-max/data/styles.csv +0 -85
- package/.opencode/skills/ui-ux-pro-max/data/typography.csv +0 -75
- package/.opencode/skills/ui-ux-pro-max/data/ui-reasoning.csv +0 -162
- package/.opencode/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
- package/.opencode/skills/ui-ux-pro-max/scripts/core.py +0 -464
- package/.opencode/skills/ui-ux-pro-max/scripts/design_system.py +0 -1479
- package/.opencode/skills/ui-ux-pro-max/scripts/search.py +0 -162
- package/.opencode/skills/ui-ux-pro-max/scripts/tests/test_core.py +0 -134
- package/.opencode/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +0 -159
- package/.opencode/skills/ui-ux-pro-max/scripts/validate_data.py +0 -114
- package/dist/hooks/bash-python-guide.d.ts +0 -4
- package/dist/hooks/bash-python-guide.d.ts.map +0 -1
- package/dist/hooks/bash-python-guide.js +0 -67
- package/dist/hooks/bash-python-guide.js.map +0 -1
- package/dist/hooks/handoff.d.ts +0 -9
- package/dist/hooks/handoff.d.ts.map +0 -1
- package/dist/hooks/handoff.js +0 -8
- package/dist/hooks/handoff.js.map +0 -1
- package/dist/hooks/hook-manager.d.ts +0 -32
- package/dist/hooks/hook-manager.d.ts.map +0 -1
- package/dist/hooks/hook-manager.js +0 -91
- package/dist/hooks/hook-manager.js.map +0 -1
- package/dist/hooks/llm-after.d.ts +0 -8
- package/dist/hooks/llm-after.d.ts.map +0 -1
- package/dist/hooks/llm-after.js +0 -8
- package/dist/hooks/llm-after.js.map +0 -1
- package/dist/hooks/media-ingest.d.ts +0 -43
- package/dist/hooks/media-ingest.d.ts.map +0 -1
- package/dist/hooks/media-ingest.js +0 -269
- package/dist/hooks/media-ingest.js.map +0 -1
- package/dist/hooks/memory-guide.d.ts +0 -3
- package/dist/hooks/memory-guide.d.ts.map +0 -1
- package/dist/hooks/memory-guide.js +0 -47
- package/dist/hooks/memory-guide.js.map +0 -1
- package/dist/hooks/observation-capture.d.ts +0 -17
- package/dist/hooks/observation-capture.d.ts.map +0 -1
- package/dist/hooks/observation-capture.js +0 -20
- package/dist/hooks/observation-capture.js.map +0 -1
- package/dist/hooks/session-compacting.d.ts +0 -6
- package/dist/hooks/session-compacting.d.ts.map +0 -1
- package/dist/hooks/session-compacting.js +0 -6
- package/dist/hooks/session-compacting.js.map +0 -1
- package/dist/hooks/session-ending.d.ts +0 -20
- package/dist/hooks/session-ending.d.ts.map +0 -1
- package/dist/hooks/session-ending.js +0 -113
- package/dist/hooks/session-ending.js.map +0 -1
- package/dist/hooks/session-recall.d.ts +0 -2
- package/dist/hooks/session-recall.d.ts.map +0 -1
- package/dist/hooks/session-recall.js +0 -103
- package/dist/hooks/session-recall.js.map +0 -1
- package/dist/hooks/session-start.d.ts +0 -9
- package/dist/hooks/session-start.d.ts.map +0 -1
- package/dist/hooks/session-start.js +0 -42
- package/dist/hooks/session-start.js.map +0 -1
- package/dist/hooks/tool-before.d.ts +0 -10
- package/dist/hooks/tool-before.d.ts.map +0 -1
- package/dist/hooks/tool-before.js +0 -6
- package/dist/hooks/tool-before.js.map +0 -1
- package/dist/hooks/tool-executed.d.ts +0 -19
- package/dist/hooks/tool-executed.d.ts.map +0 -1
- package/dist/hooks/tool-executed.js +0 -12
- package/dist/hooks/tool-executed.js.map +0 -1
- package/dist/hooks/user-profile.d.ts +0 -2
- package/dist/hooks/user-profile.d.ts.map +0 -1
- package/dist/hooks/user-profile.js +0 -30
- package/dist/hooks/user-profile.js.map +0 -1
- package/dist/hooks/user-prompt.d.ts +0 -9
- package/dist/hooks/user-prompt.d.ts.map +0 -1
- package/dist/hooks/user-prompt.js +0 -8
- package/dist/hooks/user-prompt.js.map +0 -1
- package/dist/hooks/voice-guide.d.ts +0 -8
- package/dist/hooks/voice-guide.d.ts.map +0 -1
- package/dist/hooks/voice-guide.js +0 -101
- package/dist/hooks/voice-guide.js.map +0 -1
- package/dist/plugin.d.ts +0 -55
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -342
- package/dist/plugin.js.map +0 -1
- package/dist/tools/add-memory.d.ts +0 -17
- package/dist/tools/add-memory.d.ts.map +0 -1
- package/dist/tools/add-memory.js +0 -60
- package/dist/tools/add-memory.js.map +0 -1
- package/dist/tools/media-ask.d.ts +0 -14
- package/dist/tools/media-ask.d.ts.map +0 -1
- package/dist/tools/media-ask.js +0 -224
- package/dist/tools/media-ask.js.map +0 -1
- package/dist/tools/media-speak.d.ts +0 -16
- package/dist/tools/media-speak.d.ts.map +0 -1
- package/dist/tools/media-speak.js +0 -78
- package/dist/tools/media-speak.js.map +0 -1
- package/dist/tools/media-upload.d.ts +0 -14
- package/dist/tools/media-upload.d.ts.map +0 -1
- package/dist/tools/media-upload.js +0 -155
- package/dist/tools/media-upload.js.map +0 -1
- package/dist/tools/python-exec.d.ts +0 -13
- package/dist/tools/python-exec.d.ts.map +0 -1
- package/dist/tools/python-exec.js +0 -99
- package/dist/tools/python-exec.js.map +0 -1
- package/dist/tools/python-restart.d.ts +0 -11
- package/dist/tools/python-restart.d.ts.map +0 -1
- package/dist/tools/python-restart.js +0 -33
- package/dist/tools/python-restart.js.map +0 -1
- package/dist/utils/circuit-breaker.d.ts +0 -23
- package/dist/utils/circuit-breaker.d.ts.map +0 -1
- package/dist/utils/circuit-breaker.js +0 -75
- package/dist/utils/circuit-breaker.js.map +0 -1
- package/dist/utils/config-loader.d.ts +0 -20
- package/dist/utils/config-loader.d.ts.map +0 -1
- package/dist/utils/config-loader.js +0 -194
- package/dist/utils/config-loader.js.map +0 -1
- package/dist/utils/fallback.d.ts +0 -7
- package/dist/utils/fallback.d.ts.map +0 -1
- package/dist/utils/fallback.js +0 -20
- package/dist/utils/fallback.js.map +0 -1
- package/dist/utils/git.d.ts +0 -24
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js +0 -71
- package/dist/utils/git.js.map +0 -1
- package/dist/utils/github.d.ts +0 -37
- package/dist/utils/github.d.ts.map +0 -1
- package/dist/utils/github.js +0 -53
- package/dist/utils/github.js.map +0 -1
- package/dist/utils/global-path.d.ts +0 -2
- package/dist/utils/global-path.d.ts.map +0 -1
- package/dist/utils/global-path.js +0 -32
- package/dist/utils/global-path.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -69
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/obs-capture.d.ts +0 -7
- package/dist/utils/obs-capture.d.ts.map +0 -1
- package/dist/utils/obs-capture.js +0 -49
- package/dist/utils/obs-capture.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -11
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/retry.js +0 -45
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/self-wiring.d.ts +0 -13
- package/dist/utils/self-wiring.d.ts.map +0 -1
- package/dist/utils/self-wiring.js +0 -86
- package/dist/utils/self-wiring.js.map +0 -1
- package/dist/utils/state.d.ts +0 -106
- package/dist/utils/state.d.ts.map +0 -1
- package/dist/utils/state.js +0 -166
- package/dist/utils/state.js.map +0 -1
- package/dist/utils/status.d.ts +0 -47
- package/dist/utils/status.d.ts.map +0 -1
- package/dist/utils/status.js +0 -147
- package/dist/utils/status.js.map +0 -1
- package/dist/utils/ttl-map.d.ts +0 -17
- package/dist/utils/ttl-map.d.ts.map +0 -1
- package/dist/utils/ttl-map.js +0 -67
- package/dist/utils/ttl-map.js.map +0 -1
|
@@ -1,791 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: runtime-plugin-authoring
|
|
3
|
-
description: 为 MAFW Gateway 编写 Runtime 插件的完整指南。覆盖能力契约(Tier 0/1/2)、CJS 插件格式、事件归一化、可选接口、激活与测试。当需要接入新的 agent runtime(如 pi-coding-agent、claude、自定义 LLM 服务)时使用此 skill。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# MAFW Runtime Plugin 编写指南
|
|
7
|
-
|
|
8
|
-
## 概述
|
|
9
|
-
|
|
10
|
-
MAFW Gateway 通过**能力契约**(Runtime Capability Contract)与 agent runtime 解耦。插件是一个 CJS `.js` 文件,放在 `~/.mafw/runtime-plugins/` 目录下,声明自己支持的能力等级,gateway 按能力集自动开关功能。
|
|
11
|
-
|
|
12
|
-
**核心原则:**
|
|
13
|
-
- **能力自声明**:插件声明能力,gateway 按能力降级(缺能力 → 503 或跳过,永不崩溃)
|
|
14
|
-
- **Fail-open**:插件加载/运行失败 → 自动回退到内置 opencode runtime
|
|
15
|
-
- **运行时热切换**:`POST /api/runtime/switch` 可在进程内热切换 runtime(无需重启);插件文件修改后需 `POST /api/runtime/reload` 重扫或重启 gateway
|
|
16
|
-
|
|
17
|
-
## 第一步:理解能力分级
|
|
18
|
-
|
|
19
|
-
### Tier 0(基线,所有插件自动获得)
|
|
20
|
-
| 能力 | 说明 |
|
|
21
|
-
|------|------|
|
|
22
|
-
| `sessionApi` | 会话 CRUD(create/prompt/messages/get/delete/abort/list) |
|
|
23
|
-
| `promptWhileBusy` | 会话忙碌时仍可追加输入(promptAsync) |
|
|
24
|
-
|
|
25
|
-
Tier 0 是 `minimalCapabilities()` 默认值,插件无需声明即可获得。
|
|
26
|
-
|
|
27
|
-
### Tier 1(自治执行)
|
|
28
|
-
| 能力 | 说明 | 缺省行为 |
|
|
29
|
-
|------|------|----------|
|
|
30
|
-
| `eventStream` | SSE 事件流订阅 | 跳过事件订阅,无自治触发 |
|
|
31
|
-
| `nativeApprovals` | 原生审批 UI | 4 个审批端点返回 503 |
|
|
32
|
-
| `providerConfigApi` | Provider 配置管理 | 4 个 provider 端点返回 503 |
|
|
33
|
-
| `perLlmCallTransform` | 每次 LLM 调用的 transform | 跳过 transform 注入 |
|
|
34
|
-
|
|
35
|
-
### Tier 2(桌面完整)
|
|
36
|
-
| 能力 | 说明 | 缺省行为 |
|
|
37
|
-
|------|------|----------|
|
|
38
|
-
| `sessionStorageApi` | 直读 runtime 私有存储列出会话 | 回退 `session.list` + 客户端过滤 |
|
|
39
|
-
| `agentConfigApi` | Agent 定义安装 | Manager agent 安装跳过(warn 日志) |
|
|
40
|
-
|
|
41
|
-
**选择指南:**
|
|
42
|
-
- 仅协作对话 → Tier 0 即可
|
|
43
|
-
- 需要自动化/事件驱动 → 加 `eventStream`(Tier 1)
|
|
44
|
-
- 桌面聊天完整体验 → 加 Tier 2 能力
|
|
45
|
-
|
|
46
|
-
## 第二步:编写插件文件
|
|
47
|
-
|
|
48
|
-
### 文件位置
|
|
49
|
-
```
|
|
50
|
-
~/.mafw/runtime-plugins/my-runtime.js
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### CJS module.exports 形状
|
|
54
|
-
|
|
55
|
-
```javascript
|
|
56
|
-
// ~/.mafw/runtime-plugins/my-runtime.js
|
|
57
|
-
module.exports = {
|
|
58
|
-
// 必需:唯一标识符(用于 config.yaml 激活)
|
|
59
|
-
name: "my-runtime",
|
|
60
|
-
|
|
61
|
-
// 可选:声明超出 Tier-0 基线的能力(与 minimalCapabilities() 合并)
|
|
62
|
-
capabilities: {
|
|
63
|
-
eventStream: true, // Tier 1:需要事件流
|
|
64
|
-
nativeApprovals: false, // 不需要原生审批
|
|
65
|
-
providerConfigApi: false, // 不需要 provider 管理
|
|
66
|
-
perLlmCallTransform: false,
|
|
67
|
-
// 可选能力(不在 Tier 分级内):
|
|
68
|
-
sessionStorageApi: false, // 无直读存储
|
|
69
|
-
agentConfigApi: false, // 无 agent 安装
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
// 可选:默认 true(gateway 不 spawn 进程)
|
|
73
|
-
// 设为 false 仅当你需要 gateway 启动/监管 runtime 进程
|
|
74
|
-
external: true,
|
|
75
|
-
|
|
76
|
-
// 必需:工厂函数,接收 RuntimePluginContext,返回 AgentRuntime
|
|
77
|
-
async createRuntime(ctx) {
|
|
78
|
-
// ctx 提供的工具:
|
|
79
|
-
// - ctx.fetch(url, opts) — 带 60s 默认超时的 fetch
|
|
80
|
-
// - ctx.log — gateway 日志器
|
|
81
|
-
// - ctx.pluginConfig(name) — 读取 config.yaml 的 pluginConfig 段
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
name: "my-runtime",
|
|
85
|
-
capabilities: { /* 同上 */ },
|
|
86
|
-
|
|
87
|
-
// ─── 必需:会话 API(Tier 0)──────────────────────
|
|
88
|
-
session: {
|
|
89
|
-
async create(opts) {
|
|
90
|
-
// opts: { directory?: string }
|
|
91
|
-
// 返回: { id: string, ... }
|
|
92
|
-
const res = await ctx.fetch('http://localhost:8080/sessions', {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { 'Content-Type': 'application/json' },
|
|
95
|
-
body: JSON.stringify(opts),
|
|
96
|
-
});
|
|
97
|
-
return res.json();
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
async promptAsync(opts) {
|
|
101
|
-
// opts: { sessionID, parts?, message?, agent?, model?, variant?, system?, noReply? }
|
|
102
|
-
// 返回: void 或 { error?, response? }
|
|
103
|
-
await ctx.fetch(`http://localhost:8080/sessions/${opts.sessionID}/prompt`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: { 'Content-Type': 'application/json' },
|
|
106
|
-
body: JSON.stringify(opts),
|
|
107
|
-
});
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
async prompt(opts) {
|
|
111
|
-
// 同步等待回复
|
|
112
|
-
// 返回: { parts: any[], ... }
|
|
113
|
-
const res = await ctx.fetch(`http://localhost:8080/sessions/${opts.sessionID}/prompt`, {
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: { 'Content-Type': 'application/json' },
|
|
116
|
-
body: JSON.stringify(opts),
|
|
117
|
-
});
|
|
118
|
-
return res.json();
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
async messages(opts) {
|
|
122
|
-
// opts: { sessionID, limit?, before? }
|
|
123
|
-
// 返回: { data: any[], nextCursor?: string }
|
|
124
|
-
const res = await ctx.fetch(
|
|
125
|
-
`http://localhost:8080/sessions/${opts.sessionID}/messages?limit=${opts.limit || 50}`
|
|
126
|
-
);
|
|
127
|
-
return res.json();
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
async get({ sessionID }) {
|
|
131
|
-
const res = await ctx.fetch(`http://localhost:8080/sessions/${sessionID}`);
|
|
132
|
-
return res.json();
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
async delete({ sessionID }) {
|
|
136
|
-
await ctx.fetch(`http://localhost:8080/sessions/${sessionID}`, { method: 'DELETE' });
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
async abort({ sessionID }) {
|
|
140
|
-
await ctx.fetch(`http://localhost:8080/sessions/${sessionID}/abort`, { method: 'POST' });
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
async list(opts) {
|
|
144
|
-
const res = await ctx.fetch('http://localhost:8080/sessions');
|
|
145
|
-
return res.json();
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
async todo({ sessionID }) {
|
|
149
|
-
// 返回: any[](待办事项列表)
|
|
150
|
-
return [];
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
async children({ sessionID }) {
|
|
154
|
-
// 返回: any[](子会话列表)
|
|
155
|
-
return [];
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
async summarize(opts) {
|
|
159
|
-
// opts: { sessionID, providerID?, modelID? }
|
|
160
|
-
// 返回: 压缩后的会话摘要
|
|
161
|
-
const res = await ctx.fetch(
|
|
162
|
-
`http://localhost:8080/sessions/${opts.sessionID}/summarize`,
|
|
163
|
-
{ method: 'POST' }
|
|
164
|
-
);
|
|
165
|
-
return res.json();
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
// 可选:需要 sessionStorageApi 能力
|
|
169
|
-
// async listByDirectory(directory, limit) { return []; },
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
// ─── 必需:事件流(Tier 1,若声明 eventStream)───
|
|
173
|
-
global: {
|
|
174
|
-
async event() {
|
|
175
|
-
// 返回: { stream: AsyncIterable<RawRuntimeEvent> }
|
|
176
|
-
// 见下方"事件归一化"章节
|
|
177
|
-
return { stream: createEventStream() };
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
// ─── 必需:Provider 与配置 ────────────────────────
|
|
182
|
-
provider: {
|
|
183
|
-
async list() {
|
|
184
|
-
return { all: [], connected: [], default: {} };
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
app: {
|
|
189
|
-
async agents() {
|
|
190
|
-
return [];
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
config: {
|
|
195
|
-
async get() { return {}; },
|
|
196
|
-
async update(c) { return c; },
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
// ─── 必需:Base URL ──────────────────────────────
|
|
200
|
-
getBaseUrl() {
|
|
201
|
-
return "http://127.0.0.1:8080";
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
// ─── 可选:健康检查 ──────────────────────────────
|
|
205
|
-
async healthCheck() {
|
|
206
|
-
try {
|
|
207
|
-
const res = await ctx.fetch('http://localhost:8080/health', {
|
|
208
|
-
signal: AbortSignal.timeout(3000),
|
|
209
|
-
});
|
|
210
|
-
return res.ok;
|
|
211
|
-
} catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
// ─── 可选:凭据获取 ──────────────────────────────
|
|
217
|
-
// credentials: {
|
|
218
|
-
// getApiKey(provider) { return process.env[`${provider.toUpperCase()}_API_KEY`] || null; }
|
|
219
|
-
// },
|
|
220
|
-
|
|
221
|
-
// ─── 可选:Agent 定义安装(需 agentConfigApi 能力)
|
|
222
|
-
// agents: {
|
|
223
|
-
// async install(name, definition) { /* 写入配置文件 */ },
|
|
224
|
-
// async remove(name) { /* 删除配置 */ },
|
|
225
|
-
// },
|
|
226
|
-
};
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## 第三步:事件归一化
|
|
232
|
-
|
|
233
|
-
### 事件形状
|
|
234
|
-
|
|
235
|
-
Gateway 的事件归一化器 `normalizeOpencodeEvent()` 接受两种形状:
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
// 信封形状(GlobalEvent wrapper)
|
|
239
|
-
{ payload: { type: "string", properties: {...}, sessionID: "..." } }
|
|
240
|
-
|
|
241
|
-
// 扁平形状
|
|
242
|
-
{ type: "string", properties: {...}, sessionID: "..." }
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
**推荐:** 让你的 runtime 事件尽可能接近 opencode 事件形状,这样 `normalizeOpencodeEvent()` 可直接使用,无需写新归一化器。
|
|
246
|
-
|
|
247
|
-
### 关键事件类型(opencode 参考)
|
|
248
|
-
|
|
249
|
-
| 事件类型 | EventFacets 映射 | 说明 |
|
|
250
|
-
|----------|-----------------|------|
|
|
251
|
-
| `message.part.updated` | `step`(settled step)+ `chatSignal: 'delta'` | 流式文本输出 |
|
|
252
|
-
| `message.updated` | `step`(completed message)+ `chatSignal: 'complete'` | 消息完成 |
|
|
253
|
-
| `session.idle` | `chatSignal: 'complete'` + `broadcast: 'idle'` | 会话空闲 |
|
|
254
|
-
| `session.error` | `chatSignal: 'error'` + `broadcast: 'error'` | 会话错误 |
|
|
255
|
-
| `session.next.step.ended` | `step`(legacy 兜底) | 步骤结束(旧版) |
|
|
256
|
-
|
|
257
|
-
### EventFacets 正交切面
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
interface EventFacets {
|
|
261
|
-
type: string; // 原始类型(透传)
|
|
262
|
-
properties: any; // 原始属性(透传)
|
|
263
|
-
sessionID?: string;
|
|
264
|
-
directory?: string;
|
|
265
|
-
step: StepEndedProps | null; // 已结算的 LLM step
|
|
266
|
-
chatSignal: 'delta' | 'complete' | 'error' | null; // chat 信号
|
|
267
|
-
deltaText?: string;
|
|
268
|
-
chatError?: unknown;
|
|
269
|
-
broadcast: 'idle' | 'error' | 'passthrough'; // 全局广播
|
|
270
|
-
toolCommand?: string; // shell 命令(自更新定位用)
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### 自定义事件流
|
|
275
|
-
|
|
276
|
-
若你的 runtime 事件形状与 opencode 差异大,需要:
|
|
277
|
-
1. 写新归一化函数(如 `normalizeMyRuntimeEvent(evt): EventFacets`)
|
|
278
|
-
2. 修改 `gateway/src/index.ts` 的事件分发逻辑,根据 `runtimeName` 选择归一化器
|
|
279
|
-
|
|
280
|
-
**简单路径:** 让你的 runtime 发出 opencode 兼容事件,无需改 gateway 代码。
|
|
281
|
-
|
|
282
|
-
## 第四步:激活与测试
|
|
283
|
-
|
|
284
|
-
### 激活方式
|
|
285
|
-
|
|
286
|
-
**方式 A:config.yaml**
|
|
287
|
-
```yaml
|
|
288
|
-
# ~/.mafw/config.yaml
|
|
289
|
-
runtime:
|
|
290
|
-
plugin: my-runtime # 匹配 module.exports.name
|
|
291
|
-
pluginConfig:
|
|
292
|
-
my-runtime:
|
|
293
|
-
baseUrl: "http://localhost:8080"
|
|
294
|
-
apiKey: "xxx" # 通过 ctx.pluginConfig("my-runtime") 读取
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**方式 B:环境变量**
|
|
298
|
-
```bash
|
|
299
|
-
MAFW_RUNTIME_PLUGIN=my-runtime
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### 测试流程
|
|
303
|
-
|
|
304
|
-
1. **编写插件** → 保存为 `~/.mafw/runtime-plugins/my-runtime.js`
|
|
305
|
-
2. **重启 gateway** → `mafw restart` 或前台 `mafw start`
|
|
306
|
-
3. **检查加载状态** → `GET http://localhost:3000/api/runtime`
|
|
307
|
-
```json
|
|
308
|
-
{
|
|
309
|
-
"active": { "name": "my-runtime", "capabilities": {...} },
|
|
310
|
-
"plugins": [
|
|
311
|
-
{ "file": "my-runtime.js", "name": "my-runtime", "status": "ok", "capabilities": {...} }
|
|
312
|
-
]
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
4. **测试功能** → 创建会话、发送消息、验证事件流
|
|
316
|
-
|
|
317
|
-
### 常见错误
|
|
318
|
-
|
|
319
|
-
| 现象 | 原因 | 解决 |
|
|
320
|
-
|------|------|------|
|
|
321
|
-
| `status: "error", error: "missing name"` | 未导出 `name` 字段 | 添加 `name: "my-runtime"` |
|
|
322
|
-
| `status: "error", error: "missing createRuntime(ctx)"` | 未导出工厂函数 | 添加 `async createRuntime(ctx) {...}` |
|
|
323
|
-
| `status: "error", error: "duplicate name"` | 多个文件导出相同 `name` | 检查重复插件 |
|
|
324
|
-
| Gateway 仍用 opencode | 插件加载失败 / 未配置 | 检查 `/api/runtime` 返回;确认 `config.yaml` 的 `runtime.plugin` |
|
|
325
|
-
|
|
326
|
-
## 第五步:参考实现
|
|
327
|
-
|
|
328
|
-
### 内置 opencode runtime
|
|
329
|
-
|
|
330
|
-
`gateway/src/runtime/opencode-runtime.ts` 是完整的 Tier 2 参考实现:
|
|
331
|
-
- **能力声明**:`fullCapabilities()`(全满)
|
|
332
|
-
- **凭据**:`credentials.getApiKey()` 从 opencode auth.json 读取
|
|
333
|
-
- **sessionStorageApi**:`session.listByDirectory()` 直读 SQLite
|
|
334
|
-
- **agentConfigApi**:`agents.install()` 写 frontmatter markdown
|
|
335
|
-
- **事件流**:`global.event()` 返回 SSE stream
|
|
336
|
-
- **健康检查**:`healthCheck()` 探测 `/global/health`
|
|
337
|
-
|
|
338
|
-
### 最小可用插件(Tier 0)
|
|
339
|
-
|
|
340
|
-
```javascript
|
|
341
|
-
// ~/.mafw/runtime-plugins/minimal.js
|
|
342
|
-
module.exports = {
|
|
343
|
-
name: "minimal",
|
|
344
|
-
// 不声明额外能力 → 仅 Tier 0(sessionApi + promptWhileBusy)
|
|
345
|
-
async createRuntime(ctx) {
|
|
346
|
-
const baseUrl = ctx.pluginConfig("minimal").baseUrl || "http://localhost:8080";
|
|
347
|
-
return {
|
|
348
|
-
name: "minimal",
|
|
349
|
-
capabilities: {}, // Tier 0 only
|
|
350
|
-
session: {
|
|
351
|
-
async create(opts) {
|
|
352
|
-
const res = await ctx.fetch(`${baseUrl}/sessions`, { method: 'POST', body: JSON.stringify(opts) });
|
|
353
|
-
return res.json();
|
|
354
|
-
},
|
|
355
|
-
async promptAsync(opts) {
|
|
356
|
-
await ctx.fetch(`${baseUrl}/sessions/${opts.sessionID}/prompt`, {
|
|
357
|
-
method: 'POST', body: JSON.stringify(opts)
|
|
358
|
-
});
|
|
359
|
-
},
|
|
360
|
-
async prompt(opts) {
|
|
361
|
-
const res = await ctx.fetch(`${baseUrl}/sessions/${opts.sessionID}/prompt`, {
|
|
362
|
-
method: 'POST', body: JSON.stringify(opts)
|
|
363
|
-
});
|
|
364
|
-
return res.json();
|
|
365
|
-
},
|
|
366
|
-
async messages(opts) {
|
|
367
|
-
const res = await ctx.fetch(`${baseUrl}/sessions/${opts.sessionID}/messages?limit=${opts.limit || 50}`);
|
|
368
|
-
return res.json();
|
|
369
|
-
},
|
|
370
|
-
async get({ sessionID }) {
|
|
371
|
-
const res = await ctx.fetch(`${baseUrl}/sessions/${sessionID}`);
|
|
372
|
-
return res.json();
|
|
373
|
-
},
|
|
374
|
-
async delete({ sessionID }) {
|
|
375
|
-
await ctx.fetch(`${baseUrl}/sessions/${sessionID}`, { method: 'DELETE' });
|
|
376
|
-
},
|
|
377
|
-
async abort({ sessionID }) {
|
|
378
|
-
await ctx.fetch(`${baseUrl}/sessions/${sessionID}/abort`, { method: 'POST' });
|
|
379
|
-
},
|
|
380
|
-
async list() { return []; },
|
|
381
|
-
async todo({ sessionID }) { return []; },
|
|
382
|
-
async children({ sessionID }) { return []; },
|
|
383
|
-
async summarize(opts) { return {}; },
|
|
384
|
-
},
|
|
385
|
-
global: { async event() { return { stream: (async function*(){})() }; } },
|
|
386
|
-
provider: { async list() { return { all: [], connected: [], default: {} }; } },
|
|
387
|
-
app: { async agents() { return []; } },
|
|
388
|
-
config: { async get() { return {}; }, async update(c) { return c; } },
|
|
389
|
-
getBaseUrl() { return baseUrl; },
|
|
390
|
-
};
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
## 可选接口详解
|
|
396
|
-
|
|
397
|
-
### credentials(凭据获取)
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
interface RuntimeCredentials {
|
|
401
|
-
getApiKey(provider: string): string | null;
|
|
402
|
-
}
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
**用途:** Media Agent 等服务优先从 runtime credentials 获取 API key,回退到直读 opencode auth.json。
|
|
406
|
-
|
|
407
|
-
**示例:**
|
|
408
|
-
```javascript
|
|
409
|
-
credentials: {
|
|
410
|
-
getApiKey(provider) {
|
|
411
|
-
// 从环境变量、配置文件或密钥管理器读取
|
|
412
|
-
return process.env[`${provider.toUpperCase()}_API_KEY`] || null;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### agents(Agent 定义安装)
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
interface AgentInstaller {
|
|
421
|
-
install(name: string, definition: AgentDefinition): Promise<void>;
|
|
422
|
-
remove?(name: string): Promise<void>;
|
|
423
|
-
}
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
**用途:** Manager agent 通过此接口安装自定义 agent 定义到 runtime。
|
|
427
|
-
|
|
428
|
-
**AgentDefinition 形状:**
|
|
429
|
-
```typescript
|
|
430
|
-
interface AgentDefinition {
|
|
431
|
-
description: string;
|
|
432
|
-
mode?: 'primary' | 'subagent' | 'all';
|
|
433
|
-
model?: string;
|
|
434
|
-
temperature?: number;
|
|
435
|
-
color?: string;
|
|
436
|
-
systemPrompt: string;
|
|
437
|
-
permissions: AgentPermissions;
|
|
438
|
-
}
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
**示例:**
|
|
442
|
-
```javascript
|
|
443
|
-
agents: {
|
|
444
|
-
async install(name, definition) {
|
|
445
|
-
const configDir = path.join(os.homedir(), '.config', 'my-runtime', 'agents');
|
|
446
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
447
|
-
const filePath = path.join(configDir, `${name}.yaml`);
|
|
448
|
-
fs.writeFileSync(filePath, serializeToYaml(definition));
|
|
449
|
-
ctx.log.info(`Installed agent ${name} to ${filePath}`);
|
|
450
|
-
},
|
|
451
|
-
async remove(name) {
|
|
452
|
-
const filePath = path.join(os.homedir(), '.config', 'my-runtime', 'agents', `${name}.yaml`);
|
|
453
|
-
fs.unlinkSync(filePath);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### session.listByDirectory(按目录列出会话)
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
listByDirectory?(directory: string, limit?: number): Promise<SessionInfo[]>;
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
**用途:** 直读 runtime 私有存储(如 SQLite),按项目目录列出会话。解决 `session.list` 按 `project_id` 过滤时隐藏 worktree 会话的问题。
|
|
465
|
-
|
|
466
|
-
**SessionInfo 形状:**
|
|
467
|
-
```typescript
|
|
468
|
-
interface SessionInfo {
|
|
469
|
-
id: string;
|
|
470
|
-
projectID: string;
|
|
471
|
-
directory: string;
|
|
472
|
-
title: string;
|
|
473
|
-
metadata?: Record<string, unknown>;
|
|
474
|
-
time: { created: number; updated: number };
|
|
475
|
-
}
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
**示例:**
|
|
479
|
-
```javascript
|
|
480
|
-
session: {
|
|
481
|
-
// ...其他方法...
|
|
482
|
-
async listByDirectory(directory, limit = 200) {
|
|
483
|
-
// 直读 SQLite 或文件系统
|
|
484
|
-
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
485
|
-
const rows = db.prepare(
|
|
486
|
-
`SELECT id, project_id, directory, title, metadata, time_created, time_updated
|
|
487
|
-
FROM session WHERE directory LIKE ? ORDER BY time_updated DESC LIMIT ?`
|
|
488
|
-
).all(`${directory}%`, limit);
|
|
489
|
-
return rows.map(row => ({
|
|
490
|
-
id: row.id,
|
|
491
|
-
projectID: row.project_id,
|
|
492
|
-
directory: row.directory,
|
|
493
|
-
title: row.title,
|
|
494
|
-
metadata: row.metadata ?? undefined,
|
|
495
|
-
time: { created: row.time_created, updated: row.time_updated },
|
|
496
|
-
}));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
## 常见陷阱
|
|
502
|
-
|
|
503
|
-
### 1. 忘记刷新插件
|
|
504
|
-
修改 `.js` 文件后需要重扫或重启 gateway:
|
|
505
|
-
```bash
|
|
506
|
-
# 方式 A:热重扫(推荐,不中断服务)
|
|
507
|
-
curl -X POST http://localhost:3000/api/runtime/reload
|
|
508
|
-
# 然后切换到新插件
|
|
509
|
-
curl -X POST http://localhost:3000/api/runtime/switch -H 'Content-Type: application/json' -d '{"plugin":"my-runtime"}'
|
|
510
|
-
|
|
511
|
-
# 方式 B:重启 gateway
|
|
512
|
-
mafw restart
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
### 2. 能力声明与实际实现不匹配
|
|
516
|
-
声明了 `eventStream: true` 但 `global.event()` 未实现 → 事件订阅失败。
|
|
517
|
-
|
|
518
|
-
**规则:** 声明的能力必须有对应实现;未实现的能力声明为 `false`。
|
|
519
|
-
|
|
520
|
-
### 3. 事件形状不兼容
|
|
521
|
-
自定义事件形状与 `normalizeOpencodeEvent()` 不兼容 → 归一化失败。
|
|
522
|
-
|
|
523
|
-
**解决:**
|
|
524
|
-
- 优先让事件形状接近 opencode(见"事件归一化"章节)
|
|
525
|
-
- 或写新归一化器并修改 `index.ts` 的事件分发
|
|
526
|
-
|
|
527
|
-
### 4. 忽略 external 字段
|
|
528
|
-
`external: true`(默认)→ gateway 不 spawn 进程,仅做健康探测。
|
|
529
|
-
`external: false` → gateway 尝试 spawn/kill 进程(仅内置 opencode 使用)。
|
|
530
|
-
|
|
531
|
-
**规则:** 自定义插件保持 `external: true`(或不声明)。
|
|
532
|
-
|
|
533
|
-
### 5. pluginConfig 路径错误
|
|
534
|
-
`ctx.pluginConfig("my-runtime")` 读取 `config.yaml` 的 `runtime.pluginConfig.my-runtime` 段。
|
|
535
|
-
|
|
536
|
-
**正确配置:**
|
|
537
|
-
```yaml
|
|
538
|
-
runtime:
|
|
539
|
-
plugin: my-runtime
|
|
540
|
-
pluginConfig:
|
|
541
|
-
my-runtime: # 键名必须与 name 匹配
|
|
542
|
-
key: value
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
## 调试技巧
|
|
546
|
-
|
|
547
|
-
### 查看插件扫描状态
|
|
548
|
-
```bash
|
|
549
|
-
curl http://localhost:3000/api/runtime
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
返回示例:
|
|
553
|
-
```json
|
|
554
|
-
{
|
|
555
|
-
"active": {
|
|
556
|
-
"name": "my-runtime",
|
|
557
|
-
"capabilities": {
|
|
558
|
-
"sessionApi": true,
|
|
559
|
-
"promptWhileBusy": true,
|
|
560
|
-
"eventStream": true,
|
|
561
|
-
"nativeApprovals": false,
|
|
562
|
-
"providerConfigApi": false,
|
|
563
|
-
"perLlmCallTransform": false,
|
|
564
|
-
"sessionStorageApi": false,
|
|
565
|
-
"agentConfigApi": false
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
"plugins": [
|
|
569
|
-
{
|
|
570
|
-
"file": "my-runtime.js",
|
|
571
|
-
"name": "my-runtime",
|
|
572
|
-
"status": "ok",
|
|
573
|
-
"capabilities": {...}
|
|
574
|
-
}
|
|
575
|
-
]
|
|
576
|
-
}
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
### 查看 gateway 日志
|
|
580
|
-
```bash
|
|
581
|
-
mafw logs
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
关注:
|
|
585
|
-
- `[RuntimePluginLoader] Loaded my-runtime.js (my-runtime)` — 加载成功
|
|
586
|
-
- `[Runtime] using plugin runtime 'my-runtime'` — 激活成功
|
|
587
|
-
- `[Runtime] plugin 'my-runtime' createRuntime failed: ...` — 工厂函数异常
|
|
588
|
-
|
|
589
|
-
### 健康检查
|
|
590
|
-
```bash
|
|
591
|
-
curl http://localhost:3000/health
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
返回 `{"status":"ok"}` 表示 gateway 正常运行。若插件的 `healthCheck()` 返回 `false`,gateway 会记录警告日志。
|
|
595
|
-
|
|
596
|
-
## 完整示例:接入自定义 LLM 服务
|
|
597
|
-
|
|
598
|
-
```javascript
|
|
599
|
-
// ~/.mafw/runtime-plugins/custom-llm.js
|
|
600
|
-
const http = require('http');
|
|
601
|
-
|
|
602
|
-
module.exports = {
|
|
603
|
-
name: "custom-llm",
|
|
604
|
-
capabilities: {
|
|
605
|
-
eventStream: false, // 无实时事件流
|
|
606
|
-
nativeApprovals: false,
|
|
607
|
-
providerConfigApi: false,
|
|
608
|
-
perLlmCallTransform: false,
|
|
609
|
-
sessionStorageApi: false,
|
|
610
|
-
agentConfigApi: false,
|
|
611
|
-
},
|
|
612
|
-
external: true,
|
|
613
|
-
|
|
614
|
-
async createRuntime(ctx) {
|
|
615
|
-
const cfg = ctx.pluginConfig("custom-llm");
|
|
616
|
-
const baseUrl = cfg.baseUrl || "http://localhost:9000";
|
|
617
|
-
const apiKey = cfg.apiKey || process.env.CUSTOM_LLM_API_KEY;
|
|
618
|
-
|
|
619
|
-
const headers = apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {};
|
|
620
|
-
|
|
621
|
-
// 内存会话存储(生产环境应持久化)
|
|
622
|
-
const sessions = new Map();
|
|
623
|
-
|
|
624
|
-
return {
|
|
625
|
-
name: "custom-llm",
|
|
626
|
-
capabilities: {
|
|
627
|
-
eventStream: false,
|
|
628
|
-
nativeApprovals: false,
|
|
629
|
-
providerConfigApi: false,
|
|
630
|
-
perLlmCallTransform: false,
|
|
631
|
-
sessionStorageApi: false,
|
|
632
|
-
agentConfigApi: false,
|
|
633
|
-
},
|
|
634
|
-
|
|
635
|
-
session: {
|
|
636
|
-
async create(opts) {
|
|
637
|
-
const id = `sess_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
638
|
-
sessions.set(id, { id, directory: opts.directory, messages: [], created: Date.now() });
|
|
639
|
-
return { id };
|
|
640
|
-
},
|
|
641
|
-
|
|
642
|
-
async promptAsync(opts) {
|
|
643
|
-
const sess = sessions.get(opts.sessionID);
|
|
644
|
-
if (!sess) return { error: "session not found" };
|
|
645
|
-
|
|
646
|
-
const message = opts.message || opts.parts?.[0]?.text;
|
|
647
|
-
sess.messages.push({ role: 'user', content: message });
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
const res = await ctx.fetch(`${baseUrl}/v1/chat/completions`, {
|
|
651
|
-
method: 'POST',
|
|
652
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
653
|
-
body: JSON.stringify({
|
|
654
|
-
model: cfg.model || 'custom-model',
|
|
655
|
-
messages: sess.messages,
|
|
656
|
-
}),
|
|
657
|
-
});
|
|
658
|
-
const data = await res.json();
|
|
659
|
-
const reply = data.choices?.[0]?.message?.content || '';
|
|
660
|
-
sess.messages.push({ role: 'assistant', content: reply });
|
|
661
|
-
} catch (err) {
|
|
662
|
-
ctx.log.error(`[custom-llm] prompt failed: ${err.message}`);
|
|
663
|
-
}
|
|
664
|
-
},
|
|
665
|
-
|
|
666
|
-
async prompt(opts) {
|
|
667
|
-
await this.promptAsync(opts);
|
|
668
|
-
const sess = sessions.get(opts.sessionID);
|
|
669
|
-
const lastMsg = sess?.messages[sess.messages.length - 1];
|
|
670
|
-
return { parts: [{ type: 'text', text: lastMsg?.content || '' }] };
|
|
671
|
-
},
|
|
672
|
-
|
|
673
|
-
async messages(opts) {
|
|
674
|
-
const sess = sessions.get(opts.sessionID);
|
|
675
|
-
if (!sess) return { data: [] };
|
|
676
|
-
return {
|
|
677
|
-
data: sess.messages.map((m, i) => ({
|
|
678
|
-
id: `${opts.sessionID}_${i}`,
|
|
679
|
-
role: m.role,
|
|
680
|
-
parts: [{ type: 'text', text: m.content }],
|
|
681
|
-
})),
|
|
682
|
-
};
|
|
683
|
-
},
|
|
684
|
-
|
|
685
|
-
async get({ sessionID }) {
|
|
686
|
-
return sessions.get(sessionID) || null;
|
|
687
|
-
},
|
|
688
|
-
|
|
689
|
-
async delete({ sessionID }) {
|
|
690
|
-
sessions.delete(sessionID);
|
|
691
|
-
},
|
|
692
|
-
|
|
693
|
-
async abort({ sessionID }) {
|
|
694
|
-
// 无长时间任务,忽略
|
|
695
|
-
},
|
|
696
|
-
|
|
697
|
-
async list() {
|
|
698
|
-
return [...sessions.values()].map(s => ({
|
|
699
|
-
id: s.id,
|
|
700
|
-
title: s.messages[0]?.content?.slice(0, 50) || 'New session',
|
|
701
|
-
time: { created: s.created, updated: Date.now() },
|
|
702
|
-
}));
|
|
703
|
-
},
|
|
704
|
-
|
|
705
|
-
async todo({ sessionID }) { return []; },
|
|
706
|
-
async children({ sessionID }) { return []; },
|
|
707
|
-
async summarize(opts) { return {}; },
|
|
708
|
-
},
|
|
709
|
-
|
|
710
|
-
global: {
|
|
711
|
-
async event() {
|
|
712
|
-
// 无事件流,返回空流
|
|
713
|
-
return { stream: (async function*(){})() };
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
|
|
717
|
-
provider: {
|
|
718
|
-
async list() {
|
|
719
|
-
return { all: ['custom-llm'], connected: ['custom-llm'], default: { chat: 'custom-llm' } };
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
|
|
723
|
-
app: { async agents() { return []; } },
|
|
724
|
-
config: { async get() { return {}; }, async update(c) { return c; } },
|
|
725
|
-
|
|
726
|
-
getBaseUrl() { return baseUrl; },
|
|
727
|
-
|
|
728
|
-
async healthCheck() {
|
|
729
|
-
try {
|
|
730
|
-
const res = await ctx.fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(3000) });
|
|
731
|
-
return res.ok;
|
|
732
|
-
} catch {
|
|
733
|
-
return false;
|
|
734
|
-
}
|
|
735
|
-
},
|
|
736
|
-
|
|
737
|
-
credentials: {
|
|
738
|
-
getApiKey(provider) {
|
|
739
|
-
if (provider === 'custom-llm') return apiKey;
|
|
740
|
-
return null;
|
|
741
|
-
},
|
|
742
|
-
},
|
|
743
|
-
};
|
|
744
|
-
},
|
|
745
|
-
};
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
**激活:**
|
|
749
|
-
```yaml
|
|
750
|
-
# ~/.mafw/config.yaml
|
|
751
|
-
runtime:
|
|
752
|
-
plugin: custom-llm
|
|
753
|
-
pluginConfig:
|
|
754
|
-
custom-llm:
|
|
755
|
-
baseUrl: "http://localhost:9000"
|
|
756
|
-
apiKey: "sk-xxx"
|
|
757
|
-
model: "custom-model-v1"
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
```bash
|
|
761
|
-
mafw restart
|
|
762
|
-
curl http://localhost:3000/api/runtime
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
## 架构文档
|
|
766
|
-
|
|
767
|
-
- **契约定义**:`gateway/src/runtime/contract.ts`
|
|
768
|
-
- **插件加载器**:`gateway/src/runtime/loader.ts`
|
|
769
|
-
- **事件归一化**:`gateway/src/runtime/normalize.ts`
|
|
770
|
-
- **参考实现**:`gateway/src/runtime/opencode-runtime.ts`
|
|
771
|
-
- **Agent 定义模型**:`gateway/src/runtime/agent-definition.ts`
|
|
772
|
-
- **Gateway 激活逻辑**:`gateway/src/index.ts:814`(`createRuntime()` 方法)
|
|
773
|
-
- **能力守卫**:`gateway/src/index.ts:800`(`capGuard()` 方法)
|
|
774
|
-
|
|
775
|
-
## 总结
|
|
776
|
-
|
|
777
|
-
编写 MAFW runtime 插件的核心步骤:
|
|
778
|
-
|
|
779
|
-
1. **理解能力分级**(Tier 0/1/2),选择需要的能力
|
|
780
|
-
2. **编写 CJS 插件**(`module.exports`),声明能力 + 实现 `createRuntime(ctx)`
|
|
781
|
-
3. **处理事件归一化**(优先兼容 opencode 事件形状)
|
|
782
|
-
4. **激活与测试**(config.yaml 或环境变量,热切换或重启 gateway,检查 `/api/runtime`)
|
|
783
|
-
5. **参考内置实现**(`opencode-runtime.ts` 是完整的 Tier 2 参考)
|
|
784
|
-
|
|
785
|
-
**关键原则:**
|
|
786
|
-
- 能力自声明 + fail-open 降级
|
|
787
|
-
- 插件文件修改后需 `POST /api/runtime/reload` 重扫或重启 gateway;运行时切换可热切换
|
|
788
|
-
- 插件失败自动回退 opencode
|
|
789
|
-
- 事件形状尽量兼容 opencode 归一化器
|
|
790
|
-
|
|
791
|
-
遵循这些原则,你的 runtime 插件可以无缝接入 MAFW gateway,享受记忆系统、自动化、桌面 UI 等全套功能。
|