@forgeax/interface 0.4.1 → 0.5.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 CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  forgeax 前端工程(Studio 三栏 UI)。
4
4
 
5
+ ## Public action registry migration (0.5.0)
6
+
7
+ > [!IMPORTANT]
8
+ > Action registration and dispatch now belong to `@forgeax/app-shell/application`.
9
+ > Interface no longer exports these functions or their types through `application`
10
+ > or `lib/action-registry`. Consumers must use the same exact App Shell version as
11
+ > Interface so product discovery and external registration share one registry.
12
+
13
+ ```ts
14
+ import {
15
+ dispatchAction,
16
+ registerAction,
17
+ type UiActionDef,
18
+ } from '@forgeax/app-shell/application';
19
+ ```
20
+
21
+ | App Shell owns | Interface retains |
22
+ | :-- | :-- |
23
+ | Registration, replacement-safe disposal and registry notifications | Product action definitions and capability declarations |
24
+ | Dispatch validation, source events and structured results | Handler behavior, permission decisions and visual feedback |
25
+ | Derived action/state snapshots and manifest metadata | DOM discovery, command-palette presentation and bridge wiring |
26
+
27
+ Application startup, normal/detached shells and global CSS remain Interface
28
+ entrypoints. This change removes the registry implementation, not those product
29
+ dependencies; it adds no compatibility facade or second action registry.
30
+
5
31
  ## 技术栈
6
32
 
7
33
  - **Bun** runtime · **Vite 6** dev server · **React 19** · **TypeScript**
