aihand 0.0.1 → 0.1.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 +136 -2
- package/dist/chunk-2NTK7H4W.js +10 -0
- package/dist/chunk-3X4FTHLC.cjs +369 -0
- package/dist/chunk-BXVNR4E2.js +399 -0
- package/dist/chunk-C7DGE6MY.cjs +1456 -0
- package/dist/chunk-DUUCVLC3.cjs +254 -0
- package/dist/chunk-FAHI53KO.cjs +125 -0
- package/dist/chunk-G7KVJ7NF.js +369 -0
- package/dist/chunk-GNEUSRGP.js +52 -0
- package/dist/chunk-IGNEAOLT.cjs +130 -0
- package/dist/chunk-IS5XFUDB.js +125 -0
- package/dist/chunk-JLYC76XL.js +2448 -0
- package/dist/chunk-KQOABC2O.cjs +52 -0
- package/dist/chunk-OVMK33AC.cjs +104 -0
- package/dist/chunk-OWYK2IGV.js +250 -0
- package/dist/chunk-PQSQN4CN.js +126 -0
- package/dist/chunk-QF6AG3M5.cjs +410 -0
- package/dist/chunk-QSAMLXML.js +1456 -0
- package/dist/chunk-VEKYRKPF.cjs +399 -0
- package/dist/chunk-Y6H7W7PI.cjs +2451 -0
- package/dist/chunk-YKSYW77R.js +410 -0
- package/dist/chunk-Z2Y65YOY.cjs +7 -0
- package/dist/chunk-ZJQRNIK7.js +104 -0
- package/dist/cli-FDS2C2CZ.cjs +651 -0
- package/dist/cli-HHRGYPSM.js +649 -0
- package/dist/cli-JQEIE7RQ.js +120 -0
- package/dist/cli-K3OS2QQH.cjs +122 -0
- package/dist/cli-OSYG6LJD.cjs +89 -0
- package/dist/cli-TXRW5PG6.js +89 -0
- package/dist/cli.cjs +81 -0
- package/dist/cli.js +81 -0
- package/dist/config-5KEQLN6L.cjs +13 -0
- package/dist/config-PJPYKDLQ.js +13 -0
- package/dist/graph-IH56SCPK.js +8 -0
- package/dist/graph-ZUXXCJ5A.cjs +8 -0
- package/dist/index.cjs +481 -0
- package/dist/index.d.cts +461 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +479 -0
- package/dist/locate-5XFSXJ5J.cjs +15 -0
- package/dist/locate-NKSUGL3A.js +15 -0
- package/dist/refactor-5FWSZIBN.cjs +19 -0
- package/dist/refactor-BOB3SZSA.js +19 -0
- package/dist/scan-4R7GQG2W.cjs +9 -0
- package/dist/scan-VF54GAAX.js +9 -0
- package/dist/ui/probe/server.cjs +505 -0
- package/dist/ui/probe/server.js +507 -0
- package/dist/vite.cjs +12 -0
- package/dist/vite.d.cts +12 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +12 -0
- package/package.json +82 -9
- package/src/cli.ts +107 -0
- package/src/index.ts +54 -0
- package/src/read/cli.ts +650 -0
- package/src/read/compact.ts +286 -0
- package/src/read/config.ts +62 -0
- package/src/read/graph.ts +182 -0
- package/src/read/index.ts +12 -0
- package/src/read/inject.ts +121 -0
- package/src/read/locate.ts +104 -0
- package/src/read/panel.ts +335 -0
- package/src/read/pipeline.ts +78 -0
- package/src/read/refactor.ts +576 -0
- package/src/read/render.ts +1118 -0
- package/src/read/scan.ts +61 -0
- package/src/read/seam.ts +0 -0
- package/src/read/security.ts +171 -0
- package/src/read/signals.ts +333 -0
- package/src/read/state.ts +71 -0
- package/src/read/stategraph.ts +205 -0
- package/src/read/types.ts +162 -0
- package/src/read/vite.ts +77 -0
- package/src/ui/babel/line-profiler.ts +197 -0
- package/src/ui/babel/source-loc.ts +68 -0
- package/src/ui/bridge/cdp-bridge.ts +138 -0
- package/src/ui/bridge/compile-probe.ts +80 -0
- package/src/ui/bridge/transport.ts +26 -0
- package/src/ui/bridge/vite-bridge.ts +116 -0
- package/src/ui/client/client-patch.ts +899 -0
- package/src/ui/client/client.ts +2562 -0
- package/src/ui/core/action.ts +747 -0
- package/src/ui/core/candidates.ts +348 -0
- package/src/ui/core/canvas.ts +305 -0
- package/src/ui/core/check.ts +34 -0
- package/src/ui/core/compact.ts +314 -0
- package/src/ui/core/detail.ts +244 -0
- package/src/ui/core/diff.ts +253 -0
- package/src/ui/core/emit.ts +198 -0
- package/src/ui/core/knob-exec.ts +137 -0
- package/src/ui/core/perf.ts +254 -0
- package/src/ui/core/types.ts +164 -0
- package/src/ui/core/util.ts +221 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/probe/cli.ts +139 -0
- package/src/ui/probe/server.ts +468 -0
- package/src/ui/self/act.ts +47 -0
- package/src/ui/self/discover.ts +101 -0
- package/src/ui/self/grow.ts +121 -0
- package/src/ui/self/install.ts +100 -0
- package/src/ui/self/probe.ts +105 -0
- package/src/ui/self/screen-hook.ts +44 -0
- package/src/ui/self/self.ts +48 -0
- package/src/ui/self/store-refs.ts +123 -0
- package/src/ui/self/store-schema.ts +65 -0
- package/src/ui/self/synth.ts +37 -0
- package/src/ui/server/cli.ts +102 -0
- package/src/ui/server/dispatch.ts +276 -0
- package/src/ui/server/help-text.ts +237 -0
- package/src/ui/server/knob-schema.ts +87 -0
- package/src/ui/server/plugin.ts +1151 -0
- package/src/vite.ts +39 -0
- package/index.js +0 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// 多 store 自动发现 —— Node 端(build/cold,走神经图引擎 stategraph.ts)。
|
|
2
|
+
//
|
|
3
|
+
// store 不只在 src/store/ —— imUIStore 在 features/IM、roleSettingModalStore 在 ChatHeader、
|
|
4
|
+
// leftDrawerStore 在 mobile。被旋钮写到的 store 散落全仓,domain 要闭环就得扫到它们全部。
|
|
5
|
+
// 旧实现 fs 递归 + 逐文件 tree-sitter grow(只认 callee 文本 === 'create',换别名/换工厂名漏)。
|
|
6
|
+
// 现统一到引擎:ts-morph 的 type checker 按返回类型的响应式 marker 字段认 store(构造无关,
|
|
7
|
+
// 不看 callee 名),一次长出全仓状态子图。marker 由调用方透传(默认 '_loading')。
|
|
8
|
+
//
|
|
9
|
+
// 引擎离线:跑在 build/cold load,绝不进浏览器、绝不进 per-HMR 热路径(ts-morph 拖整个 Project)。
|
|
10
|
+
// grow.ts 的 tree-sitter 路径作浏览器零-AST 兜底保留,但真理源是这里。
|
|
11
|
+
// 与 synth.ts 拆开:synth 是纯函数(浏览器安全),discover 拖 ts-morph(Node 限定)。
|
|
12
|
+
|
|
13
|
+
import type { Grown } from './grow'
|
|
14
|
+
import { existsSync, statSync } from 'node:fs'
|
|
15
|
+
import { dirname, resolve } from 'node:path'
|
|
16
|
+
import { buildStateGraph, openProject } from '../../read/stategraph.js'
|
|
17
|
+
|
|
18
|
+
// 从 root 向上走找到含 tsconfig(.app) 的项目根。引擎按 cwd 探 tsconfig,但 discover 的 roots
|
|
19
|
+
// 是 app 的源目录(可能不在 aihand 的 cwd 下,如单测 process.cwd()=packages/aihand),
|
|
20
|
+
// 故从 roots 反推真正的项目根,引擎才能解出 app 的源图(而非 aihand 自己的)。
|
|
21
|
+
function projectRootFor(start: string): string {
|
|
22
|
+
let dir = statSync(start).isDirectory() ? start : dirname(start)
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (['tsconfig.app.json', 'tsconfig.json'].some(f => existsSync(resolve(dir, f))))
|
|
25
|
+
return dir
|
|
26
|
+
const up = dirname(dir)
|
|
27
|
+
if (up === dir)
|
|
28
|
+
return start // 没找到 → 退回 start,openProject 会自行报错
|
|
29
|
+
dir = up
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 把一个根归一成绝对路径前缀,用于判断引擎长出的节点(绝对文件路径)是否落在该根下。
|
|
34
|
+
// 目录 → 加尾斜杠(前缀匹配避免 src/IM 误吞 src/IMx);单文件 → 精确路径。
|
|
35
|
+
function scopePrefix(root: string): string {
|
|
36
|
+
const abs = resolve(root)
|
|
37
|
+
try {
|
|
38
|
+
return statSync(abs).isDirectory() ? `${abs}/` : abs
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return abs
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// roots = 目录(递归)或单文件,任意混合。开一次 Project,跑状态子图,过滤落在 roots 内的 store。
|
|
46
|
+
// anchor 用绝对路径长出(供 roots 前缀过滤),再缩成末两段保持运行时契约(与旧 grow 同形)。
|
|
47
|
+
// 返回按中心性(rank)降序(StateNode extends Grown,中心性字段对运行时是无害附加)。
|
|
48
|
+
export async function discover(roots: string | string[], marker: string = '_loading'): Promise<Grown[]> {
|
|
49
|
+
return (await discoverWithPaths(roots, marker)).map(({ abs: _abs, ...n }) => {
|
|
50
|
+
const i = n.anchor.lastIndexOf(':')
|
|
51
|
+
const abs = n.anchor.slice(0, i)
|
|
52
|
+
return { ...n, anchor: `${abs.split('/').slice(-2).join('/')}${n.anchor.slice(i)}` }
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 同 discover,但 anchor 不缩末两段,额外带未截断的绝对路径 abs。store-refs 用它 codegen import:
|
|
57
|
+
// 末两段 anchor(store/appUI.ts)对深层 store(features/IM/shared/store.ts → shared/store.ts)
|
|
58
|
+
// 歧义、不可 import;绝对路径才能精确解出 @/ 别名路径。create() store 的真路径源。
|
|
59
|
+
export async function discoverWithPaths(
|
|
60
|
+
roots: string | string[],
|
|
61
|
+
marker: string = '_loading',
|
|
62
|
+
): Promise<Array<Grown & { abs: string }>> {
|
|
63
|
+
const rawRoots = Array.isArray(roots) ? roots : [roots]
|
|
64
|
+
const list = rawRoots.map(scopePrefix)
|
|
65
|
+
const project = await openProject(projectRootFor(resolve(rawRoots[0])))
|
|
66
|
+
const { nodes } = buildStateGraph(project, abs => abs, marker)
|
|
67
|
+
return nodes
|
|
68
|
+
.filter((n) => {
|
|
69
|
+
const fp = n.anchor.slice(0, n.anchor.lastIndexOf(':'))
|
|
70
|
+
return list.some(p => p.endsWith('/') ? fp.startsWith(p) : fp === p)
|
|
71
|
+
})
|
|
72
|
+
.map(n => ({ ...n, abs: n.anchor.slice(0, n.anchor.lastIndexOf(':')) }))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 按名定位 module-level export const 的声明文件(绝对路径)。store-refs 用它找 facade store
|
|
76
|
+
// (botStore/memoryStore 是 plain-object,无 _loading marker,buildStateGraph 看不见),
|
|
77
|
+
// 但它们仍是 `export const X = {...}` —— ts-morph 按变量名扫一次即得真路径。
|
|
78
|
+
// names 命中即停,返回 名→abs;漏的名(理论上不该有)不进 map,调用方据缺失诚实报错。
|
|
79
|
+
export async function locateExports(
|
|
80
|
+
roots: string | string[],
|
|
81
|
+
names: Set<string>,
|
|
82
|
+
): Promise<Map<string, string>> {
|
|
83
|
+
const rawRoots = Array.isArray(roots) ? roots : [roots]
|
|
84
|
+
const project = await openProject(projectRootFor(resolve(rawRoots[0])))
|
|
85
|
+
const out = new Map<string, string>()
|
|
86
|
+
for (const sf of project.getSourceFiles()) {
|
|
87
|
+
const fp = sf.getFilePath()
|
|
88
|
+
if (fp.endsWith('.d.ts') || fp.includes('/node_modules/'))
|
|
89
|
+
continue
|
|
90
|
+
for (const v of sf.getVariableDeclarations()) {
|
|
91
|
+
const name = v.getName()
|
|
92
|
+
if (!names.has(name) || out.has(name))
|
|
93
|
+
continue
|
|
94
|
+
// module-level(变量语句的直接父是 SourceFile)—— 局部同名变量不算 export。
|
|
95
|
+
const stmt = v.getVariableStatement()
|
|
96
|
+
if (stmt && stmt.isExported())
|
|
97
|
+
out.set(name, fp)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// 第一堆的真考验:Self 的结构能否从真实代码静态长出 —— 零模型,纯 tree-sitter AST。
|
|
2
|
+
//
|
|
3
|
+
// 命题精确形式:aipeek 要手挂 window.__AIPEEK_SCREEN__ = () => ({ view: appUIStore.mode, … })
|
|
4
|
+
// 才能让探针读到「view 能取哪些值、有哪些语义状态」。但这些东西 —— 字段名、值域(联合类型
|
|
5
|
+
// 的字面量集)、声明在哪行 —— 源码里本就写着。手挂那行是在重新声明已声明的东西。
|
|
6
|
+
//
|
|
7
|
+
// 所以「从代码静态长出 Self」拆开才成立:静态长出的是【结构】(字段 + 值域 + anchor),
|
|
8
|
+
// 不是【值】(此刻是 chat 还是 settings —— 那是运行时,归 screen 投影那一半)。grow 长结构,
|
|
9
|
+
// screen 投影值,两半合起来才是完整的 self-rep。这一文件证的是:结构无需手挂,AST 即得。
|
|
10
|
+
//
|
|
11
|
+
// 禁正则铁律:对象字面量 + 类型注解是「有语法的东西」,只能走 AST。用 repodex 同款
|
|
12
|
+
// web-tree-sitter(wasm,零模型纯语法树),不引入任何模型。
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
|
+
import { Language, Parser } from 'web-tree-sitter'
|
|
17
|
+
|
|
18
|
+
// 一个语义字段的静态结构。domain 非空 = 这是个有限值域的「档位」(联合字面量类型),
|
|
19
|
+
// 正是 aipeek 手挂进 __AIPEEK_SCREEN__ 的那种 view/mode 变量;domain 空 = 普通状态。
|
|
20
|
+
export interface Field {
|
|
21
|
+
name: string
|
|
22
|
+
type: string // 'string' | 'boolean' | 'string|null' | 'string[]' | union…
|
|
23
|
+
domain: string[] // 联合字面量的取值集,如 ['chat','im','workbench'];无则 []
|
|
24
|
+
line: number // 1-based,声明所在源码行 —— 这就是 anchor 的来源
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 一个 store 声明长出的结构投影。anchor 指向声明本身;fields 是它暴露的语义状态 schema。
|
|
28
|
+
export interface Grown {
|
|
29
|
+
store: string // 变量名,如 'appUIStore'
|
|
30
|
+
anchor: string // 'file:line',声明所在(file 由调用方给,line 由 AST 给)
|
|
31
|
+
fields: Field[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let langPromise: Promise<Language> | null = null
|
|
35
|
+
function loadLang(): Promise<Language> {
|
|
36
|
+
if (!langPromise) {
|
|
37
|
+
langPromise = (async () => {
|
|
38
|
+
const req = createRequire(import.meta.url)
|
|
39
|
+
const wasmDir = dirname(req.resolve('@repomix/tree-sitter-wasms/out/tree-sitter-tsx.wasm'))
|
|
40
|
+
await Parser.init()
|
|
41
|
+
return Language.load(join(wasmDir, 'tree-sitter-tsx.wasm'))
|
|
42
|
+
})()
|
|
43
|
+
}
|
|
44
|
+
return langPromise
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 从一个 pair 节点的 value 侧推出 type + domain。两种形状:
|
|
48
|
+
// as_expression(value as Type) → 读 Type 那一侧
|
|
49
|
+
// 裸 value(true/false/数字/字符串)→ 从字面量推原始类型
|
|
50
|
+
function typeOf(value: import('web-tree-sitter').Node): { type: string, domain: string[] } {
|
|
51
|
+
if (value.type === 'as_expression') {
|
|
52
|
+
// as_expression = [valueExpr, typeAnnotation];取类型注解(最后一个具名子节点)
|
|
53
|
+
const typeNode = value.namedChild(value.namedChildCount - 1)
|
|
54
|
+
if (!typeNode)
|
|
55
|
+
return { type: 'unknown', domain: [] }
|
|
56
|
+
// 联合字面量类型 → 值域。收集所有 literal_type 下的 string_fragment。
|
|
57
|
+
const literals = typeNode.descendantsOfType('literal_type')
|
|
58
|
+
const domain = literals
|
|
59
|
+
.map(l => l.descendantsOfType('string_fragment')[0]?.text)
|
|
60
|
+
.filter((s): s is string => !!s)
|
|
61
|
+
return { type: typeNode.text, domain }
|
|
62
|
+
}
|
|
63
|
+
// 裸字面量 → 原始类型
|
|
64
|
+
if (value.type === 'true' || value.type === 'false')
|
|
65
|
+
return { type: 'boolean', domain: [] }
|
|
66
|
+
if (value.type === 'number')
|
|
67
|
+
return { type: 'number', domain: [] }
|
|
68
|
+
if (value.type === 'string')
|
|
69
|
+
return { type: 'string', domain: [] }
|
|
70
|
+
if (value.type === 'array')
|
|
71
|
+
return { type: 'array', domain: [] }
|
|
72
|
+
if (value.type === 'null')
|
|
73
|
+
return { type: 'null', domain: [] }
|
|
74
|
+
return { type: value.type, domain: [] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 静态长出:扫源码,找 `const X = create({ …state… }, …)`,提取 state 对象的字段结构。
|
|
78
|
+
// file 仅用于拼 anchor(AST 不知道自己的路径)。同步契约依赖 lang 已加载,故是 async。
|
|
79
|
+
export async function grow(code: string, file: string): Promise<Grown[]> {
|
|
80
|
+
const lang = await loadLang()
|
|
81
|
+
const parser = new Parser()
|
|
82
|
+
parser.setLanguage(lang)
|
|
83
|
+
const tree = parser.parse(code)
|
|
84
|
+
const root = tree?.rootNode
|
|
85
|
+
if (!root)
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
const out: Grown[] = []
|
|
89
|
+
// 找所有 `identifier = create(object, …)` 形态的 variable_declarator
|
|
90
|
+
for (const decl of root.descendantsOfType('variable_declarator')) {
|
|
91
|
+
const nameNode = decl.namedChild(0)
|
|
92
|
+
const valueNode = decl.childForFieldName('value')
|
|
93
|
+
if (!nameNode || nameNode.type !== 'identifier' || valueNode?.type !== 'call_expression')
|
|
94
|
+
continue
|
|
95
|
+
const callee = valueNode.namedChild(0)
|
|
96
|
+
if (callee?.type !== 'identifier' || callee.text !== 'create')
|
|
97
|
+
continue
|
|
98
|
+
const args = valueNode.childForFieldName('arguments')
|
|
99
|
+
const stateObj = args?.namedChildren.find(c => c?.type === 'object')
|
|
100
|
+
if (!stateObj)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
const fields: Field[] = []
|
|
104
|
+
for (const pair of stateObj.namedChildren) {
|
|
105
|
+
if (pair?.type !== 'pair')
|
|
106
|
+
continue
|
|
107
|
+
const key = pair.childForFieldName('key')
|
|
108
|
+
const value = pair.childForFieldName('value')
|
|
109
|
+
if (!key || !value)
|
|
110
|
+
continue
|
|
111
|
+
const { type, domain } = typeOf(value)
|
|
112
|
+
fields.push({ name: key.text, type, domain, line: key.startPosition.row + 1 })
|
|
113
|
+
}
|
|
114
|
+
out.push({
|
|
115
|
+
store: nameNode.text,
|
|
116
|
+
anchor: `${file}:${decl.startPosition.row + 1}`,
|
|
117
|
+
fields,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// self-log 的运行时落地 —— 第二堆命题的真证据。
|
|
2
|
+
//
|
|
3
|
+
// fixture 里手填 channel 只证了「形状能收敛」;这里证「真触发能收敛」:真 patch 五路
|
|
4
|
+
// 浏览器副作用(console/fetch/XHR/error/SSE),五路出口全调同一个 log。aihand runtime 在这五处
|
|
5
|
+
// 各 push 各自的 ring buffer(console/network/errors 三个缓冲,五种采集形状);这里五路
|
|
6
|
+
// 坍缩成一个 append。物理债没被消灭(副作用照样发生),但它的形状从五叉变一条线。
|
|
7
|
+
//
|
|
8
|
+
// 拦截手法本身(tee body / XHR loadend / unhandledrejection / EventSource Proxy)是物理债,
|
|
9
|
+
// 复用既有做法,不重写。新的只有「出口统一」这一点 —— 它把 self.ts 的 log 纯函数接到真触发。
|
|
10
|
+
|
|
11
|
+
import { log, type LogEntry } from './self'
|
|
12
|
+
|
|
13
|
+
export interface Sink {
|
|
14
|
+
seq: LogEntry[]
|
|
15
|
+
now: () => number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 五路的唯一出口。每一路 patch 只负责把副作用翻译成 (channel, detail),append 是同一个。
|
|
19
|
+
function emit(sink: Sink, channel: LogEntry['channel'], detail: string) {
|
|
20
|
+
sink.seq = log(sink.seq, { channel, detail, ts: sink.now() })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 返回 restore:把五路 patch 全部还原(测试可隔离,真页面可热重载)。
|
|
24
|
+
export function installSelfLog(win: Window & typeof globalThis, sink: Sink): () => void {
|
|
25
|
+
const restores: Array<() => void> = []
|
|
26
|
+
|
|
27
|
+
// ── console ──
|
|
28
|
+
const levels = ['log', 'info', 'warn', 'error', 'debug'] as const
|
|
29
|
+
for (const level of levels) {
|
|
30
|
+
const orig = win.console[level]
|
|
31
|
+
win.console[level] = (...args: unknown[]) => {
|
|
32
|
+
emit(sink, 'console', `[${level}] ${args.map(String).join(' ')}`)
|
|
33
|
+
orig.apply(win.console, args)
|
|
34
|
+
}
|
|
35
|
+
restores.push(() => { win.console[level] = orig })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── fetch ──
|
|
39
|
+
const origFetch = win.fetch
|
|
40
|
+
win.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
41
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
|
|
42
|
+
const method = init?.method || 'GET'
|
|
43
|
+
try {
|
|
44
|
+
const res = await origFetch(input, init)
|
|
45
|
+
emit(sink, 'fetch', `${method} ${url} ${res.status}`)
|
|
46
|
+
return res
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
emit(sink, 'fetch', `${method} ${url} failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
50
|
+
throw e
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
restores.push(() => { win.fetch = origFetch })
|
|
54
|
+
|
|
55
|
+
// ── XHR ──
|
|
56
|
+
const XHRProto = win.XMLHttpRequest.prototype
|
|
57
|
+
const origOpen = XHRProto.open
|
|
58
|
+
const origSend = XHRProto.send
|
|
59
|
+
interface Tracked { __m?: string, __u?: string }
|
|
60
|
+
XHRProto.open = function (this: XMLHttpRequest & Tracked, method: string, url: string | URL, ...rest: unknown[]) {
|
|
61
|
+
this.__m = method
|
|
62
|
+
this.__u = typeof url === 'string' ? url : url.toString()
|
|
63
|
+
// @ts-expect-error variadic passthrough to the native signature
|
|
64
|
+
return origOpen.call(this, method, url, ...rest)
|
|
65
|
+
}
|
|
66
|
+
XHRProto.send = function (this: XMLHttpRequest & Tracked, ...args: unknown[]) {
|
|
67
|
+
this.addEventListener('loadend', () => {
|
|
68
|
+
emit(sink, 'xhr', `${this.__m ?? 'GET'} ${this.__u ?? ''} ${this.status}`)
|
|
69
|
+
})
|
|
70
|
+
// @ts-expect-error variadic passthrough to the native signature
|
|
71
|
+
return origSend.apply(this, args)
|
|
72
|
+
}
|
|
73
|
+
restores.push(() => { XHRProto.open = origOpen; XHRProto.send = origSend })
|
|
74
|
+
|
|
75
|
+
// ── error + unhandledrejection ──
|
|
76
|
+
const onError = (e: ErrorEvent) => emit(sink, 'error', e.message || String(e.error))
|
|
77
|
+
const onRejection = (e: PromiseRejectionEvent) => emit(sink, 'error', `unhandled rejection: ${String(e.reason)}`)
|
|
78
|
+
win.addEventListener('error', onError)
|
|
79
|
+
win.addEventListener('unhandledrejection', onRejection)
|
|
80
|
+
restores.push(() => {
|
|
81
|
+
win.removeEventListener('error', onError)
|
|
82
|
+
win.removeEventListener('unhandledrejection', onRejection)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// ── SSE ──
|
|
86
|
+
const OrigES = win.EventSource
|
|
87
|
+
if (OrigES) {
|
|
88
|
+
win.EventSource = new Proxy(OrigES, {
|
|
89
|
+
construct(Target, cArgs) {
|
|
90
|
+
const es = new Target(...cArgs as ConstructorParameters<typeof EventSource>)
|
|
91
|
+
const url = String(cArgs[0])
|
|
92
|
+
es.addEventListener('error', () => emit(sink, 'sse', `${url} error (readyState ${es.readyState})`))
|
|
93
|
+
return es
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
restores.push(() => { win.EventSource = OrigES })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return () => { for (const r of restores) r() }
|
|
100
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// aihand 运行时探针的安装逻辑 —— 把 schema(virtual:store-schema)⋉ 运行时实例(__AIPEEK_STORES__)
|
|
2
|
+
// 接成 4 个 window 全局(__AIPEEK_SCREEN__/__AIPEEK_VIEW__/__AISELF__/__AIHAND_ACTIONS__),供 aihand
|
|
3
|
+
// /screen·/state·/action 读。这一坨原本内联在宿主 app 的 main.tsx(~90 行嵌套 .then),全是通用胶水,
|
|
4
|
+
// 只有「哪个 store 是 view 轴、哪个 facade 要枚举、哪些跨店派生量、动作表」是 app 特有 —— 那些经 ProbeConfig
|
|
5
|
+
// 传入。app 端塌成「import store-refs → installProbe(config)」两步。
|
|
6
|
+
//
|
|
7
|
+
// 框架无关:核心拓扑是「(字段值域 schema) ⋉ (运行时实例) → 投影」,与状态库无关(MobX/Zustand/Valtio/
|
|
8
|
+
// Signal 都是「命名状态容器 × 字段」)。两处曾把 MobX 实现当领域真相、现已拔成可插拔策略:
|
|
9
|
+
// ① view 轴 —— 由 config.view 显式声明(probe 不靠「第一个有限值域字段」猜)。
|
|
10
|
+
// ② facade 枚举 —— enumerateFacade 默认认 MobX observable 的 setter 描述符,其它框架可覆盖。
|
|
11
|
+
// 纯函数(synth/screenHook/act)各自单测;这里只负责把它们接到运行时 + 装窗。浏览器安全(不碰 ts-morph)。
|
|
12
|
+
|
|
13
|
+
import type { Grown } from './grow'
|
|
14
|
+
import { screenHook } from './screen-hook'
|
|
15
|
+
import { synth } from './synth'
|
|
16
|
+
|
|
17
|
+
interface Schema {
|
|
18
|
+
[store: string]: Grown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// facade 枚举策略:一个 plain-object store 暴露哪些字段当 domain。grow 静态看不见 facade(无响应式
|
|
22
|
+
// marker),只能运行时枚举。默认 = MobX observable 的「带 setter 的字段」(= 被旋钮写的可变状态);
|
|
23
|
+
// 换框架可传别的(如 Vue reactive 用 Reflect.ownKeys,Signal 用 .value 探测)。返回字段名数组。
|
|
24
|
+
export type FacadeEnumerator = (inst: object) => string[]
|
|
25
|
+
|
|
26
|
+
// 默认:MobX observable —— getOwnPropertyDescriptors 里带 set 的就是可变状态字段。
|
|
27
|
+
export const enumerateMobxSetters: FacadeEnumerator = inst =>
|
|
28
|
+
Object.entries(Object.getOwnPropertyDescriptors(inst))
|
|
29
|
+
.filter(([, d]) => d.set)
|
|
30
|
+
.map(([k]) => k)
|
|
31
|
+
|
|
32
|
+
// probe 写的 4 个全局。aihand 是独立包,不依赖宿主 app 的 ambient Window 声明 —— 在此本地结构化声明,
|
|
33
|
+
// 宿主传 window 结构上满足即可(宿主 global.d.ts 里的 __AIPEEK_* 与这里同形)。
|
|
34
|
+
export interface ProbeWindow {
|
|
35
|
+
__AIPEEK_STORES__?: Record<string, unknown>
|
|
36
|
+
__AIPEEK_SCREEN__?: () => Record<string, unknown>
|
|
37
|
+
__AIPEEK_VIEW__?: () => string
|
|
38
|
+
__AISELF__?: { self: () => unknown, schemaFieldCount: () => number, handMountedKeys: () => string[] }
|
|
39
|
+
__AIHAND_ACTIONS__?: Record<string, (...args: never[]) => unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProbeConfig {
|
|
43
|
+
// schema(virtual:store-schema 的 build 产物:store 名 → 字段/值域)。
|
|
44
|
+
schema: Schema
|
|
45
|
+
// 名→实例 映射(virtual:store-refs 的 codegen 产物)。schema∩实例 配对喂 screenHook。
|
|
46
|
+
stores: Record<string, unknown>
|
|
47
|
+
// view 轴:app 声明哪个状态是顶层视图(probe 不猜)。如 () => String(appUIStore.mode)。
|
|
48
|
+
view: () => string
|
|
49
|
+
// synth(__AISELF__.self())用哪个 store 的 schema+实例。
|
|
50
|
+
selfStore: string
|
|
51
|
+
// 跨店派生量(grow 长不出的,如 流式中/模型/会话)。并进 domain,key 即字段名。
|
|
52
|
+
derived?: Record<string, () => unknown>
|
|
53
|
+
// facade store:grow 看不见(plain object 无 marker),运行时枚举它的字段并入 domain,前缀 `${name}.`。
|
|
54
|
+
// 值取 instance[k],异常吞成 '[error]'。
|
|
55
|
+
facades?: Record<string, object>
|
|
56
|
+
// facade 字段枚举策略(框架无关注入点)。默认 enumerateMobxSetters(认 MobX observable 的 setter)。
|
|
57
|
+
enumerateFacade?: FacadeEnumerator
|
|
58
|
+
// 一等公民语义动作(对偶状态自表示):AI 直接调绕 DOM。每个动作返回 { delta }(动作改了什么)。
|
|
59
|
+
actions?: Record<string, (...args: never[]) => Promise<void>>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pick<T>(f: () => T): T | string {
|
|
63
|
+
try {
|
|
64
|
+
return f()
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return '[error]'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function installProbe(win: ProbeWindow, config: ProbeConfig): void {
|
|
72
|
+
win.__AIPEEK_STORES__ = config.stores
|
|
73
|
+
|
|
74
|
+
// schema 里有的 store(create 变量名 = export 名)按名配对运行时实例;schema 没有的(facade、纯派生量)
|
|
75
|
+
// 经 `in stores` 天然滤除 —— 一份 schema 当判据,清单自动闭环(手列 live 必漏)。
|
|
76
|
+
const snaps = Object.keys(config.schema)
|
|
77
|
+
.filter(name => name in config.stores)
|
|
78
|
+
.map(name => ({ schema: config.schema[name], snapshot: config.stores[name] as Record<string, unknown> }))
|
|
79
|
+
|
|
80
|
+
const enumerate = config.enumerateFacade ?? enumerateMobxSetters
|
|
81
|
+
win.__AIPEEK_SCREEN__ = () => {
|
|
82
|
+
const { domain } = screenHook(snaps)
|
|
83
|
+
for (const [name, inst] of Object.entries(config.facades ?? {})) {
|
|
84
|
+
for (const k of enumerate(inst))
|
|
85
|
+
domain[`${name}.${k}`] = pick(() => (inst as Record<string, unknown>)[k])
|
|
86
|
+
}
|
|
87
|
+
for (const [k, f] of Object.entries(config.derived ?? {}))
|
|
88
|
+
domain[k] = pick(f)
|
|
89
|
+
return domain
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
win.__AIPEEK_VIEW__ = config.view
|
|
93
|
+
win.__AISELF__ = {
|
|
94
|
+
self: () => synth(config.schema[config.selfStore], config.stores[config.selfStore] as Record<string, unknown>),
|
|
95
|
+
schemaFieldCount: () => Object.values(config.schema).reduce((n, s) => n + s.fields.length, 0),
|
|
96
|
+
handMountedKeys: () => Object.keys(win.__AIPEEK_SCREEN__!()),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 动作自表示:app 把「按钮 onClick 跑的 (store-write + 真语义函数)、减去 DOM」挂成裸 fn。
|
|
100
|
+
// 反馈(改了什么 + 异步流轨迹)由 client 端唯一终端 settleAndTrace 统一接管 —— 与 click/knob
|
|
101
|
+
// 同一条 waitForStable+traceFlow 路径,无分裂、无弱化。曾在此自造 before/after 同步夹 await 的
|
|
102
|
+
// act() delta,但它捕不到异步流(generateWithPrompt 首字节即 resolve → delta 永远空),且与 client
|
|
103
|
+
// 的轨迹采集重复;现已坍缩 —— 动作只管「做」,反馈一律走 client 终端。
|
|
104
|
+
win.__AIHAND_ACTIONS__ = config.actions ?? {}
|
|
105
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 命题2 的核心 —— 决定性 head-to-head:aipeek 要开发者手挂
|
|
2
|
+
// window.__AIPEEK_SCREEN__ = () => ({ view: appUIStore.mode, streaming: ..., ... })
|
|
3
|
+
// 并人工枚举哪个字段是 view、domain 有哪些 key。aiself 把这一行从源码长出:
|
|
4
|
+
// 静态引擎(stategraph)已得到字段名 + 值域 + 哪个是档位(domain 非空),
|
|
5
|
+
// screenHook 据此生成同形状对象,view 字段、domain keys 全自动。
|
|
6
|
+
//
|
|
7
|
+
// 单店 → 多店:被旋钮写的 store 散落 11 个,domain 要覆盖它们全部才能闭环(拨任一旋钮 →
|
|
8
|
+
// diff 必能看到它改的字段)。domain key 用 `store.field` 命名空间:① 避免跨店重名(多店都有
|
|
9
|
+
// loading/error)② diff 行直接读成 `botStore.filterByCurrentBot: false → true`,与旋钮态射
|
|
10
|
+
// `botStore.{filterByCurrentBot=...}` 同构。这就是「超越」的硬证据:aipeek diff.ts 自己承认
|
|
11
|
+
// domain 状态机是 DOM 投影看不见、只能靠 __AIPEEK_SCREEN__ 手挂的;aiself 让那一行不必手写。
|
|
12
|
+
|
|
13
|
+
import type { Grown } from './grow'
|
|
14
|
+
import type { Snapshot } from './synth'
|
|
15
|
+
|
|
16
|
+
// 一个 store 的 schema + 它此刻的运行时快照。screenHook 吃一批这个 → 合并 domain。
|
|
17
|
+
export interface StoreSnap {
|
|
18
|
+
schema: Grown
|
|
19
|
+
snapshot: Snapshot
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 与 aipeek ScreenSnap 同形状的投影。view = 第一个有限值域字段(domain 非空)的当前值;
|
|
23
|
+
// domain = 全店全字段,key 命名空间化为 `store.field`(零手写枚举,零跨店重名)。
|
|
24
|
+
export interface ScreenHook {
|
|
25
|
+
view: string
|
|
26
|
+
domain: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 一批 (schema, snapshot) → aipeek __AIPEEK_SCREEN__ 同形状对象。
|
|
30
|
+
// view 取第一个档位字段(有限值域),它就是 appUIStore.mode —— 把 appUIStore 放数组首位即得。
|
|
31
|
+
// domain[`${store}.${field}`] = 此刻值。挂到 window.__AIPEEK_SCREEN__,aipeek /screen 读到的
|
|
32
|
+
// domain 就来自源码声明,不来自任何手写枚举。
|
|
33
|
+
export function screenHook(stores: StoreSnap[]): ScreenHook {
|
|
34
|
+
const domain: Record<string, unknown> = {}
|
|
35
|
+
let view = ''
|
|
36
|
+
for (const { schema, snapshot } of stores) {
|
|
37
|
+
for (const f of schema.fields) {
|
|
38
|
+
domain[`${schema.store}.${f.name}`] = snapshot[f.name]
|
|
39
|
+
if (!view && f.domain.length > 0)
|
|
40
|
+
view = String(snapshot[f.name] ?? '')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { view, domain }
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// aiself — app 即自己。两堆,分治。
|
|
2
|
+
//
|
|
3
|
+
// 读完 aipeek 全部源码(5378 行)后的诚实账本:它的体量分两类。
|
|
4
|
+
//
|
|
5
|
+
// 一类是「自表示债」——fiber walk / 解 sourcemap / babel 盖 data-insp-path 戳 /
|
|
6
|
+
// 手挂 __AIPEEK_STORES__,≈1000 行。全因为 app 不声明自己,只能从黑盒逆向抠。
|
|
7
|
+
// 这一堆 aiself 删到 0:状态/结构/源码 anchor 是同一个声明,「感知自己」是读,非抠。
|
|
8
|
+
//
|
|
9
|
+
// 另一类是「物理债」——浏览器全局副作用(网络/未捕获异常/SSE 流),不在任何 app
|
|
10
|
+
// 状态声明里,它们是运行时事件。这一堆谁都删不掉。aiself 不消灭它,而是把 aipeek
|
|
11
|
+
// 的五处分散 patch(console/fetch/XHR/SSE/error)+ 各自 ring buffer 收敛成一条 Log。
|
|
12
|
+
//
|
|
13
|
+
// 所以命题不是「感知自己 0 行」(假——物理债删不掉),而是:
|
|
14
|
+
// ① 自表示债 → 0(screen 投影无函数体)
|
|
15
|
+
// ② 物理债 → 一条 self-log 原语,取代五处分散 patch
|
|
16
|
+
|
|
17
|
+
// ── 第一堆:self-rep ──
|
|
18
|
+
// view 是互斥档位;modal 决定底层能否操作;focus 是当前焦点;
|
|
19
|
+
// state 是这个 self 暴露的语义变量;anchor 是声明自带的源码来源(非 babel 事后盖戳)。
|
|
20
|
+
export interface Self {
|
|
21
|
+
view: string
|
|
22
|
+
modal: string | null
|
|
23
|
+
focus: string | null
|
|
24
|
+
state: Record<string, unknown>
|
|
25
|
+
anchor: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 感知自己 = 投影。无 walk、无 patch、无 sourcemap —— 因为 self 本就自表示。
|
|
29
|
+
// 这是第一堆「自表示债 → 0」的证据:函数趋近无函数体。
|
|
30
|
+
export function screen(self: Self): Self {
|
|
31
|
+
return self
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 第二堆:self-log ──
|
|
35
|
+
// 一条 Log,不是五个 ring buffer。channel 标出这条记录来自哪路浏览器副作用
|
|
36
|
+
// (console/fetch/xhr/sse/error),但它们流进同一个序列、同一个形状。
|
|
37
|
+
// aipeek 为这五路写了五处 monkey-patch + 五个独立缓冲;这里是一个 append。
|
|
38
|
+
export interface LogEntry {
|
|
39
|
+
channel: 'console' | 'fetch' | 'xhr' | 'sse' | 'error'
|
|
40
|
+
detail: string
|
|
41
|
+
ts: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 收敛点:五路副作用 → 一次 append。物理债没被消灭(浏览器副作用照样发生),
|
|
45
|
+
// 但它的「形状」从五个分叉坍缩成一条线。这是第二堆的赢点 —— 统一,非删除。
|
|
46
|
+
export function log(seq: LogEntry[], entry: LogEntry): LogEntry[] {
|
|
47
|
+
return [...seq, entry]
|
|
48
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// aihand/self —— store 实例映射的 build 时 codegen。对偶于 store-schema(那个 emit 值域 schema,
|
|
2
|
+
// 这个 emit 运行时实例)。
|
|
3
|
+
//
|
|
4
|
+
// 绿色通道(executeKnob)要拿真 store 实例直写字段,实例来源 window.__AIPEEK_STORES__。
|
|
5
|
+
// 旧实现是 main.tsx 一份手抄清单 —— 加旋钮引新 store 没机制强制并入,会无声腐烂(实测漏过
|
|
6
|
+
// memoryStore + 5 个 store)。这里把那份清单 codegen 掉:从「被旋钮写的 store 名」(knob schema 的
|
|
7
|
+
// 真理源)精确长出 import,main.tsx 塌成一行 import('virtual:store-refs')。
|
|
8
|
+
//
|
|
9
|
+
// 范围 = 旋钮引用的 store(非全仓发现的 ~21 个)。create 类从 discoverWithPaths 拿路径,
|
|
10
|
+
// facade 类(botStore/memoryStore,无 _loading marker)= 旋钮集 ∖ 发现集,按名 locateExports 定位。
|
|
11
|
+
// 引擎离线:跑在 build/cold load,ts-morph + tree-sitter 绝不进浏览器。
|
|
12
|
+
|
|
13
|
+
import { relative } from 'node:path'
|
|
14
|
+
import { buildPanel } from '../../read/panel.js'
|
|
15
|
+
import { discoverWithPaths, locateExports } from './discover.js'
|
|
16
|
+
|
|
17
|
+
const VIRTUAL = 'virtual:store-refs'
|
|
18
|
+
const RESOLVED = `\0${VIRTUAL}`
|
|
19
|
+
|
|
20
|
+
interface Options {
|
|
21
|
+
// store 发现的根(绝对路径),同 storeSchema。create() store 从这里的状态子图长出。
|
|
22
|
+
roots: string | string[]
|
|
23
|
+
// 旋钮提取扫的组件文件(绝对路径 tsx/jsx)。store 名集合 = 这些文件里旋钮写到的 store。
|
|
24
|
+
knobFiles: string[]
|
|
25
|
+
// 项目源根(绝对路径,= <projectRoot>/src),codegen 的 @/ 别名据此从 abs 反解。
|
|
26
|
+
srcRoot: string
|
|
27
|
+
storeMarker?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StoreRef {
|
|
31
|
+
name: string // export 名 = 变量名,如 'botStore'
|
|
32
|
+
alias: string // import 路径,如 '@/store/bot'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 绝对路径 → @/ 别名 import 路径。strip srcRoot 前缀 + 去扩展名,逐字符(无正则,本仓铁律)。
|
|
36
|
+
// '/p/src/features/IM/shared/store.tsx' + '/p/src' → '@/features/IM/shared/store'
|
|
37
|
+
export function toAlias(abs: string, srcRoot: string): string {
|
|
38
|
+
let rel = relative(srcRoot, abs)
|
|
39
|
+
for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
|
|
40
|
+
if (rel.endsWith(ext)) {
|
|
41
|
+
rel = rel.slice(0, -ext.length)
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return `@/${rel}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// facade 集 = 旋钮引用的 store 名 ∖ marker 发现的 store 名。补集,零手列。
|
|
49
|
+
export function pickFacades(knobNames: Set<string>, discoveredNames: Set<string>): string[] {
|
|
50
|
+
return [...knobNames].filter(n => !discoveredNames.has(n))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// codegen 虚拟模块文本。同 alias 路径去重(appUIStore/updateStore 同文件 → 一行 import 两名),
|
|
54
|
+
// 末尾聚合 export const stores = { ...全部名 }。
|
|
55
|
+
export function emitModule(refs: StoreRef[]): string {
|
|
56
|
+
const byAlias = new Map<string, string[]>()
|
|
57
|
+
for (const r of refs) {
|
|
58
|
+
const names = byAlias.get(r.alias) ?? []
|
|
59
|
+
names.push(r.name)
|
|
60
|
+
byAlias.set(r.alias, names)
|
|
61
|
+
}
|
|
62
|
+
const imports: string[] = []
|
|
63
|
+
for (const [alias, names] of byAlias)
|
|
64
|
+
imports.push(`import { ${[...new Set(names)].sort().join(', ')} } from '${alias}'`)
|
|
65
|
+
const all = [...new Set(refs.map(r => r.name))].sort()
|
|
66
|
+
return `${imports.join('\n')}\nexport const stores = { ${all.join(', ')} }\n`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function storeRefs(options: Options) {
|
|
70
|
+
const roots = Array.isArray(options.roots) ? options.roots : [options.roots]
|
|
71
|
+
let cache: string | null = null
|
|
72
|
+
|
|
73
|
+
const build = async (): Promise<string> => {
|
|
74
|
+
if (cache)
|
|
75
|
+
return cache
|
|
76
|
+
// 1. create 类:状态子图发现,带未截断绝对路径。
|
|
77
|
+
const discovered = await discoverWithPaths(roots, options.storeMarker)
|
|
78
|
+
const byName = new Map(discovered.map(d => [d.store, d.abs]))
|
|
79
|
+
|
|
80
|
+
// 2. 旋钮引用的 store 名集合(真理源:绿色通道实际需要的精确集)。
|
|
81
|
+
const knobs = await buildPanel(options.knobFiles)
|
|
82
|
+
const knobNames = new Set(knobs.map(k => k.store).filter(Boolean))
|
|
83
|
+
|
|
84
|
+
// 3. facade 类:旋钮集 ∖ 发现集,按名定位真路径。
|
|
85
|
+
const facadeNames = new Set(pickFacades(knobNames, new Set(byName.keys())))
|
|
86
|
+
const facadePaths = facadeNames.size ? await locateExports(roots, facadeNames) : new Map<string, string>()
|
|
87
|
+
|
|
88
|
+
// 4. needed = 旋钮引用的 store。create 取发现路径,facade 取 locate 路径;路径缺失 → 跳过 + warn。
|
|
89
|
+
const refs: StoreRef[] = []
|
|
90
|
+
const missing: string[] = []
|
|
91
|
+
for (const name of knobNames) {
|
|
92
|
+
const abs = byName.get(name) ?? facadePaths.get(name)
|
|
93
|
+
if (!abs) {
|
|
94
|
+
missing.push(name)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
refs.push({ name, alias: toAlias(abs, options.srcRoot) })
|
|
98
|
+
}
|
|
99
|
+
if (missing.length)
|
|
100
|
+
console.warn(`[aihand:store-refs] 旋钮引用但定位不到声明的 store(已跳过): ${missing.join(', ')}`)
|
|
101
|
+
|
|
102
|
+
cache = emitModule(refs)
|
|
103
|
+
return cache
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: 'aihand:store-refs',
|
|
108
|
+
resolveId(id: string) {
|
|
109
|
+
if (id === VIRTUAL)
|
|
110
|
+
return RESOLVED
|
|
111
|
+
},
|
|
112
|
+
async load(id: string) {
|
|
113
|
+
if (id !== RESOLVED)
|
|
114
|
+
return
|
|
115
|
+
return build()
|
|
116
|
+
},
|
|
117
|
+
// store 文件或组件改动 → 失效缓存,下次 cold load 重 codegen(加旋钮引新 store → 自动并入)。
|
|
118
|
+
handleHotUpdate(ctx: { file: string }) {
|
|
119
|
+
if (roots.some(r => ctx.file.startsWith(r)) || ctx.file.endsWith('.tsx') || ctx.file.endsWith('.jsx'))
|
|
120
|
+
cache = null
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|