@jack200714/mafw 4.5.1 → 4.8.0
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/gateway/dist/index.js +52 -38
- package/gateway/dist/media/media-plugin-loader.js +0 -12
- package/gateway/dist/plugins/hub.js +153 -19
- package/gateway/dist/routes/plugins.js +15 -5
- package/gateway/dist/runtime/event-broadcast.js +33 -0
- package/gateway/dist/runtime/loader.js +23 -13
- package/gateway/dist/usage/builtin-plugins/gateway.js +25 -3
- package/gateway/dist/usage/plugin-loader.js +0 -11
- package/gateway/package.json +2 -2
- package/package.json +1 -7
- package/packages/tui/dist/cli.js +8 -3
- 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
package/README.md
CHANGED
|
@@ -146,14 +146,17 @@ npx electron-vite build
|
|
|
146
146
|
|
|
147
147
|
### 3. opencode 接入(可选)
|
|
148
148
|
|
|
149
|
+
> npm 包仅含 **Gateway + TUI**(`bin: mafw`)。opencode 插件入口(`dist/plugin.js`)不在
|
|
150
|
+
> npm 包内——插件方式接入请从源码构建(`npm run build` 后本地安装 tgz),或仅用下方
|
|
151
|
+
> MCP remote 接线(无需插件)。
|
|
152
|
+
|
|
149
153
|
```json
|
|
150
154
|
{
|
|
151
|
-
"plugin": ["@jack200714/mafw"],
|
|
152
155
|
"mcp": { "mafw": { "type": "remote", "url": "http://127.0.0.1:3000/mcp", "enabled": true } }
|
|
153
156
|
}
|
|
154
157
|
```
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
源码构建后插件激活时会自动补写缺失的 MCP 接线并备份原配置(fail-open)。
|
|
157
160
|
|
|
158
161
|
## 仓库结构
|
|
159
162
|
|
package/bin/mafw.js
CHANGED
|
@@ -125,8 +125,23 @@ function startGateway(background) {
|
|
|
125
125
|
process.exit(1);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Background mode stdio: stdin/stdout 'ignore' (devnull), stderr redirected
|
|
129
|
+
// straight to a file fd. All three must avoid live pipes: a pipe's write end
|
|
130
|
+
// breaks once this short-lived CLI exits, and any later console.log in the
|
|
131
|
+
// gateway (e.g. mdns-advertiser) then crashes it with an unhandled EPIPE; a
|
|
132
|
+
// piped stderr would also keep THIS CLI's event loop alive and make
|
|
133
|
+
// `mafw daemon` hang. File fd = crash traces persist with zero plumbing.
|
|
134
|
+
let stdio = background ? ['ignore', 'ignore', 'ignore'] : 'inherit';
|
|
135
|
+
if (background) {
|
|
136
|
+
try {
|
|
137
|
+
const errLog = path.join(LOG_DIR, 'gateway-stderr.log');
|
|
138
|
+
try { if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true }); } catch {}
|
|
139
|
+
stdio = ['ignore', 'ignore', fs.openSync(errLog, 'a')];
|
|
140
|
+
} catch { /* fall back to full ignore */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
const child = spawn(process.execPath, [GATEWAY_SCRIPT], {
|
|
129
|
-
stdio
|
|
144
|
+
stdio,
|
|
130
145
|
detached: background,
|
|
131
146
|
windowsHide: true,
|
|
132
147
|
});
|
|
@@ -134,19 +149,6 @@ function startGateway(background) {
|
|
|
134
149
|
writePid(child.pid);
|
|
135
150
|
|
|
136
151
|
if (background) {
|
|
137
|
-
// Diagnostics: capture stderr to a file (a detached gateway that crashes
|
|
138
|
-
// silently otherwise leaves no trace). stdout is discarded; the gateway's
|
|
139
|
-
// own file logging covers normal logs.
|
|
140
|
-
// NOTE: use a raw fd + writeSync — a fs.WriteStream here never ends, which
|
|
141
|
-
// keeps the parent's event loop alive so `mafw daemon` hangs (and any
|
|
142
|
-
// tree-kill then takes the detached gateway down with it).
|
|
143
|
-
const errLog = path.join(LOG_DIR, 'gateway-stderr.log');
|
|
144
|
-
try { if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true }); } catch {}
|
|
145
|
-
const errFd = fs.openSync(errLog, 'a');
|
|
146
|
-
if (child.stderr) {
|
|
147
|
-
child.stderr.on('data', (chunk) => { try { fs.writeSync(errFd, chunk); } catch {} });
|
|
148
|
-
}
|
|
149
|
-
if (child.stdout) child.stdout.destroy();
|
|
150
152
|
child.unref();
|
|
151
153
|
console.log(`Gateway started in background (PID: ${child.pid})`);
|
|
152
154
|
console.log(`Logs: ${LOG_FILE}`);
|
package/gateway/dist/index.js
CHANGED
|
@@ -99,6 +99,7 @@ const self_update_1 = require("./self-update");
|
|
|
99
99
|
const step_inject_1 = require("./recall/step-inject");
|
|
100
100
|
const inject_format_1 = require("./recall/inject-format");
|
|
101
101
|
const normalize_1 = require("./runtime/normalize");
|
|
102
|
+
const event_broadcast_1 = require("./runtime/event-broadcast");
|
|
102
103
|
const budget_guard_1 = require("./core/budget-guard");
|
|
103
104
|
const goal_budget_1 = require("./core/goal-budget");
|
|
104
105
|
const contract_1 = require("./runtime/contract");
|
|
@@ -107,6 +108,7 @@ const pi_runtime_1 = require("./runtime/plugins/pi-runtime");
|
|
|
107
108
|
const permission_1 = require("./routes/permission");
|
|
108
109
|
const runtime_switch_1 = require("./routes/runtime-switch");
|
|
109
110
|
const plugins_1 = require("./routes/plugins");
|
|
111
|
+
const hub_1 = require("./plugins/hub");
|
|
110
112
|
const restart_agent_1 = require("./routes/restart-agent");
|
|
111
113
|
const session_mutations_1 = require("./routes/session-mutations");
|
|
112
114
|
const serve_supervisor_1 = require("./runtime/serve-supervisor");
|
|
@@ -157,6 +159,10 @@ class MafwScheduler {
|
|
|
157
159
|
serveOwned = false;
|
|
158
160
|
serveExitStreak = 0;
|
|
159
161
|
serveRecovering = false;
|
|
162
|
+
// External runtimes (pi) do not own serve: the watchdog can only warn and
|
|
163
|
+
// reconnect the event stream. Flag keeps that warning a one-shot ERROR per
|
|
164
|
+
// outage instead of an unbounded WARN loop.
|
|
165
|
+
serveExternalDownNotified = false;
|
|
160
166
|
serveWatchdogTimer = null;
|
|
161
167
|
serveRetryTimer = null;
|
|
162
168
|
serveStableTimer = null;
|
|
@@ -206,8 +212,6 @@ class MafwScheduler {
|
|
|
206
212
|
registeredProjects = new Map();
|
|
207
213
|
registryPath;
|
|
208
214
|
registryWriteQueue = Promise.resolve();
|
|
209
|
-
configPath;
|
|
210
|
-
configWriteQueue = Promise.resolve();
|
|
211
215
|
running = true;
|
|
212
216
|
// private dashboard?: DashboardServer;
|
|
213
217
|
mcpEndpoint;
|
|
@@ -252,7 +256,6 @@ class MafwScheduler {
|
|
|
252
256
|
this.serveUrl = config_1.config.server.serveUrl;
|
|
253
257
|
this.apiPort = config_1.config.server.apiPort;
|
|
254
258
|
this.pollInterval = config_1.config.timeouts.backupPollInterval;
|
|
255
|
-
this.configPath = config_1.config.paths.globalConfig;
|
|
256
259
|
this.registryPath = config_1.config.paths.registryFile;
|
|
257
260
|
this.chatSessions = new chat_sessions_1.ChatSessionManager();
|
|
258
261
|
this.serveSupervisor = (0, serve_supervisor_1.createServeSupervisor)({
|
|
@@ -423,8 +426,7 @@ class MafwScheduler {
|
|
|
423
426
|
// 4. Dashboard is now served via the API server on the same port
|
|
424
427
|
// this.dashboard = new DashboardServer(3001, this.projectDir, this);
|
|
425
428
|
// this.dashboard.start();
|
|
426
|
-
// 5.
|
|
427
|
-
await this.recoverConfig();
|
|
429
|
+
// 5. 恢复注册表(权威源:gateway DB kv_store + legacy 文件兜底)
|
|
428
430
|
await this.recoverRegistry();
|
|
429
431
|
// 5.0 Data-directory migration: move memory store + pipeline files from
|
|
430
432
|
// the previously-fixed gateway package .mafw (and any project-relative
|
|
@@ -773,7 +775,8 @@ class MafwScheduler {
|
|
|
773
775
|
registeredAt: new Date().toISOString()
|
|
774
776
|
});
|
|
775
777
|
this.persistRegistry();
|
|
776
|
-
|
|
778
|
+
// 桌面 renderer 依此事件刷新 Rail 项目列表(否则只在 gateway ready 时拉一次)。
|
|
779
|
+
this.broadcast((0, event_broadcast_1.projectRegisteredEvent)(projectDir));
|
|
777
780
|
logger_1.log.info(`[Scheduler] Project registered via filesystem: ${projectDir}`);
|
|
778
781
|
}
|
|
779
782
|
catch {
|
|
@@ -952,13 +955,15 @@ class MafwScheduler {
|
|
|
952
955
|
catch (err) {
|
|
953
956
|
logger_1.log.warn(`[Trajectory] idle aggregation failed (non-fatal): ${err.message}`);
|
|
954
957
|
}
|
|
955
|
-
this.broadcast(
|
|
958
|
+
this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type: 'message.complete', sessionID }, memoryWorker));
|
|
956
959
|
}
|
|
957
960
|
else if (f.broadcast === 'error') {
|
|
958
|
-
this.broadcast(
|
|
961
|
+
this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type: 'message.error', sessionID, error: props?.error instanceof Error ? props.error.message : String(props?.error ?? 'Unknown error') }, memoryWorker));
|
|
959
962
|
}
|
|
960
963
|
else {
|
|
961
|
-
|
|
964
|
+
// directory 透传:session.created/updated/deleted 的消费方(桌面 Rail)
|
|
965
|
+
// 据此定位所属项目做定向刷新(normalize 已从 GlobalEvent 信封提取)。
|
|
966
|
+
this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type, properties: props, sessionID, directory: f.directory }, memoryWorker));
|
|
962
967
|
}
|
|
963
968
|
}
|
|
964
969
|
/** 能力门:runtime 未声明该能力时以 503 显式拒绝(fail-open 的声明式降级)。 */
|
|
@@ -2122,6 +2127,10 @@ class MafwScheduler {
|
|
|
2122
2127
|
if (!this.running || this.serveRecovering)
|
|
2123
2128
|
return;
|
|
2124
2129
|
if (await this.isServeHealthy()) {
|
|
2130
|
+
if (failures > 0 || this.serveExternalDownNotified) {
|
|
2131
|
+
logger_1.log.info('[Scheduler] Serve back online');
|
|
2132
|
+
}
|
|
2133
|
+
this.serveExternalDownNotified = false;
|
|
2125
2134
|
failures = 0;
|
|
2126
2135
|
return;
|
|
2127
2136
|
}
|
|
@@ -2130,7 +2139,10 @@ class MafwScheduler {
|
|
|
2130
2139
|
if (failures >= this.serveWatchdogFailures) {
|
|
2131
2140
|
failures = 0;
|
|
2132
2141
|
if (this.opencodeClient?.external) {
|
|
2133
|
-
|
|
2142
|
+
if (!this.serveExternalDownNotified) {
|
|
2143
|
+
this.serveExternalDownNotified = true;
|
|
2144
|
+
logger_1.log.error('[Scheduler] External serve unreachable — the active runtime does not own the serve process, so it will NOT be respawned. Serve-dependent features (provider list, sessions, approvals) are degraded.');
|
|
2145
|
+
}
|
|
2134
2146
|
try {
|
|
2135
2147
|
await this.subscribeToEvents();
|
|
2136
2148
|
}
|
|
@@ -3600,7 +3612,8 @@ class MafwScheduler {
|
|
|
3600
3612
|
});
|
|
3601
3613
|
// 持久化到磁盘(写队列防并发覆盖)
|
|
3602
3614
|
await this.persistRegistry();
|
|
3603
|
-
|
|
3615
|
+
// 桌面 renderer 依此事件刷新 Rail 项目列表(否则只在 gateway ready 时拉一次)。
|
|
3616
|
+
this.broadcast((0, event_broadcast_1.projectRegisteredEvent)(projectDir));
|
|
3604
3617
|
if (this.opencodeClient) {
|
|
3605
3618
|
try {
|
|
3606
3619
|
await this.ensureManagerSession(projectDir, mafwDir);
|
|
@@ -4089,6 +4102,9 @@ class MafwScheduler {
|
|
|
4089
4102
|
if (this.automationEngine)
|
|
4090
4103
|
this.automationEngine.setRuntimeClient(rt);
|
|
4091
4104
|
await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
|
|
4105
|
+
// Desktop hint: a runtime switch swaps the session storage backend
|
|
4106
|
+
// (opencode SQLite vs pi), so cached session lists are stale.
|
|
4107
|
+
this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
|
|
4092
4108
|
if (prev && prev.dispose) {
|
|
4093
4109
|
try {
|
|
4094
4110
|
await prev.dispose();
|
|
@@ -4144,6 +4160,21 @@ class MafwScheduler {
|
|
|
4144
4160
|
usage: config_1.config.resolvePath('usage-plugins'),
|
|
4145
4161
|
ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
|
|
4146
4162
|
},
|
|
4163
|
+
builtinEntries: () => {
|
|
4164
|
+
const entries = [];
|
|
4165
|
+
const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
4166
|
+
entries.push(rt('opencode'));
|
|
4167
|
+
for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
|
|
4168
|
+
entries.push(rt(name));
|
|
4169
|
+
entries.push({ type: 'media', name: 'pi', file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
4170
|
+
const usageState = this.pluginLoader?.getState?.() ?? [];
|
|
4171
|
+
for (const s of usageState) {
|
|
4172
|
+
if (s.builtin && s.status === 'ok' && s.name) {
|
|
4173
|
+
entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
return entries;
|
|
4177
|
+
},
|
|
4147
4178
|
getErrors: (type) => {
|
|
4148
4179
|
const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
|
|
4149
4180
|
const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
|
|
@@ -4166,6 +4197,16 @@ class MafwScheduler {
|
|
|
4166
4197
|
},
|
|
4167
4198
|
},
|
|
4168
4199
|
};
|
|
4200
|
+
try {
|
|
4201
|
+
const cleaned = (0, hub_1.cleanupExamples)(pluginHubDeps.hub);
|
|
4202
|
+
if (cleaned.removed.length)
|
|
4203
|
+
logger_1.log.info(`[PluginsHub] removed stale examples: ${cleaned.removed.length}`);
|
|
4204
|
+
if (cleaned.failed.length)
|
|
4205
|
+
logger_1.log.warn(`[PluginsHub] cleanupExamples failed: ${cleaned.failed.join(', ')}`);
|
|
4206
|
+
}
|
|
4207
|
+
catch (err) {
|
|
4208
|
+
logger_1.log.warn(`[PluginsHub] cleanupExamples error: ${err.message}`);
|
|
4209
|
+
}
|
|
4169
4210
|
if (req.method === 'GET' && req.url?.match(/^\/api\/plugins(?:\?|$)/)) {
|
|
4170
4211
|
await (0, plugins_1.handlePluginsList)(req, res, pluginHubDeps);
|
|
4171
4212
|
return;
|
|
@@ -5433,33 +5474,6 @@ class MafwScheduler {
|
|
|
5433
5474
|
logger_1.log.info(`[Scheduler] Recovered ${filtered.length} registered projects`);
|
|
5434
5475
|
}
|
|
5435
5476
|
}
|
|
5436
|
-
async persistConfig() {
|
|
5437
|
-
this.configWriteQueue = this.configWriteQueue.then(async () => {
|
|
5438
|
-
const dir = path.dirname(this.configPath);
|
|
5439
|
-
if (!fs.existsSync(dir))
|
|
5440
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
5441
|
-
let config = {};
|
|
5442
|
-
if (fs.existsSync(this.configPath)) {
|
|
5443
|
-
config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
5444
|
-
}
|
|
5445
|
-
config.projects = Object.fromEntries(this.registeredProjects);
|
|
5446
|
-
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
5447
|
-
});
|
|
5448
|
-
await this.configWriteQueue;
|
|
5449
|
-
}
|
|
5450
|
-
async recoverConfig() {
|
|
5451
|
-
if (fs.existsSync(this.configPath)) {
|
|
5452
|
-
try {
|
|
5453
|
-
const config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
5454
|
-
const entries = Object.entries((config.projects || {}))
|
|
5455
|
-
.filter(([dir]) => !this.isUserDataDir(dir));
|
|
5456
|
-
this.registeredProjects = new Map(entries);
|
|
5457
|
-
}
|
|
5458
|
-
catch (err) {
|
|
5459
|
-
logger_1.log.error(`[Scheduler] Failed to recover config: ${err.message}`);
|
|
5460
|
-
}
|
|
5461
|
-
}
|
|
5462
|
-
}
|
|
5463
5477
|
// ── 4. 轮询(降级兜底 + autoresume) ──
|
|
5464
5478
|
startBackupPolling() {
|
|
5465
5479
|
const interval = config_1.config.timeouts.backupPollInterval;
|
|
@@ -60,7 +60,6 @@ class MediaPluginLoader {
|
|
|
60
60
|
if (!fs.existsSync(this.pluginsDir)) {
|
|
61
61
|
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
|
62
62
|
fs.writeFileSync(path.join(this.pluginsDir, 'README.md'), README_CONTENT);
|
|
63
|
-
fs.writeFileSync(path.join(this.pluginsDir, 'example.js.disabled'), EXAMPLE_CONTENT);
|
|
64
63
|
logger_1.log.info(`[MediaPluginLoader] Created ${this.pluginsDir}`);
|
|
65
64
|
}
|
|
66
65
|
}
|
|
@@ -241,14 +240,3 @@ media:
|
|
|
241
240
|
engine: qwen-vl # override for video
|
|
242
241
|
\`\`\`
|
|
243
242
|
`;
|
|
244
|
-
const EXAMPLE_CONTENT = `// Rename to example.js to activate
|
|
245
|
-
module.exports = {
|
|
246
|
-
name: "example",
|
|
247
|
-
modalities: ["image"],
|
|
248
|
-
async createPrompt(ctx) {
|
|
249
|
-
return async (parts, opts) => {
|
|
250
|
-
return "Example analysis result";
|
|
251
|
-
};
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
`;
|
|
@@ -36,11 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.HubError = void 0;
|
|
37
37
|
exports.parseType = parseType;
|
|
38
38
|
exports.listPlugins = listPlugins;
|
|
39
|
+
exports.cleanupExamples = cleanupExamples;
|
|
39
40
|
exports.installPlugin = installPlugin;
|
|
40
41
|
exports.setPluginEnabled = setPluginEnabled;
|
|
41
42
|
exports.deletePlugin = deletePlugin;
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
43
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const logger_1 = require("../core/utils/logger");
|
|
44
47
|
class HubError extends Error {
|
|
45
48
|
status;
|
|
46
49
|
constructor(status, message) {
|
|
@@ -87,6 +90,19 @@ function statEntry(type, dir, file) {
|
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
function listPlugins(deps) {
|
|
93
|
+
const userEntries = collectUserEntries(deps);
|
|
94
|
+
const builtins = (deps.builtinEntries?.() ?? []).map((e) => ({ ...e, builtin: true }));
|
|
95
|
+
const userKeys = new Set(userEntries.map((e) => `${e.type}/${e.name}`));
|
|
96
|
+
const configDisabled = deps.configDisabledUsage?.() ?? new Set();
|
|
97
|
+
for (const b of builtins) {
|
|
98
|
+
if (userKeys.has(`${b.type}/${b.name}`))
|
|
99
|
+
b.overridden = true;
|
|
100
|
+
if (b.type === 'usage' && configDisabled.has(b.name) && b.status === 'enabled')
|
|
101
|
+
b.status = 'config-disabled';
|
|
102
|
+
}
|
|
103
|
+
return [...builtins, ...userEntries];
|
|
104
|
+
}
|
|
105
|
+
function collectUserEntries(deps) {
|
|
90
106
|
const entries = [];
|
|
91
107
|
for (const type of PLUGIN_TYPES) {
|
|
92
108
|
const dir = deps.dirs[type];
|
|
@@ -130,24 +146,52 @@ function listPlugins(deps) {
|
|
|
130
146
|
}
|
|
131
147
|
return entries;
|
|
132
148
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
function cleanupExamples(deps) {
|
|
150
|
+
const removed = [];
|
|
151
|
+
const failed = [];
|
|
152
|
+
for (const dir of Object.values(deps.dirs)) {
|
|
153
|
+
const target = path.join(dir, 'example.js.disabled');
|
|
154
|
+
try {
|
|
155
|
+
if (fs.existsSync(target)) {
|
|
156
|
+
fs.unlinkSync(target);
|
|
157
|
+
removed.push(target);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
failed.push(target);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { removed, failed };
|
|
145
165
|
}
|
|
146
166
|
async function installPlugin(deps, input) {
|
|
147
|
-
const type = parseType(input.type);
|
|
148
167
|
const filename = validateFilename(input.filename);
|
|
149
168
|
if (!filename.endsWith('.js'))
|
|
150
169
|
throw new HubError(400, `install filename must end with .js: ${filename}`);
|
|
170
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
171
|
+
if (!Buffer.isBuffer(input.bytes) || input.bytes.length === 0)
|
|
172
|
+
throw new HubError(400, 'empty content');
|
|
173
|
+
if (input.bytes.length > maxBytes)
|
|
174
|
+
throw new HubError(413, `content exceeds ${maxBytes} bytes`);
|
|
175
|
+
const explicitType = input.type ? parseType(input.type) : undefined;
|
|
176
|
+
const { matches, mod } = inspectPlugin(filename, input.bytes);
|
|
177
|
+
validateName(mod, filename);
|
|
178
|
+
let type;
|
|
179
|
+
if (matches.length === 0)
|
|
180
|
+
throw new HubError(400, 'unrecognized plugin interface: export createRuntime / createPrompt / fetch / tools');
|
|
181
|
+
if (explicitType) {
|
|
182
|
+
if (!matches.includes(explicitType)) {
|
|
183
|
+
const listing = matches.map((m) => `${m} (${MATCH_IFACE[m]})`).join('/');
|
|
184
|
+
throw new HubError(400, `plugin interface mismatch: selected ${explicitType}, exports ${listing}`);
|
|
185
|
+
}
|
|
186
|
+
type = explicitType;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (matches.length > 1)
|
|
190
|
+
throw new HubError(400, `ambiguous plugin interface: ${matches.join('/')}`);
|
|
191
|
+
type = matches[0];
|
|
192
|
+
}
|
|
193
|
+
if (type === 'media')
|
|
194
|
+
await validateMediaActivation(mod);
|
|
151
195
|
const dir = deps.dirs[type];
|
|
152
196
|
fs.mkdirSync(dir, { recursive: true });
|
|
153
197
|
const target = resolveInDir(dir, filename);
|
|
@@ -156,23 +200,113 @@ async function installPlugin(deps, input) {
|
|
|
156
200
|
if (!input.overwrite && (fs.existsSync(dup) || fs.existsSync(dupDisabled))) {
|
|
157
201
|
throw new HubError(409, `plugin already exists: ${filename}`);
|
|
158
202
|
}
|
|
159
|
-
const buf = decodeContent(input.contentBase64, deps.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
160
203
|
const tmp = `${target}.tmp`;
|
|
161
|
-
fs.writeFileSync(tmp,
|
|
204
|
+
fs.writeFileSync(tmp, input.bytes);
|
|
162
205
|
fs.renameSync(tmp, target);
|
|
163
206
|
await deps.reload?.(type);
|
|
164
207
|
return statEntry(type, dir, filename);
|
|
165
208
|
}
|
|
209
|
+
const MATCH_IFACE = {
|
|
210
|
+
runtime: 'createRuntime',
|
|
211
|
+
media: 'createPrompt',
|
|
212
|
+
usage: 'fetch',
|
|
213
|
+
ui: 'tools',
|
|
214
|
+
};
|
|
215
|
+
function computeMatches(mod) {
|
|
216
|
+
const matches = [];
|
|
217
|
+
if (typeof mod?.createRuntime === 'function')
|
|
218
|
+
matches.push('runtime');
|
|
219
|
+
if (typeof mod?.createPrompt === 'function' || typeof mod?.fixPayload === 'function'
|
|
220
|
+
|| typeof mod?.engine === 'string' || Array.isArray(mod?.modalities))
|
|
221
|
+
matches.push('media');
|
|
222
|
+
if (typeof mod?.fetch === 'function'
|
|
223
|
+
&& (mod.type === undefined || mod.type === 'api' || mod.type === 'token-plan' || mod.type === 'local'))
|
|
224
|
+
matches.push('usage');
|
|
225
|
+
if (mod?.tools && typeof mod.tools === 'object' && Object.keys(mod.tools).length > 0)
|
|
226
|
+
matches.push('ui');
|
|
227
|
+
return matches;
|
|
228
|
+
}
|
|
229
|
+
function inspectPlugin(filename, bytes) {
|
|
230
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mafw-hub-sniff-'));
|
|
231
|
+
const tmpFile = path.join(tmpDir, filename);
|
|
232
|
+
try {
|
|
233
|
+
fs.writeFileSync(tmpFile, bytes);
|
|
234
|
+
try {
|
|
235
|
+
delete require.cache[require.resolve(tmpFile)];
|
|
236
|
+
}
|
|
237
|
+
catch { /* first load */ }
|
|
238
|
+
let mod;
|
|
239
|
+
try {
|
|
240
|
+
mod = require(tmpFile);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
throw new HubError(400, `plugin failed to load: ${err.message}`);
|
|
244
|
+
}
|
|
245
|
+
return { matches: computeMatches(mod), mod };
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
try {
|
|
249
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
catch { /* best effort */ }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function validateName(mod, filename) {
|
|
255
|
+
const name = mod?.name;
|
|
256
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
257
|
+
throw new HubError(400, 'missing plugin name');
|
|
258
|
+
const stem = baseName(filename);
|
|
259
|
+
if (name !== stem) {
|
|
260
|
+
throw new HubError(400, `plugin name mismatch: exports '${name}', filename '${filename}' (must match)`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const VALID_MODALITIES = new Set(['image', 'video', 'audio']);
|
|
264
|
+
const STUB_MEDIA_CTX = {
|
|
265
|
+
apiKey: () => null,
|
|
266
|
+
fetch: (url, opts) => fetch(url, opts),
|
|
267
|
+
pluginConfig: () => null,
|
|
268
|
+
log: logger_1.log,
|
|
269
|
+
};
|
|
270
|
+
async function validateMediaActivation(mod) {
|
|
271
|
+
const modalities = mod?.modalities;
|
|
272
|
+
if (!Array.isArray(modalities) || modalities.length === 0
|
|
273
|
+
|| !modalities.every((m) => VALID_MODALITIES.has(m))) {
|
|
274
|
+
throw new HubError(400, 'invalid modalities');
|
|
275
|
+
}
|
|
276
|
+
const hasCreatePrompt = typeof mod?.createPrompt === 'function';
|
|
277
|
+
const hasPiEngine = mod?.engine === 'pi';
|
|
278
|
+
if (!hasCreatePrompt && !hasPiEngine)
|
|
279
|
+
throw new HubError(400, 'missing createPrompt() or engine:"pi"');
|
|
280
|
+
if (hasCreatePrompt && hasPiEngine)
|
|
281
|
+
throw new HubError(400, 'createPrompt and engine:"pi" are mutually exclusive');
|
|
282
|
+
if (hasCreatePrompt) {
|
|
283
|
+
const fn = await mod.createPrompt(STUB_MEDIA_CTX);
|
|
284
|
+
if (typeof fn !== 'function')
|
|
285
|
+
throw new HubError(400, 'createPrompt did not return a function');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
166
288
|
async function setPluginEnabled(deps, input) {
|
|
167
289
|
const type = parseType(input.type);
|
|
168
290
|
const filename = validateFilename(input.filename);
|
|
169
291
|
const dir = deps.dirs[type];
|
|
170
|
-
|
|
171
|
-
|
|
292
|
+
// Accept either on-disk state: a stale client snapshot (double-toggled
|
|
293
|
+
// switch, unrefreshed list) sends the opposite-state filename, which must
|
|
294
|
+
// resolve idempotently instead of 404ing ("激活 not found").
|
|
295
|
+
const primary = resolveInDir(dir, filename);
|
|
296
|
+
const variant = /\.js\.disabled$/.test(filename)
|
|
297
|
+
? filename.replace(/\.js\.disabled$/, '.js')
|
|
298
|
+
: filename.replace(/\.js$/, '.js.disabled');
|
|
299
|
+
const variantPath = resolveInDir(dir, variant);
|
|
300
|
+
const current = fs.existsSync(primary) ? primary : fs.existsSync(variantPath) ? variantPath : null;
|
|
301
|
+
if (!current)
|
|
172
302
|
throw new HubError(404, `plugin file not found: ${filename}`);
|
|
173
|
-
const
|
|
303
|
+
const currentName = path.basename(current);
|
|
304
|
+
const nextName = input.enabled
|
|
305
|
+
? currentName.replace(/\.js\.disabled$/, '.js')
|
|
306
|
+
: currentName.replace(/\.js$/, '.js.disabled');
|
|
174
307
|
const next = resolveInDir(dir, nextName);
|
|
175
|
-
|
|
308
|
+
if (next !== current)
|
|
309
|
+
fs.renameSync(current, next);
|
|
176
310
|
await deps.reload?.(type);
|
|
177
311
|
return statEntry(type, dir, nextName);
|
|
178
312
|
}
|
|
@@ -15,6 +15,14 @@ function readBody(req) {
|
|
|
15
15
|
req.on('error', reject);
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
function readBodyBuffer(req) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
req.on('data', (c) => chunks.push(c));
|
|
22
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
23
|
+
req.on('error', reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
18
26
|
async function readJsonBody(req) {
|
|
19
27
|
const raw = await readBody(req);
|
|
20
28
|
try {
|
|
@@ -47,11 +55,13 @@ async function handlePluginsList(_req, res, deps) {
|
|
|
47
55
|
}
|
|
48
56
|
async function handlePluginsInstall(req, res, deps) {
|
|
49
57
|
await guarded(res, async () => {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
59
|
+
const filename = url.searchParams.get('filename') || '';
|
|
60
|
+
const type = (url.searchParams.get('type') || undefined);
|
|
61
|
+
const overwrite = url.searchParams.get('overwrite') === '1' || url.searchParams.get('overwrite') === 'true';
|
|
62
|
+
const bytes = await readBodyBuffer(req);
|
|
63
|
+
const entry = await (0, hub_1.installPlugin)(deps.hub, { filename, type, bytes, overwrite });
|
|
64
|
+
logger_1.log.info(`[PluginsHub] installed ${entry.type}/${filename}`);
|
|
55
65
|
return entry;
|
|
56
66
|
});
|
|
57
67
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Mode A 全局广播信封构造器(MafwScheduler.broadcast 的载荷形状)。
|
|
4
|
+
*
|
|
5
|
+
* wire 契约(桌面 renderer / TUI 均按此解析):
|
|
6
|
+
* { type: 'opencode_event', data: { type, properties?, sessionID?, directory?, error?, internal? } }
|
|
7
|
+
*
|
|
8
|
+
* 规则:可选字段缺失时不得出现在 data 上(key 稳定性,消费方用
|
|
9
|
+
* `'x' in data` 判定时不受缺省值污染)。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.opencodeBroadcast = opencodeBroadcast;
|
|
13
|
+
exports.projectRegisteredEvent = projectRegisteredEvent;
|
|
14
|
+
function opencodeBroadcast(data, internal) {
|
|
15
|
+
return {
|
|
16
|
+
type: 'opencode_event',
|
|
17
|
+
data: {
|
|
18
|
+
...data,
|
|
19
|
+
...(data.directory === undefined ? {} : { directory: data.directory }),
|
|
20
|
+
...(internal ? { internal: true } : {}),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 项目注册广播(桌面 renderer 依此刷新 Rail 项目列表)。
|
|
26
|
+
*
|
|
27
|
+
* 顶层广播必须扁平(无 `data` 键):desktop 剥壳逻辑 `event = raw?.data || raw`
|
|
28
|
+
* 会把 `data` 当作 opencode_event 的内层载荷,`type` 随之丢失——与
|
|
29
|
+
* runtime_switched / user_question 同一约定。
|
|
30
|
+
*/
|
|
31
|
+
function projectRegisteredEvent(projectDir) {
|
|
32
|
+
return { type: 'project_registered', projectDir };
|
|
33
|
+
}
|
|
@@ -69,7 +69,6 @@ class RuntimePluginLoader {
|
|
|
69
69
|
if (!fs.existsSync(this.pluginsDir)) {
|
|
70
70
|
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
|
71
71
|
fs.writeFileSync(path.join(this.pluginsDir, 'README.md'), README_CONTENT);
|
|
72
|
-
fs.writeFileSync(path.join(this.pluginsDir, 'example.js.disabled'), EXAMPLE_CONTENT);
|
|
73
72
|
logger_1.log.info(`[RuntimePluginLoader] Created ${this.pluginsDir}`);
|
|
74
73
|
}
|
|
75
74
|
}
|
|
@@ -90,6 +89,10 @@ class RuntimePluginLoader {
|
|
|
90
89
|
this.factories.delete(prev.name);
|
|
91
90
|
this.meta.delete(prev.name);
|
|
92
91
|
}
|
|
92
|
+
if (prev?.alias) {
|
|
93
|
+
this.factories.delete(prev.alias);
|
|
94
|
+
this.meta.delete(prev.alias);
|
|
95
|
+
}
|
|
93
96
|
this.state.delete(file);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
@@ -123,9 +126,22 @@ class RuntimePluginLoader {
|
|
|
123
126
|
const external = mod.external !== false;
|
|
124
127
|
this.factories.set(name, mod.createRuntime);
|
|
125
128
|
this.meta.set(name, { capabilities, external });
|
|
126
|
-
this.state.set(file, { file, name, status: 'ok', capabilities });
|
|
127
129
|
loadedNames.add(name);
|
|
128
|
-
|
|
130
|
+
// Filename-stem alias: the plugin hub addresses runtime plugins by
|
|
131
|
+
// filename stem (statEntry baseName) while switch/createRuntime
|
|
132
|
+
// validate by module.exports.name — register both so hub "激活" works
|
|
133
|
+
// regardless of the declared name. The real module name always wins
|
|
134
|
+
// (unconditional set above overwrites an earlier stem alias), and an
|
|
135
|
+
// alias never shadows a builtin.
|
|
136
|
+
const stem = file.replace(/\.js$/, '');
|
|
137
|
+
let alias;
|
|
138
|
+
if (stem !== name && !this.factories.has(stem) && !this.builtins.has(stem)) {
|
|
139
|
+
this.factories.set(stem, mod.createRuntime);
|
|
140
|
+
this.meta.set(stem, { capabilities, external });
|
|
141
|
+
alias = stem;
|
|
142
|
+
}
|
|
143
|
+
this.state.set(file, { file, name, status: 'ok', capabilities, alias });
|
|
144
|
+
logger_1.log.info(`[RuntimePluginLoader] Loaded ${file} (${name})${alias ? ` [alias: ${alias}]` : ''}`);
|
|
129
145
|
}
|
|
130
146
|
catch (err) {
|
|
131
147
|
const prev = this.state.get(file);
|
|
@@ -144,6 +160,10 @@ class RuntimePluginLoader {
|
|
|
144
160
|
return { createRuntime: builtin.factory, ...builtin };
|
|
145
161
|
return undefined;
|
|
146
162
|
}
|
|
163
|
+
/** 已注册内置件名(builtin 文件件不在其中,opencode 为恒等默认由 index.ts 注入 hub)。 */
|
|
164
|
+
getBuiltinNames() {
|
|
165
|
+
return [...this.builtins.keys()];
|
|
166
|
+
}
|
|
147
167
|
getState() {
|
|
148
168
|
for (const [name, b] of this.builtins) {
|
|
149
169
|
this.state.set(`builtin:${name}`, { file: `builtin:${name}`, name, status: 'ok', capabilities: b.capabilities });
|
|
@@ -228,13 +248,3 @@ Capabilities declared here gate gateway features declaratively: missing
|
|
|
228
248
|
capabilities disable the corresponding features (503 on gated endpoints,
|
|
229
249
|
skipped event subscription) — they never crash.
|
|
230
250
|
`;
|
|
231
|
-
const EXAMPLE_CONTENT = `// Rename to example.js to activate
|
|
232
|
-
module.exports = {
|
|
233
|
-
name: "example",
|
|
234
|
-
capabilities: { eventStream: false },
|
|
235
|
-
async createRuntime(ctx) {
|
|
236
|
-
ctx.log.info("[example-runtime] created");
|
|
237
|
-
throw new Error("example plugin: implement createRuntime before activating");
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
`;
|