@@ -31,5 +31,3 @@ export declare function startInterfaceApplication(overrides?: AppHostBootstrapOv
31
31
  export type { AppHostBootstrapOverrides } from './appHostBootstrap';
32
32
  export type { KeyboardRouterDeps } from './lib/global-shortcuts';
33
33
  export { configureStudioDomainClients, type StudioDomainClients, } from './store-parts/domain-clients';
34
- export { dispatchAction, registerAction } from './lib/action-registry';
35
- export type { JsonSchemaObject, UiActionDef, UiActionResult, UiCapability, } from './lib/action-registry';
@@ -61,4 +61,3 @@ export async function startInterfaceApplication(overrides = {}, options = {}) {
61
61
  // Product boot injects the existing domain authority without importing store
62
62
  // internals. This is the same configuration function, not another client registry.
63
63
  export { configureStudioDomainClients, } from './store-parts/domain-clients';
64
- export { dispatchAction, registerAction } from './lib/action-registry';
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { Command } from 'cmdk';
14
14
  import { createRestartableTimeoutTaskLifecycle } from '@forgeax/app-shell/react';
15
- import { dispatchAction, getAction, snapshotActions, } from '../../lib/action-registry';
15
+ import { dispatchAction, getAction, snapshotActions, } from '@forgeax/app-shell/application';
16
16
  import { setCommandPaletteOpen, useCommandPaletteOpen } from '../../lib/command-palette-store';
17
17
  import './CommandPalette.css';
18
18
  /** 从 action 的 JSON Schema 派生顶层参数清单(与 registry 的 validateArgs 同口径)。 */
@@ -1,4 +1,4 @@
1
- import { registerAction, registerStateSlice } from '../../lib/action-registry';
1
+ import { registerAction, registerStateSlice } from '@forgeax/app-shell/application';
2
2
  import { TRAJECTORY_MAX, clearTrajectory, readTrajectory, startTrajectoryRecording, } from '../../lib/ui-trajectory';
3
3
  /** 随每次 ui_snapshot 内联出墙的尾部条数(压 token;更多靠 trajectory.read 拉)。 */
4
4
  const SNAPSHOT_TAIL = 20;
@@ -12,7 +12,7 @@
12
12
  * data-fx-capability 权限分级(8 类之一;缺省 'other'。危险按钮**务必**标
13
13
  * delete/credential,编排层会据此弹确认卡)
14
14
  */
15
- import { registerAction } from './action-registry';
15
+ import { registerAction } from '@forgeax/app-shell/application';
16
16
  const VALID_CAPS = new Set([
17
17
  'read', 'write', 'delete', 'exec', 'network', 'credential', 'delegate', 'other',
18
18
  ]);
@@ -1,4 +1,4 @@
1
- import { getAction } from './action-registry';
1
+ import { getAction } from '@forgeax/app-shell/application';
2
2
  /** 按 RefDescriptor.kind 的专属 intent(SSOT:每类可引用单元的 AI 动作都列在这里)。 */
3
3
  const KIND_INTENTS = {
4
4
  'console-row': [
@@ -8,7 +8,7 @@
8
8
  * surface 口径(方案 §5):纯视图操作 'ui';背后是 server REST 的标 'both'(headless
9
9
  * 等价路径 P1 接线,声明先行,UI run() 已经就是调同一 HTTP API,server 是行为 SSOT)。
10
10
  */
11
- import { registerAction, registerStateSlice } from './action-registry';
11
+ import { registerAction, registerStateSlice } from '@forgeax/app-shell/application';
12
12
  import { getSessionClient } from '../store-parts/session-client';
13
13
  import { useShellStore, tabLabel } from '../store';
14
14
  import { openExtensionPage, resolveRegisteredOverlayId } from '../core/page-navigation';
@@ -7,7 +7,7 @@
7
7
  * 纯 DOM overlay,零 store 依赖,装不上(SSR)静默跳过;观测层绝不影响派发主流程。
8
8
  */
9
9
  import { installCustomEventObservation } from '@forgeax/app-shell/react';
10
- import { UI_ACTION_DISPATCH_EVENT, getAction } from './action-registry';
10
+ import { UI_ACTION_DISPATCH_EVENT, getAction } from '@forgeax/app-shell/application';
11
11
  const STYLE_ID = 'fx-ui-action-highlight-style';
12
12
  const BADGE_ID = 'fx-ui-action-badge';
13
13
  const HIGHLIGHT_CSS = `
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { installWindowMessageObservation } from '@forgeax/app-shell/react';
15
15
  import { getSessionClient, hasSessionClient } from '../store-parts/session-client';
16
- import { buildManifest, dispatchAction, onRegistryChange, snapshotActions, snapshotState, } from './action-registry';
16
+ import { buildManifest, dispatchAction, onRegistryChange, snapshotActions, snapshotState, } from '@forgeax/app-shell/application';
17
17
  import { registerBuiltinActions } from './builtin-actions';
18
18
  import { startActionDomDiscovery } from './action-dom-discovery';
19
19
  import { installUiActionHighlight } from './ui-action-highlight';
@@ -9,7 +9,7 @@
9
9
  * 可序列化:entry 只含原始标量 / 浅裁剪后的 args,过得了 snapshotState 的 JSON 出墙。
10
10
  * 隐私:credential 级 action 的 args 整体打码(不把密钥类入参喂进 AI 上下文)。 */
11
11
  import { installCustomEventObservation } from '@forgeax/app-shell/react';
12
- import { getAction, UI_ACTION_DISPATCH_EVENT } from './action-registry';
12
+ import { getAction, UI_ACTION_DISPATCH_EVENT } from '@forgeax/app-shell/application';
13
13
  // 环形缓冲容量 == `trajectory.read` 单次可拉上限(readTrajectory 用它收敛 limit)。
14
14
  // 二者本就相等,故一个常量封两处:内存占用,以及 AI 一次拉取的**最坏上下文体积**
15
15
  // (SNAPSHOT_TAIL 20 条随每次 ui_snapshot 出墙不受此限;这里管的是按需 read 的天花板)。
@@ -15,7 +15,7 @@
15
15
  * 是 host 半边,协议以此为准)。
16
16
  */
17
17
  import { installWindowMessageObservation } from '@forgeax/app-shell/react';
18
- import { registerAction } from './action-registry';
18
+ import { registerAction } from '@forgeax/app-shell/application';
19
19
  import { isTrustedMessageOrigin } from './trustedOrigins';
20
20
  const VALID_CAPS = new Set([
21
21
  'read', 'write', 'delete', 'exec', 'network', 'credential', 'delegate', 'other',
@@ -61,4 +61,3 @@ export async function startInterfaceApplication(overrides = {}, options = {}) {
61
61
  // Product boot injects the existing domain authority without importing store
62
62
  // internals. This is the same configuration function, not another client registry.
63
63
  export { configureStudioDomainClients, } from './store-parts/domain-clients.js';
64
- export { dispatchAction, registerAction } from './lib/action-registry.js';
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { Command } from 'cmdk';
14
14
  import { createRestartableTimeoutTaskLifecycle } from '@forgeax/app-shell/react';
15
- import { dispatchAction, getAction, snapshotActions, } from '../../lib/action-registry.js';
15
+ import { dispatchAction, getAction, snapshotActions, } from '@forgeax/app-shell/application';
16
16
  import { setCommandPaletteOpen, useCommandPaletteOpen } from '../../lib/command-palette-store.js';
17
17
 
18
18
  /** 从 action 的 JSON Schema 派生顶层参数清单(与 registry 的 validateArgs 同口径)。 */
@@ -1,4 +1,4 @@
1
- import { registerAction, registerStateSlice } from '../../lib/action-registry.js';
1
+ import { registerAction, registerStateSlice } from '@forgeax/app-shell/application';
2
2
  import { TRAJECTORY_MAX, clearTrajectory, readTrajectory, startTrajectoryRecording, } from '../../lib/ui-trajectory.js';
3
3
  /** 随每次 ui_snapshot 内联出墙的尾部条数(压 token;更多靠 trajectory.read 拉)。 */
4
4
  const SNAPSHOT_TAIL = 20;
@@ -12,7 +12,7 @@
12
12
  * data-fx-capability 权限分级(8 类之一;缺省 'other'。危险按钮**务必**标
13
13
  * delete/credential,编排层会据此弹确认卡)
14
14
  */
15
- import { registerAction } from './action-registry.js';
15
+ import { registerAction } from '@forgeax/app-shell/application';
16
16
  const VALID_CAPS = new Set([
17
17
  'read', 'write', 'delete', 'exec', 'network', 'credential', 'delegate', 'other',
18
18
  ]);
@@ -1,4 +1,4 @@
1
- import { getAction } from './action-registry.js';
1
+ import { getAction } from '@forgeax/app-shell/application';
2
2
  /** 按 RefDescriptor.kind 的专属 intent(SSOT:每类可引用单元的 AI 动作都列在这里)。 */
3
3
  const KIND_INTENTS = {
4
4
  'console-row': [
@@ -8,7 +8,7 @@
8
8
  * surface 口径(方案 §5):纯视图操作 'ui';背后是 server REST 的标 'both'(headless
9
9
  * 等价路径 P1 接线,声明先行,UI run() 已经就是调同一 HTTP API,server 是行为 SSOT)。
10
10
  */
11
- import { registerAction, registerStateSlice } from './action-registry.js';
11
+ import { registerAction, registerStateSlice } from '@forgeax/app-shell/application';
12
12
  import { getSessionClient } from '../store-parts/session-client.js';
13
13
  import { useShellStore, tabLabel } from '../store.js';
14
14
  import { openExtensionPage, resolveRegisteredOverlayId } from '../core/page-navigation.js';
@@ -7,7 +7,7 @@
7
7
  * 纯 DOM overlay,零 store 依赖,装不上(SSR)静默跳过;观测层绝不影响派发主流程。
8
8
  */
9
9
  import { installCustomEventObservation } from '@forgeax/app-shell/react';
10
- import { UI_ACTION_DISPATCH_EVENT, getAction } from './action-registry.js';
10
+ import { UI_ACTION_DISPATCH_EVENT, getAction } from '@forgeax/app-shell/application';
11
11
  const STYLE_ID = 'fx-ui-action-highlight-style';
12
12
  const BADGE_ID = 'fx-ui-action-badge';
13
13
  const HIGHLIGHT_CSS = `
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { installWindowMessageObservation } from '@forgeax/app-shell/react';
15
15
  import { getSessionClient, hasSessionClient } from '../store-parts/session-client.js';
16
- import { buildManifest, dispatchAction, onRegistryChange, snapshotActions, snapshotState, } from './action-registry.js';
16
+ import { buildManifest, dispatchAction, onRegistryChange, snapshotActions, snapshotState, } from '@forgeax/app-shell/application';
17
17
  import { registerBuiltinActions } from './builtin-actions.js';
18
18
  import { startActionDomDiscovery } from './action-dom-discovery.js';
19
19
  import { installUiActionHighlight } from './ui-action-highlight.js';
@@ -9,7 +9,7 @@
9
9
  * 可序列化:entry 只含原始标量 / 浅裁剪后的 args,过得了 snapshotState 的 JSON 出墙。
10
10
  * 隐私:credential 级 action 的 args 整体打码(不把密钥类入参喂进 AI 上下文)。 */
11
11
  import { installCustomEventObservation } from '@forgeax/app-shell/react';
12
- import { getAction, UI_ACTION_DISPATCH_EVENT } from './action-registry.js';
12
+ import { getAction, UI_ACTION_DISPATCH_EVENT } from '@forgeax/app-shell/application';
13
13
  // 环形缓冲容量 == `trajectory.read` 单次可拉上限(readTrajectory 用它收敛 limit)。
14
14
  // 二者本就相等,故一个常量封两处:内存占用,以及 AI 一次拉取的**最坏上下文体积**
15
15
  // (SNAPSHOT_TAIL 20 条随每次 ui_snapshot 出墙不受此限;这里管的是按需 read 的天花板)。
@@ -15,7 +15,7 @@
15
15
  * 是 host 半边,协议以此为准)。
16
16
  */
17
17
  import { installWindowMessageObservation } from '@forgeax/app-shell/react';
18
- import { registerAction } from './action-registry.js';
18
+ import { registerAction } from '@forgeax/app-shell/application';
19
19
  import { isTrustedMessageOrigin } from './trustedOrigins.js';
20
20
  const VALID_CAPS = new Set([
21
21
  'read', 'write', 'delete', 'exec', 'network', 'credential', 'delegate', 'other',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/interface",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "ForgeaX product interface shell and application composition boundary.",
6
6
  "license": "Apache-2.0",
@@ -92,7 +92,7 @@
92
92
  "lint:dep": "depcruise -c .dependency-cruiser.cjs src"
93
93
  },
94
94
  "dependencies": {
95
- "@forgeax/app-shell": "0.84.0",
95
+ "@forgeax/app-shell": "0.86.0",
96
96
  "@forgeax/extension-host": "0.3.2",
97
97
  "@forgeax/extension-platform": "0.5.0",
98
98
  "@forgeax/toolkit": "0.1.2",
@@ -1,94 +0,0 @@
1
- /** action-registry —— UI 语义操作层(产品 AI 化 P0)的单一真值源:
2
- * 「本产品当前暴露哪些可被人 / AI 调用的功能」。
3
- *
4
- * 一张注册表服务三个消费者(压缩公理:消掉「AI 能做的和人能做的是不是同一套?」):
5
- * 1. 按钮 onClick —— 组件经 `dispatchAction(id, args)` 派发(与 AI 同一条路);
6
- * 2. 命令面板(P2,cmdk 激活时数据源即本表);
7
- * 3. AI 工具调用 —— `ui_snapshot` 读清单 / `ui_invoke` 派发,经 lib/ui-bridge.ts
8
- * 的 perception 往返接到编排层(@forgeax/orchestrator)。
9
- *
10
- * 契约形态(评审 2.1):`schema` 的 SSOT 是 **JSON Schema 纯对象**(AI 侧 ToolSpec 与
11
- * postMessage 结构化克隆都只吃 JSON;函数永不过 wire)。`available` / `run` 只活在
12
- * 本模块,manifest(`buildManifest`)只导出可序列化子集。
13
- *
14
- * headless 边界(方案 §5):`surface:'both'` 的 action,UI `run()` 必须调 server 的
15
- * 同一 HTTP API(server 是行为 SSOT),UI 侧只许加视觉反馈——不许长出独立业务逻辑。
16
- */
17
- /** 与编排层 trust-gate 的 Capability 8 类对齐(权限分级的声明值,manifest 里出墙)。 */
18
- export type UiCapability = 'read' | 'write' | 'delete' | 'exec' | 'network' | 'credential' | 'delegate' | 'other';
19
- /** 派发结果(评审 2.2 act→observe 合并):
20
- * - completed:已完成,`stateDigest` 携带可观察的状态变化(多数场景免掉后续 snapshot);
21
- * - accepted:已受理、异步在跑(慢 action 快速返回,勿等勿重试;P0 期完成态靠下次 snapshot 观察);
22
- * - rejected:未执行,`reason` 给人话原因(未注册 / 不可用 / 参数校验失败 / run 抛错)。 */
23
- export interface UiActionResult {
24
- status: 'completed' | 'accepted' | 'rejected';
25
- reason?: string;
26
- stateDigest?: unknown;
27
- }
28
- /** JSON Schema 纯对象(不引 zod 类型;zod 只是书写便利,登记时即转换落表)。 */
29
- export type JsonSchemaObject = Record<string, unknown>;
30
- export interface UiActionDef {
31
- /** 稳定 id,'domain.verb' 形("game.switch" / "panel.toggle_sidebar")。 */
32
- id: string;
33
- /** 人读标题(菜单 / 命令面板 / 权限卡展示)。 */
34
- title: string;
35
- /** AI 读说明(manifest 出墙;写清做什么 + 何时用)。 */
36
- description?: string;
37
- /** 参数契约(JSON Schema 纯对象)。缺省 = 无参数。 */
38
- schema?: JsonSchemaObject;
39
- /** 权限分级声明 —— 编排层 trust-gate 按此弹卡/直放(delete/credential 会请求用户确认)。 */
40
- capability: UiCapability;
41
- /** headless 可用性:'ui'(纯视图,须 UI 在线)/ 'server'/'both'(状态型,server 是 SSOT)。 */
42
- surface?: 'ui' | 'server' | 'both';
43
- /** 预期执行时长(ms)——编排层往返超时据此放宽;慢 action 的正道是快速回 accepted。 */
44
- timeoutMs?: number;
45
- /** 当前是否可调。返回 string = 不可用 + 人话原因(AI 可读可解释);缺省恒可用。 */
46
- available?: () => true | string;
47
- /** P1-9 一等工具化:标 true → 编排层从 manifest 派生独立 ToolSpec(ui_act_*)下发
48
- * 模型,免一次 snapshot 发现往返。只给高频 action 打标(编排层有数量上限)。 */
49
- firstClass?: boolean;
50
- /** 「人」界面(命令面板)用的**动态候选值**提供器,按参数名给一列合法取值 —— 让
51
- * 自由文本参数(如 game.switch 的 slug)变成下拉,避免瞎填后触发 server 404。
52
- * 仅活在客户端(同 run/available,不进 manifest);AI 侧靠 ui_snapshot / 状态片发现,
53
- * 不依赖此。静态 `schema.enum` 优先于此(两者都无则退回文本框)。 */
54
- choices?: Record<string, () => string[] | Promise<string[]>>;
55
- /** 执行体。返回 void 视为 completed;抛错翻成 rejected(fail-soft,不炸往返)。 */
56
- run: (args: Record<string, unknown>) => UiActionResult | void | Promise<UiActionResult | void>;
57
- }
58
- /** snapshot 状态摘要的一片(评审 2.5:注册式 derive,禁手写台账)。selector 从 store
59
- * 等真值源读,值必须可序列化(过 JSON.stringify)。 */
60
- export type StateSliceSelector = () => unknown;
61
- /** 注册一个 action(同 id 重复注册 = 替换,幂等;HMR 安全)。返回注销函数。 */
62
- export declare function registerAction(def: UiActionDef): () => void;
63
- /** 注册一片状态摘要(同 id 替换,幂等)。返回注销函数。 */
64
- export declare function registerStateSlice(id: string, selector: StateSliceSelector): () => void;
65
- /** 注册表(action 或 state slice)变更订阅 —— ui-bridge 据此 debounce 重推 manifest。 */
66
- export declare function onRegistryChange(cb: () => void): () => void;
67
- export declare function getAction(id: string): UiActionDef | undefined;
68
- /** AI 派发时打的来源标记;window 事件供 ghost 高亮层(P1)与 telemetry / 轨迹追踪衔接。
69
- * detail 形:`{ id, source, args }` —— args 让轨迹追踪(lib/ui-trajectory)记得下「做了啥」,
70
- * ghost 高亮只用 id/source(向后兼容,多带的字段无害)。 */
71
- export declare const UI_ACTION_DISPATCH_EVENT = "forgeax:ui-action-dispatch";
72
- /** 派发一个 action —— 按钮与 AI 共用的**唯一**执行入口(未注册的 id 拒绝,fail-closed)。 */
73
- export declare function dispatchAction(id: string, args?: Record<string, unknown>, opts?: {
74
- source?: 'human' | 'ai';
75
- }): Promise<UiActionResult>;
76
- export interface UiActionSummary {
77
- id: string;
78
- title: string;
79
- available: boolean;
80
- reason?: string;
81
- /** detail:'schema' 时补上。 */
82
- description?: string;
83
- inputSchema?: JsonSchemaObject;
84
- }
85
- /** ui_snapshot 的 action 视图。分层(评审 2.7):默认轻量清单;detail:'schema' + ids
86
- * 按需展开 schema 与详细说明(长尾几百 action 也不炸 token)。 */
87
- export declare function snapshotActions(detail?: string, ids?: string[]): UiActionSummary[];
88
- /** ui_snapshot 的状态摘要:逐片 derive,单片异常不传染(fail-soft)。 */
89
- export declare function snapshotState(): Record<string, unknown>;
90
- /** manifest(POST /:sid/ui-manifest 的 payload)—— 只含可序列化声明,函数永不出墙。
91
- * 它是编排层 trust-gate 的权限输入:capability 必须如实声明。 */
92
- export declare function buildManifest(): Array<Record<string, unknown>>;
93
- /** 测试用:清空注册表(生产代码不要调)。 */
94
- export declare function __resetRegistryForTest(): void;
@@ -1,210 +0,0 @@
1
- /** action-registry —— UI 语义操作层(产品 AI 化 P0)的单一真值源:
2
- * 「本产品当前暴露哪些可被人 / AI 调用的功能」。
3
- *
4
- * 一张注册表服务三个消费者(压缩公理:消掉「AI 能做的和人能做的是不是同一套?」):
5
- * 1. 按钮 onClick —— 组件经 `dispatchAction(id, args)` 派发(与 AI 同一条路);
6
- * 2. 命令面板(P2,cmdk 激活时数据源即本表);
7
- * 3. AI 工具调用 —— `ui_snapshot` 读清单 / `ui_invoke` 派发,经 lib/ui-bridge.ts
8
- * 的 perception 往返接到编排层(@forgeax/orchestrator)。
9
- *
10
- * 契约形态(评审 2.1):`schema` 的 SSOT 是 **JSON Schema 纯对象**(AI 侧 ToolSpec 与
11
- * postMessage 结构化克隆都只吃 JSON;函数永不过 wire)。`available` / `run` 只活在
12
- * 本模块,manifest(`buildManifest`)只导出可序列化子集。
13
- *
14
- * headless 边界(方案 §5):`surface:'both'` 的 action,UI `run()` 必须调 server 的
15
- * 同一 HTTP API(server 是行为 SSOT),UI 侧只许加视觉反馈——不许长出独立业务逻辑。
16
- */
17
- // ─── 注册表本体(模块级 Map,页级单例)────────────────────────────────────────
18
- const actions = new Map();
19
- const stateSlices = new Map();
20
- const changeListeners = new Set();
21
- function notifyChange() {
22
- for (const cb of changeListeners) {
23
- try {
24
- cb();
25
- }
26
- catch {
27
- /* listener 异常不传染 */
28
- }
29
- }
30
- }
31
- /** 注册一个 action(同 id 重复注册 = 替换,幂等;HMR 安全)。返回注销函数。 */
32
- export function registerAction(def) {
33
- actions.set(def.id, def);
34
- notifyChange();
35
- return () => {
36
- if (actions.get(def.id) === def) {
37
- actions.delete(def.id);
38
- notifyChange();
39
- }
40
- };
41
- }
42
- /** 注册一片状态摘要(同 id 替换,幂等)。返回注销函数。 */
43
- export function registerStateSlice(id, selector) {
44
- stateSlices.set(id, selector);
45
- notifyChange();
46
- return () => {
47
- if (stateSlices.get(id) === selector) {
48
- stateSlices.delete(id);
49
- notifyChange();
50
- }
51
- };
52
- }
53
- /** 注册表(action 或 state slice)变更订阅 —— ui-bridge 据此 debounce 重推 manifest。 */
54
- export function onRegistryChange(cb) {
55
- changeListeners.add(cb);
56
- return () => changeListeners.delete(cb);
57
- }
58
- export function getAction(id) {
59
- return actions.get(id);
60
- }
61
- // ─── 派发单入口(评审 2.4:data-* 只做发现,执行永远走这里)──────────────────
62
- /** AI 派发时打的来源标记;window 事件供 ghost 高亮层(P1)与 telemetry / 轨迹追踪衔接。
63
- * detail 形:`{ id, source, args }` —— args 让轨迹追踪(lib/ui-trajectory)记得下「做了啥」,
64
- * ghost 高亮只用 id/source(向后兼容,多带的字段无害)。 */
65
- export const UI_ACTION_DISPATCH_EVENT = 'forgeax:ui-action-dispatch';
66
- /** 极简 JSON Schema 参数校验(P0:required + 顶层 properties 的原始类型)。
67
- * 不引 ajv——契约漂移由编排层契约测试兜,这里挡住明显错参即可(Fail Fast)。 */
68
- function validateArgs(schema, args) {
69
- if (!schema)
70
- return true;
71
- const required = Array.isArray(schema.required) ? schema.required : [];
72
- for (const k of required) {
73
- if (typeof k === 'string' && !(k in args))
74
- return `missing required arg "${k}"`;
75
- }
76
- const props = schema.properties && typeof schema.properties === 'object'
77
- ? schema.properties
78
- : {};
79
- for (const [k, v] of Object.entries(args)) {
80
- const p = props[k];
81
- if (!p)
82
- continue; // 未声明的多余参数放过(向前兼容),由 run 自行忽略
83
- const t = p.type;
84
- if (typeof t === 'string') {
85
- const actual = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
86
- const okType = (t === 'string' && actual === 'string') ||
87
- (t === 'number' && actual === 'number') ||
88
- (t === 'integer' && actual === 'number' && Number.isInteger(v)) ||
89
- (t === 'boolean' && actual === 'boolean') ||
90
- (t === 'object' && actual === 'object') ||
91
- (t === 'array' && actual === 'array');
92
- if (!okType)
93
- return `arg "${k}" should be ${t}, got ${actual}`;
94
- }
95
- if (Array.isArray(p.enum) && !p.enum.includes(v)) {
96
- return `arg "${k}" must be one of ${JSON.stringify(p.enum)}`;
97
- }
98
- }
99
- return true;
100
- }
101
- /** 派发一个 action —— 按钮与 AI 共用的**唯一**执行入口(未注册的 id 拒绝,fail-closed)。 */
102
- export async function dispatchAction(id, args = {}, opts = {}) {
103
- const src = opts.source ?? 'human';
104
- // [fx-action-trace] 定位「点了/调了没反应」的最小充分打点:
105
- // ① entry —— 派发是否发生 + args/source(缺此行 = 根本没触发 dispatch)。
106
- // ② terminal —— 唯一终态行:completed/accepted 带 stateDigest(判「到底改了啥」),
107
- // rejected 带 reason(未注册/不可用/参数错/run 抛错 已统一收敛进 reason)。
108
- // 两行即可定位:有 entry 无 terminal = 卡在 run();rejected+reason = 为何被拒;
109
- // completed 但 stateDigest 没变 = action 效果与 UI 脱节。故只留这两点(+抛错补栈)。
110
- console.info(`%c[fx-action]%c ▶ ${id} (${src})`, 'color:#818cf8;font-weight:bold', 'color:inherit', { args });
111
- const done = (r) => {
112
- if (r.status === 'rejected') {
113
- console.warn(`[fx-action] ✗ ${id} rejected: ${r.reason ?? ''}`);
114
- }
115
- else {
116
- console.info(`%c[fx-action]%c ✔ ${id} → ${r.status}`, 'color:#34d399;font-weight:bold', 'color:inherit', r.stateDigest !== undefined ? { stateDigest: r.stateDigest } : {});
117
- }
118
- return r;
119
- };
120
- const def = actions.get(id);
121
- if (!def)
122
- return done({ status: 'rejected', reason: `unknown action "${id}" (not in the registry)` });
123
- const avail = def.available ? safeAvailable(def) : true;
124
- if (avail !== true)
125
- return done({ status: 'rejected', reason: avail });
126
- const valid = validateArgs(def.schema, args);
127
- if (valid !== true)
128
- return done({ status: 'rejected', reason: valid });
129
- try {
130
- window.dispatchEvent(new CustomEvent(UI_ACTION_DISPATCH_EVENT, { detail: { id, source: src, args } }));
131
- }
132
- catch {
133
- /* SSR / no window — ignore */
134
- }
135
- try {
136
- const out = await def.run(args);
137
- return done(out ?? { status: 'completed' });
138
- }
139
- catch (e) {
140
- console.error(`[fx-action] ✗ ${id} threw`, e); // 抛错补一行栈(定位真异常)
141
- return done({ status: 'rejected', reason: `action "${id}" threw: ${e instanceof Error ? e.message : String(e)}` });
142
- }
143
- }
144
- function safeAvailable(def) {
145
- try {
146
- return def.available();
147
- }
148
- catch (e) {
149
- return `availability check threw: ${e instanceof Error ? e.message : String(e)}`;
150
- }
151
- }
152
- /** ui_snapshot 的 action 视图。分层(评审 2.7):默认轻量清单;detail:'schema' + ids
153
- * 按需展开 schema 与详细说明(长尾几百 action 也不炸 token)。 */
154
- export function snapshotActions(detail, ids) {
155
- const expand = detail === 'schema' ? new Set(ids ?? []) : null;
156
- const out = [];
157
- for (const def of actions.values()) {
158
- const avail = def.available ? safeAvailable(def) : true;
159
- const row = {
160
- id: def.id,
161
- title: def.title,
162
- available: avail === true,
163
- ...(avail === true ? {} : { reason: avail }),
164
- };
165
- if (expand?.has(def.id)) {
166
- if (def.description)
167
- row.description = def.description;
168
- row.inputSchema = def.schema ?? { type: 'object', properties: {} };
169
- }
170
- out.push(row);
171
- }
172
- return out;
173
- }
174
- /** ui_snapshot 的状态摘要:逐片 derive,单片异常不传染(fail-soft)。 */
175
- export function snapshotState() {
176
- const out = {};
177
- for (const [id, selector] of stateSlices) {
178
- try {
179
- out[id] = selector();
180
- }
181
- catch (e) {
182
- out[id] = { error: `state slice threw: ${e instanceof Error ? e.message : String(e)}` };
183
- }
184
- }
185
- return out;
186
- }
187
- /** manifest(POST /:sid/ui-manifest 的 payload)—— 只含可序列化声明,函数永不出墙。
188
- * 它是编排层 trust-gate 的权限输入:capability 必须如实声明。 */
189
- export function buildManifest() {
190
- const out = [];
191
- for (const def of actions.values()) {
192
- out.push({
193
- id: def.id,
194
- title: def.title,
195
- ...(def.description ? { description: def.description } : {}),
196
- inputSchema: def.schema ?? { type: 'object', properties: {} },
197
- capability: def.capability,
198
- ...(def.surface ? { surface: def.surface } : {}),
199
- ...(def.timeoutMs ? { timeoutMs: def.timeoutMs } : {}),
200
- ...(def.firstClass ? { firstClass: true } : {}),
201
- });
202
- }
203
- return out;
204
- }
205
- /** 测试用:清空注册表(生产代码不要调)。 */
206
- export function __resetRegistryForTest() {
207
- actions.clear();
208
- stateSlices.clear();
209
- changeListeners.clear();
210
- }
@@ -1,210 +0,0 @@
1
- /** action-registry —— UI 语义操作层(产品 AI 化 P0)的单一真值源:
2
- * 「本产品当前暴露哪些可被人 / AI 调用的功能」。
3
- *
4
- * 一张注册表服务三个消费者(压缩公理:消掉「AI 能做的和人能做的是不是同一套?」):
5
- * 1. 按钮 onClick —— 组件经 `dispatchAction(id, args)` 派发(与 AI 同一条路);
6
- * 2. 命令面板(P2,cmdk 激活时数据源即本表);
7
- * 3. AI 工具调用 —— `ui_snapshot` 读清单 / `ui_invoke` 派发,经 lib/ui-bridge.ts
8
- * 的 perception 往返接到编排层(@forgeax/orchestrator)。
9
- *
10
- * 契约形态(评审 2.1):`schema` 的 SSOT 是 **JSON Schema 纯对象**(AI 侧 ToolSpec 与
11
- * postMessage 结构化克隆都只吃 JSON;函数永不过 wire)。`available` / `run` 只活在
12
- * 本模块,manifest(`buildManifest`)只导出可序列化子集。
13
- *
14
- * headless 边界(方案 §5):`surface:'both'` 的 action,UI `run()` 必须调 server 的
15
- * 同一 HTTP API(server 是行为 SSOT),UI 侧只许加视觉反馈——不许长出独立业务逻辑。
16
- */
17
- // ─── 注册表本体(模块级 Map,页级单例)────────────────────────────────────────
18
- const actions = new Map();
19
- const stateSlices = new Map();
20
- const changeListeners = new Set();
21
- function notifyChange() {
22
- for (const cb of changeListeners) {
23
- try {
24
- cb();
25
- }
26
- catch {
27
- /* listener 异常不传染 */
28
- }
29
- }
30
- }
31
- /** 注册一个 action(同 id 重复注册 = 替换,幂等;HMR 安全)。返回注销函数。 */
32
- export function registerAction(def) {
33
- actions.set(def.id, def);
34
- notifyChange();
35
- return () => {
36
- if (actions.get(def.id) === def) {
37
- actions.delete(def.id);
38
- notifyChange();
39
- }
40
- };
41
- }
42
- /** 注册一片状态摘要(同 id 替换,幂等)。返回注销函数。 */
43
- export function registerStateSlice(id, selector) {
44
- stateSlices.set(id, selector);
45
- notifyChange();
46
- return () => {
47
- if (stateSlices.get(id) === selector) {
48
- stateSlices.delete(id);
49
- notifyChange();
50
- }
51
- };
52
- }
53
- /** 注册表(action 或 state slice)变更订阅 —— ui-bridge 据此 debounce 重推 manifest。 */
54
- export function onRegistryChange(cb) {
55
- changeListeners.add(cb);
56
- return () => changeListeners.delete(cb);
57
- }
58
- export function getAction(id) {
59
- return actions.get(id);
60
- }
61
- // ─── 派发单入口(评审 2.4:data-* 只做发现,执行永远走这里)──────────────────
62
- /** AI 派发时打的来源标记;window 事件供 ghost 高亮层(P1)与 telemetry / 轨迹追踪衔接。
63
- * detail 形:`{ id, source, args }` —— args 让轨迹追踪(lib/ui-trajectory)记得下「做了啥」,
64
- * ghost 高亮只用 id/source(向后兼容,多带的字段无害)。 */
65
- export const UI_ACTION_DISPATCH_EVENT = 'forgeax:ui-action-dispatch';
66
- /** 极简 JSON Schema 参数校验(P0:required + 顶层 properties 的原始类型)。
67
- * 不引 ajv——契约漂移由编排层契约测试兜,这里挡住明显错参即可(Fail Fast)。 */
68
- function validateArgs(schema, args) {
69
- if (!schema)
70
- return true;
71
- const required = Array.isArray(schema.required) ? schema.required : [];
72
- for (const k of required) {
73
- if (typeof k === 'string' && !(k in args))
74
- return `missing required arg "${k}"`;
75
- }
76
- const props = schema.properties && typeof schema.properties === 'object'
77
- ? schema.properties
78
- : {};
79
- for (const [k, v] of Object.entries(args)) {
80
- const p = props[k];
81
- if (!p)
82
- continue; // 未声明的多余参数放过(向前兼容),由 run 自行忽略
83
- const t = p.type;
84
- if (typeof t === 'string') {
85
- const actual = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
86
- const okType = (t === 'string' && actual === 'string') ||
87
- (t === 'number' && actual === 'number') ||
88
- (t === 'integer' && actual === 'number' && Number.isInteger(v)) ||
89
- (t === 'boolean' && actual === 'boolean') ||
90
- (t === 'object' && actual === 'object') ||
91
- (t === 'array' && actual === 'array');
92
- if (!okType)
93
- return `arg "${k}" should be ${t}, got ${actual}`;
94
- }
95
- if (Array.isArray(p.enum) && !p.enum.includes(v)) {
96
- return `arg "${k}" must be one of ${JSON.stringify(p.enum)}`;
97
- }
98
- }
99
- return true;
100
- }
101
- /** 派发一个 action —— 按钮与 AI 共用的**唯一**执行入口(未注册的 id 拒绝,fail-closed)。 */
102
- export async function dispatchAction(id, args = {}, opts = {}) {
103
- const src = opts.source ?? 'human';
104
- // [fx-action-trace] 定位「点了/调了没反应」的最小充分打点:
105
- // ① entry —— 派发是否发生 + args/source(缺此行 = 根本没触发 dispatch)。
106
- // ② terminal —— 唯一终态行:completed/accepted 带 stateDigest(判「到底改了啥」),
107
- // rejected 带 reason(未注册/不可用/参数错/run 抛错 已统一收敛进 reason)。
108
- // 两行即可定位:有 entry 无 terminal = 卡在 run();rejected+reason = 为何被拒;
109
- // completed 但 stateDigest 没变 = action 效果与 UI 脱节。故只留这两点(+抛错补栈)。
110
- console.info(`%c[fx-action]%c ▶ ${id} (${src})`, 'color:#818cf8;font-weight:bold', 'color:inherit', { args });
111
- const done = (r) => {
112
- if (r.status === 'rejected') {
113
- console.warn(`[fx-action] ✗ ${id} rejected: ${r.reason ?? ''}`);
114
- }
115
- else {
116
- console.info(`%c[fx-action]%c ✔ ${id} → ${r.status}`, 'color:#34d399;font-weight:bold', 'color:inherit', r.stateDigest !== undefined ? { stateDigest: r.stateDigest } : {});
117
- }
118
- return r;
119
- };
120
- const def = actions.get(id);
121
- if (!def)
122
- return done({ status: 'rejected', reason: `unknown action "${id}" (not in the registry)` });
123
- const avail = def.available ? safeAvailable(def) : true;
124
- if (avail !== true)
125
- return done({ status: 'rejected', reason: avail });
126
- const valid = validateArgs(def.schema, args);
127
- if (valid !== true)
128
- return done({ status: 'rejected', reason: valid });
129
- try {
130
- window.dispatchEvent(new CustomEvent(UI_ACTION_DISPATCH_EVENT, { detail: { id, source: src, args } }));
131
- }
132
- catch {
133
- /* SSR / no window — ignore */
134
- }
135
- try {
136
- const out = await def.run(args);
137
- return done(out ?? { status: 'completed' });
138
- }
139
- catch (e) {
140
- console.error(`[fx-action] ✗ ${id} threw`, e); // 抛错补一行栈(定位真异常)
141
- return done({ status: 'rejected', reason: `action "${id}" threw: ${e instanceof Error ? e.message : String(e)}` });
142
- }
143
- }
144
- function safeAvailable(def) {
145
- try {
146
- return def.available();
147
- }
148
- catch (e) {
149
- return `availability check threw: ${e instanceof Error ? e.message : String(e)}`;
150
- }
151
- }
152
- /** ui_snapshot 的 action 视图。分层(评审 2.7):默认轻量清单;detail:'schema' + ids
153
- * 按需展开 schema 与详细说明(长尾几百 action 也不炸 token)。 */
154
- export function snapshotActions(detail, ids) {
155
- const expand = detail === 'schema' ? new Set(ids ?? []) : null;
156
- const out = [];
157
- for (const def of actions.values()) {
158
- const avail = def.available ? safeAvailable(def) : true;
159
- const row = {
160
- id: def.id,
161
- title: def.title,
162
- available: avail === true,
163
- ...(avail === true ? {} : { reason: avail }),
164
- };
165
- if (expand?.has(def.id)) {
166
- if (def.description)
167
- row.description = def.description;
168
- row.inputSchema = def.schema ?? { type: 'object', properties: {} };
169
- }
170
- out.push(row);
171
- }
172
- return out;
173
- }
174
- /** ui_snapshot 的状态摘要:逐片 derive,单片异常不传染(fail-soft)。 */
175
- export function snapshotState() {
176
- const out = {};
177
- for (const [id, selector] of stateSlices) {
178
- try {
179
- out[id] = selector();
180
- }
181
- catch (e) {
182
- out[id] = { error: `state slice threw: ${e instanceof Error ? e.message : String(e)}` };
183
- }
184
- }
185
- return out;
186
- }
187
- /** manifest(POST /:sid/ui-manifest 的 payload)—— 只含可序列化声明,函数永不出墙。
188
- * 它是编排层 trust-gate 的权限输入:capability 必须如实声明。 */
189
- export function buildManifest() {
190
- const out = [];
191
- for (const def of actions.values()) {
192
- out.push({
193
- id: def.id,
194
- title: def.title,
195
- ...(def.description ? { description: def.description } : {}),
196
- inputSchema: def.schema ?? { type: 'object', properties: {} },
197
- capability: def.capability,
198
- ...(def.surface ? { surface: def.surface } : {}),
199
- ...(def.timeoutMs ? { timeoutMs: def.timeoutMs } : {}),
200
- ...(def.firstClass ? { firstClass: true } : {}),
201
- });
202
- }
203
- return out;
204
- }
205
- /** 测试用:清空注册表(生产代码不要调)。 */
206
- export function __resetRegistryForTest() {
207
- actions.clear();
208
- stateSlices.clear();
209
- changeListeners.clear();
210
- }