@jhh96/aiopsdk
v0.1.0
Published
面向 AI 指令下发 / 新手引导 / 远程配置的能力(capability)总线层:registry + dispatch 调度模型 + manifest 能力清单树
Downloads
43
Readme
aiopsdk
面向 AI 指令下发 / 新手引导 / 远程配置 的「能力(capability)总线」层。 把页面里跨页公用的能力(滚动、高亮、筛选……)抽象成统一的
registry +
dispatch(name, payload)调度模型
并附带一棵结构化的 manifest 能力清单树,把"有哪些能力、在哪、参数怎么填" 暴露给 AI / 后端 / 业务调用方。
目录
能力一览
| 能力 | dispatch 命名空间 | 命令式入口 | 内部实现 | 是否需要 controller |
|---|---|---|---|---|
| scroll | scroll.to / scroll.toTop | useScroll() | scrollToTarget | 否(DOM 全局可用) |
| highlight | highlight.show / highlight.hide | useHighlight() | showHighlight | 否 |
| filter | filter.apply / .reset / .get / .list / .options | useFilter() | filterCore | 是(业务状态) |
| manifest | (自身不调度,提供描述) | useManifestRegistry() / exportManifest() | registry.getTree | 否 |
三层模型
┌─────────────────────────────────────────────────────────────┐
│ 调用方 │
│ ① dispatch('scroll.to', {...}) ← AI / 引导流程(runtime)│
│ ② useScroll().scrollTo({...}) ← 开发者写代码(静态) │
└────────────┬───────────────────────────────┬────────────────┘
│ │
┌─────────▼─────────┐ ┌────────▼─────────┐
│ core registry │ │ composable │
│ (Pinia store) │ │ (useFoo 薄封装) │
└─────────┬──────────┘ └────────┬─────────┘
│ dispatch │ 直接调用
▼ ▼
┌──────────────────────────────────────┐
│ 能力实现(纯函数 / controller 适配) │
│ scrollToTarget / showHighlight │
│ applyFilterOp / FilterController │
└──────────────────────────────────────┘核心设计:两条入口共用一份纯逻辑,行为可证明一致。
装配步骤
main.ts 只需 4 行:
import { createApp } from 'vue'
import { CapabilitiesPlugin, setFilterController, useManifestRegistry } from 'aiopsdk'
import App from './App.vue'
import { useFilterController } from './composables/useFilterController'
import { PAGES } from './manifest' // import.meta.glob 收集的 *.manifest.ts
const app = createApp(App)
// 1) 仅 filter 能力需要业务状态:注入 controller(DI 接口)
setFilterController(useFilterController())
// 2) 注入 manifest 描述树(build-time 收集,运行时一次性灌入)
useManifestRegistry().setPages(PAGES)
// 3) 装所有能力 plugin(聚合器:scroll + highlight + filter 一次到位)
app.use(CapabilitiesPlugin)
app.mount('#app')前置约束:setFilterController 必须在 app.use(CapabilitiesPlugin) 之前;
filter plugin 在 install 时只注册 dispatch handler,真正 dispatch 时才读 holder。
调度式与命令式双入口
每个能力都提供两条路径,共享同一份纯逻辑:
调度式(runtime 解耦)—— 给 AI / 引导流程 / 远程下发用
import { useCapabilityRegistry } from 'aiopsdk'
const reg = useCapabilityRegistry()
const res = await reg.dispatch('scroll.to', { selector: '#module-x', offset: -56 })
// => { ok: true, data: { found: true, scrolled: true } }
// 未注册的动作稳定返回:{ ok: false, error: 'action not active: scroll.to' }特性:
- 永不抛异常,永远返回
CapabilityResult—— 上游可直接根据ok/error判定重试或跳过。 - 未注册的 action 返回稳定的错误码
'action not active: ${name}',便于 AI 决策。 - 故意不给 dispatch 加泛型:handler 存进 store 后类型已擦除,
<T>是单方面的 cast。 需要类型保护时走命令式入口。
命令式(静态调用)—— 给开发者用,带完整类型推断
import { useScroll, useHighlight, useFilter } from 'aiopsdk'
const { scrollTo } = useScroll()
await scrollTo({ selector: '#module-x', offset: -56 })
const { show, hide } = useHighlight()
const { id } = await show({ selector: '#btn-go', timeout: 3000 })
const { apply } = useFilter()
await apply({ field: '岗位类别', values: ['技术'], mode: 'add' })FilterController 接口契约
filter 的本质是改一份业务状态,存哪(Pinia store / 组件 state)由 FilterController 决定,
shared 只定义语义与算子。这是 Hexagonal Architecture 的端口与适配器模式:
// shared 定义端口
export interface FilterController {
get(): Promise<FilterState> // 读
listFields(): Promise<FilterFieldMeta[]> // 读
options(field: string): Promise<FilterValue[]> // 读
set(state: FilterState): Promise<void> // 写
apply(op: FilterOperation): Promise<FilterState> // 写
}
// src 实现适配器
export const useFilterController = (): FilterController => ({ ... })
setFilterController(useFilterController()) // main.ts 注入一次全异步(前瞻设计)
本地阶段 controller 内部其实是同步的(直接改 store),但接口全部签 Promise。
为什么:接口化(远程组合筛选)那天,controller 实现内部从"同步过滤"改成 "await fetch + 落 store",对外契约 0 改动,AI 链路 / 命令式调用零改动。
holder 未注入时的退化
setFilterController 未调用时:
- 调度式入口
dispatch('filter.xxx')统一返回{ ok: false, error: 'filter controller not registered' }。 - 命令式入口
useFilter().apply(...)返回null。
两种路径都优雅退化,shared 包单独 build / 单独被引用时不会崩。
Manifest 能力清单树
把"整页布局 + 每个节点挂哪些可被 dispatch 的动作"用一棵纯数据树暴露出去。
描述与运行时分离
一份能力由两半组成,性质不同,必须分开存放:
| 半边 | 写在哪 | 收集时机 | 例 |
|---|---|---|---|
| 描述(action/params/说明) | *.manifest.ts 同伴文件 | build-time(import.meta.glob eager) | { action: 'drilldown', params: [...] } |
| handler(怎么执行) | 组件 useManifestNode(...) | runtime(组件 mount 时) | drilldown: ({team}) => showTalentList(...) |
关键收益:即便某页组件没 mount,它的 .manifest.ts 默认导出仍在树里 ——
满足"AI 可调用其他页面能力"的诉求。
描述形态 vs wire 形态
作者写的是 action(局部名),但 exportManifest 时自动拼成 dispatch(调度全名):
// 作者这样写(*.manifest.ts)
{
id: 'supplyAge',
capabilities: [{ action: 'exportImage', description: '...' }]
}
// getTree() 拼成 wire 形态(AI / 后端实际看到)
{
id: 'supplyAge',
capabilities: [{ dispatch: 'supplyAge.exportImage', description: '...' }]
}输出格式(拆分后的文件族)
vite-plugin-manifest 在 build / dev 启动期生成一族 JSON,按路由 name 拆分
(便于单页维护、git diff 精准定位、AI 按需加载):
public/
manifest.json # 索引清单:列文件路径,不含能力本体
manifest/
globals.json # 全局能力(scroll.* / highlight.*)
<RouteName>.json # 各页面能力树(含 scopes + components)索引清单(极小,AI 拉这一份即可发现所有子文件):
{
"version": "1.0",
"generatedAt": 1783494150000,
"files": {
"globals": "manifest/globals.json",
"pages": ["manifest/Dashboard.json"]
}
}AI 加载流程:
fetch('/manifest.json')拿索引 → 得到 globals + 各 page JSON 的相对路径。- 按需
fetch('/manifest/globals.json')与fetch('/manifest/<RouteName>.json')。 - 单页能力变化时,MVP 文件中只有
<RouteName>.json重写——content-level 幂等 保证不变的字节不被改写(generatedAt只在结构真变化时更新,不会再驱动 IO)。
schema 版本契约:MANIFEST_SCHEMA_VERSION = '1.0' as const。若结构破坏性变更,
bump 版本,让上游 AI / 后端做兼容校验。
命名约定与生命周期
dispatch 命名空间
| 类型 | 作者写法 | dispatch 全名 |
|---|---|---|
| 全局能力(globals) | 直接写完整字面量 | scroll.to / highlight.show / filter.apply |
| 节点能力 | 写 action | ${nodeId}.${action},如 supplyAge.drilldown |
handler 生命周期
| 来源 | 注册时机 | 注销时机 |
|---|---|---|
| 全局能力(plugin install) | app 启动 | app 销毁(一般无需手动清理) |
| 节点能力(useManifestNode) | 组件 onMounted | 组件 onBeforeUnmount(自动) |
组件卸载后,AI 调用 dispatch('supplyAge.exportImage') 会拿到
'action not active: supplyAge.exportImage' 而非命中已销毁的 dead handler。
能力清单(注册项)
scroll
| dispatch | 入参 | 说明 |
|---|---|---|
| scroll.to | { selector, offset?, behavior?, wait? } | 滚动到元素;找不到元素返回 ok:false,AI 可据此重试 |
| scroll.toTop | — | 整页回顶 |
highlight
| dispatch | 入参 | 说明 |
|---|---|---|
| highlight.show | { selector, timeout?, wait? } | 聚光灯高亮;timeout=0 常驻、>0 自动隐藏 |
| highlight.hide | { id? } | 隐藏某实例;不传 id 隐藏全部 |
返回值带 id,可后续精确隐藏。
filter
| dispatch | 入参 | 说明 |
|---|---|---|
| filter.apply | { field, values, mode? } | 应用一次筛选;非法值过滤,data.invalid 返回明细 |
| filter.reset | { field? } | 清空全部 / 单字段 |
| filter.get | — | 当前状态快照(字段名 → 选中队列) |
| filter.list | — | 所有字段元信息(label/field/type),让 AI 知道有哪些维度 |
| filter.options | { field } | 某字段合法值词表(基于已加载数据动态产出) |
mode: set(覆盖)、add(追加去重)、remove(移除)。
空值约定(避免 AI 误传清空状态):
set空 values → 清空字段add空 values → no-opremove空 values → no-op(整字段清空走reset({field}))
性能与副作用声明
性能优化
waitForSelectorrAF 合批:MutationObserver 每 mutation 不立即 querySelector, 合并到下一帧。大列表懒加载场景下避免观测到上千次 mutation、每次同步扫 DOM。- highlight 位置同步 rAF 合批:scroll / resize 期间合并到单帧对齐一次。
sideEffects 声明
package.json 已精确声明带副作用的文件(不是粗暴的 false):
"sideEffects": [
"./src/highlight/styles.ts", // 首次 import 时 inject <style>
"./src/**/plugin.ts", // install 时 register handler
"./src/plugins.ts"
]目的:让 bundler 在 false 时不会误把这些 install-time 注册逻辑砍掉。
维护约定
新增一个能力(最简)
- 在
src/<能力名>/下建子目录,按现有 scroll/highlight/filter 的分层实现:types.ts:对外契约xxxCore.ts或xxx.ts:纯函数实现(不依赖 Vue / Pinia)composable.ts:useFoo命令式入口plugin.ts:调度式入口(install 时 register 到 registry)index.ts:barrel 导出
- 在
src/plugins.ts的capabilityPlugins数组里加一行。 - 在
src/manifest/globals.ts列出对外 dispatch 动作(与 plugin.ts 的 register 1:1)。 - 在
src/index.ts顶层 barrel re-export。
仅此 4 步,main.ts 不需要改动。
维护 check
- 每个 plugin.ts 注册的 action,要和 globals.ts 的描述 1:1 对应(漂移会让 AI 调到「描述有、运行时没有」的死项)。
*.manifest.ts里写的 action,要和 vue 组件useManifestNode注册的 handler 同 key。- 改 schema 结构时同步 bump
MANIFEST_SCHEMA_VERSION。
