@tbox.cn/app-template-agent 0.2.0 → 0.7.5
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/.gitignore +6 -0
- package/.tbox/app.json +12 -3
- package/AGENTS.md +17 -6
- package/ENV.md +36 -5
- package/README.md +23 -7
- package/apps/client/package.json +3 -2
- package/apps/client/src/app.tsx +28 -0
- package/apps/client/src/cards/CardRenderer.tsx +4 -5
- package/apps/client/src/components/ActionBar/DebugButton.tsx +5 -1
- package/apps/client/src/components/ActionBar/index.tsx +7 -1
- package/apps/client/src/components/ChatInput/index.less +54 -0
- package/apps/client/src/components/ChatInput/index.tsx +33 -9
- package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +20 -1
- package/apps/client/src/components/MessageBubble/index.less +12 -1
- package/apps/client/src/components/MessageBubble/index.tsx +16 -8
- package/apps/client/src/components/PullDownIndicator/index.less +1 -1
- package/apps/client/src/components/PullDownIndicator/index.tsx +3 -4
- package/apps/client/src/components/ToolCallBlock/index.less +29 -0
- package/apps/client/src/components/ToolCallBlock/index.tsx +58 -24
- package/apps/client/src/hooks/useMediaUpload.ts +7 -2
- package/apps/client/src/main.tsx +4 -1
- package/apps/client/src/pages/index/index.tsx +35 -13
- package/apps/client/src/platform/AgentChatApp.tsx +5 -1
- package/apps/client/src/store/chat-config.ts +12 -1
- package/apps/client/src/store/chat.ts +62 -92
- package/apps/client/vite.config.ts +46 -26
- package/apps/server/package.json +5 -11
- package/apps/server/src/app.ts +452 -0
- package/apps/server/src/cli/collect-registry.ts +7 -6
- package/apps/server/src/config/deployment.ts +36 -70
- package/apps/server/src/config/integration.ts +81 -0
- package/apps/server/src/dev.ts +81 -0
- package/apps/server/src/index.ts +23 -376
- package/apps/server/src/intent-routes.ts +11 -4
- package/apps/server/src/modules.ts +3 -0
- package/apps/server/tests/cards-registry.test.ts +9 -17
- package/apps/server/tests/deployment-loader.test.ts +140 -0
- package/apps/server/tests/intent-dispatcher.test.ts +58 -0
- package/apps/server/tests/tool-runtime.test.ts +97 -23
- package/apps/server/tsup.config.ts +15 -2
- package/config/chat.json +4 -1
- package/config/llm_provider.json +3 -1
- package/package.json +19 -11
- package/packages/contracts-mall/package.json +30 -0
- package/packages/contracts-mall/src/index.ts +1 -0
- package/packages/contracts-mall/src/runtime.ts +1 -0
- package/packages/contracts-mall/src/server/index.ts +1 -0
- package/packages/contracts-mall/tsconfig.json +7 -0
- package/packages/module-weather/package.json +4 -4
- package/packages/module-weather/skills/weather-guide/SKILL.md +3 -0
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
- package/packages/module-weather/src/server/index.ts +1 -0
- package/packages/module-weather/src/server/tool.ts +24 -24
- package/packages/provider-custom/AGENTS.md +33 -0
- package/packages/provider-custom/LICENSE +21 -0
- package/packages/provider-custom/README.md +8 -0
- package/packages/provider-custom/package.json +62 -0
- package/packages/provider-custom/src/index.ts +5 -0
- package/packages/provider-custom/src/server/index.ts +40 -0
- package/packages/provider-custom/tbox.module.json +29 -0
- package/packages/provider-custom/tests/provider-custom.test.ts +17 -0
- package/packages/provider-custom/tsconfig.json +7 -0
- package/packages/provider-custom/tsup.config.ts +18 -0
- package/pnpm-lock.template.yaml +6297 -0
- package/pnpm-workspace.template.yaml +45 -46
- package/scripts/build.sh +6 -12
- package/scripts/check-workspace.mjs +11 -10
- package/scripts/stop.sh +48 -14
- package/scripts/verify-generated.mjs +4 -4
- package/tsconfig.base.json +1 -0
- package/apps/client/babel-plugins/code-inspect.cjs +0 -110
- package/apps/client/src/hooks/usePullDownHistory.ts +0 -97
- package/apps/server/skills/shopping-guide-demo/SKILL.md +0 -21
- package/apps/server/skills/shopping-guide-demo/references/guide/parking.md +0 -11
- package/apps/server/skills/shopping-guide-demo/references/mall-directory.md +0 -17
- package/apps/server/skills/shopping-guide-demo/references/promotion.md +0 -10
- package/apps/server/src/config/feature-gate.ts +0 -17
- package/apps/server/tests/feature-gate.test.ts +0 -30
- package/config/deployments/development.json +0 -27
- package/scripts/check-inspector.sh +0 -91
- /package/packages/module-weather/{tbox.component.json → tbox.module.json} +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# 应用 workspace 模板:tbox-app create 用它生成应用的 pnpm-workspace.yaml。
|
|
2
2
|
# 本文件不触发 workspace root(非 pnpm-workspace.yaml 文件名)。
|
|
3
|
-
#
|
|
3
|
+
# 同步规则(guard-workspace 恒等式校验):
|
|
4
|
+
# - catalog 键集 = agent 三模板成员 catalog: 引用并集 ∪ 平台三键(@tbox.cn/app-{sdk,contracts*}——
|
|
5
|
+
# codegen 展开恒改写为 catalog: 的完备键);键随成员引用增删,不镜像根全量。
|
|
6
|
+
# - catalog 值与根 pnpm-workspace.yaml 同键全等(scripts/release.mjs sync-catalog 维护)。
|
|
7
|
+
# - 非 catalog 顶层键与根镜像(packages/overrides 除外——发布态不带 overrides)。
|
|
4
8
|
packages:
|
|
5
9
|
- apps/*
|
|
6
10
|
- packages/*
|
|
@@ -10,52 +14,47 @@ onlyBuiltDependencies:
|
|
|
10
14
|
|
|
11
15
|
allowBuilds:
|
|
12
16
|
esbuild: true
|
|
17
|
+
lefthook: false
|
|
18
|
+
|
|
19
|
+
resolvePeersFromWorkspaceRoot: true
|
|
20
|
+
|
|
21
|
+
# 递归脚本并发上限(pnpm -r 默认 4):全速并行时多包 vitest worker 独立 transform
|
|
22
|
+
# 同一源码树,CI 峰值内存曾致 V8 fatal(SIGABRT)。本地提速可 CLI 旗标覆盖。
|
|
23
|
+
workspaceConcurrency: 2
|
|
13
24
|
|
|
14
25
|
catalog:
|
|
15
|
-
typescript:
|
|
16
|
-
tsx:
|
|
17
|
-
zod:
|
|
18
|
-
vite:
|
|
19
|
-
vitest:
|
|
20
|
-
react:
|
|
21
|
-
react-dom:
|
|
22
|
-
"@types/react":
|
|
23
|
-
"@types/react-dom":
|
|
24
|
-
"@types/node":
|
|
25
|
-
express:
|
|
26
|
-
"@types/express":
|
|
27
|
-
ws:
|
|
28
|
-
"@types/ws":
|
|
29
|
-
"@mastra/core": ^1.57.0
|
|
30
|
-
"@mastra/observability": 1.16.5
|
|
31
|
-
"gray-matter": ^4.0.3
|
|
32
|
-
"@ag-ui/core": ^0.0.47
|
|
33
|
-
ai: ^6.0.0
|
|
34
|
-
"@ai-sdk/openai": ^3.0.0
|
|
35
|
-
"@ai-sdk/provider": ^3.0.10
|
|
26
|
+
typescript: 5.9.3
|
|
27
|
+
tsx: 4.23.11
|
|
28
|
+
zod: 3.25.76
|
|
29
|
+
vite: 6.4.3
|
|
30
|
+
vitest: 4.1.10
|
|
31
|
+
react: 18.3.1
|
|
32
|
+
react-dom: 18.3.1
|
|
33
|
+
"@types/react": 18.3.31
|
|
34
|
+
"@types/react-dom": 18.3.7
|
|
35
|
+
"@types/node": 24.13.3
|
|
36
|
+
express: 4.22.2
|
|
37
|
+
"@types/express": 4.17.25
|
|
38
|
+
ws: 8.21.3
|
|
39
|
+
"@types/ws": 8.18.1
|
|
36
40
|
"@tbox.cn/agent-plugin": 0.1.0
|
|
37
|
-
"@tbox.cn/app-sdk":
|
|
38
|
-
"@tbox.cn/app-contracts":
|
|
39
|
-
"@tbox.cn/app-contracts-mall":
|
|
40
|
-
zustand:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cors: ^2.8.5
|
|
49
|
-
jose: ^6.2.2
|
|
50
|
-
"@types/cors": ^2.8.17
|
|
41
|
+
"@tbox.cn/app-sdk": 0.18.0
|
|
42
|
+
"@tbox.cn/app-contracts": 0.9.0
|
|
43
|
+
"@tbox.cn/app-contracts-mall": 0.9.0
|
|
44
|
+
zustand: 4.5.7
|
|
45
|
+
eslint: 9.39.5
|
|
46
|
+
typescript-eslint: 8.67.0
|
|
47
|
+
eslint-plugin-react-hooks: 5.2.0
|
|
48
|
+
tsup: 8.5.1
|
|
49
|
+
cors: 2.8.6
|
|
50
|
+
jose: 6.2.8
|
|
51
|
+
"@types/cors": 2.8.19
|
|
51
52
|
"@tbox-dev-js/sdk": 0.1.18
|
|
52
|
-
"@vitejs/plugin-react":
|
|
53
|
-
less:
|
|
54
|
-
"@phosphor-icons/react":
|
|
55
|
-
antd-mobile:
|
|
56
|
-
react-markdown:
|
|
57
|
-
remark-gfm:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
happy-dom: ^20.9.0
|
|
61
|
-
postcss-pxtorem: ^6.1.0
|
|
53
|
+
"@vitejs/plugin-react": 4.7.0
|
|
54
|
+
less: 4.8.1
|
|
55
|
+
"@phosphor-icons/react": 2.1.10
|
|
56
|
+
antd-mobile: 5.42.3
|
|
57
|
+
react-markdown: 9.1.0
|
|
58
|
+
remark-gfm: 4.0.1
|
|
59
|
+
happy-dom: 20.11.2
|
|
60
|
+
postcss-pxtorem: 6.1.0
|
package/scripts/build.sh
CHANGED
|
@@ -9,7 +9,7 @@ cd "$PROJECT_DIR"
|
|
|
9
9
|
echo "Project directory: $PROJECT_DIR"
|
|
10
10
|
|
|
11
11
|
export NODE_ENV=production
|
|
12
|
-
echo "APP_API_URL: $APP_API_URL"
|
|
12
|
+
echo "APP_API_URL: ${APP_API_URL:-<unset>}"
|
|
13
13
|
|
|
14
14
|
SKIP_SERVER=false
|
|
15
15
|
for arg in "$@"; do
|
|
@@ -17,23 +17,17 @@ for arg in "$@"; do
|
|
|
17
17
|
done
|
|
18
18
|
echo "Skip server: $SKIP_SERVER"
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# 平台基础设施守卫
|
|
24
|
-
bash "$SCRIPT_DIR/check-inspector.sh"
|
|
20
|
+
# 零依赖守卫 install 前 fail-fast(幂等毫秒级;check:inspector 依赖 node_modules,经 check 组合随构建序列执行)
|
|
21
|
+
pnpm check:workspace
|
|
25
22
|
|
|
26
23
|
echo "Installing dependencies..."
|
|
27
24
|
CI=true pnpm install
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
pnpm --filter @app/client build
|
|
31
|
-
|
|
26
|
+
# 构建序列单源 = package.json build / build:client(守卫经 check 组合注入);本脚本只管环境与 install
|
|
32
27
|
if [ "$SKIP_SERVER" = false ]; then
|
|
33
|
-
|
|
34
|
-
pnpm --filter @app/server build
|
|
28
|
+
pnpm build
|
|
35
29
|
else
|
|
36
|
-
|
|
30
|
+
pnpm build:client
|
|
37
31
|
fi
|
|
38
32
|
|
|
39
33
|
echo "All builds completed successfully!"
|
|
@@ -25,6 +25,7 @@ const nestedLockfiles = new Set([
|
|
|
25
25
|
'bun.lockb',
|
|
26
26
|
'package-lock.json',
|
|
27
27
|
'pnpm-lock.yaml',
|
|
28
|
+
'pnpm-lock.template.yaml',
|
|
28
29
|
'yarn.lock',
|
|
29
30
|
]);
|
|
30
31
|
const viteConfigPattern = /^vite\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
|
|
@@ -90,22 +91,22 @@ function printFailure(findings) {
|
|
|
90
91
|
console.error('\n检测到 apps/client/ 之外的 Vite 项目配置:');
|
|
91
92
|
for (const file of findings.detachedViteConfigs) console.error(` - ${file}`);
|
|
92
93
|
}
|
|
93
|
-
if (findings.
|
|
94
|
-
console.error('\npackages/* 缺少 tbox.
|
|
95
|
-
for (const file of findings.
|
|
94
|
+
if (findings.missingModuleDescriptors.length > 0) {
|
|
95
|
+
console.error('\npackages/* 缺少 tbox.module.json(contracts / app-sdk / shared 除外):');
|
|
96
|
+
for (const file of findings.missingModuleDescriptors) console.error(` - ${file}`);
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
console.error('');
|
|
99
100
|
process.exitCode = 1;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
/** 豁免:SDK/契约包无 tbox.
|
|
103
|
+
/** 豁免:SDK/契约包无 tbox.module.json(002 §2.3 / 003 §4.1) */
|
|
103
104
|
function isExemptPackage(dirName) {
|
|
104
105
|
return dirName === 'app-sdk' || dirName === 'shared' || dirName.startsWith('contracts');
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
/** 校验 packages/* 每个业务包必须有 tbox.
|
|
108
|
-
async function
|
|
108
|
+
/** 校验 packages/* 每个业务包必须有 tbox.module.json(002 §2.3 / 003 §4.1) */
|
|
109
|
+
async function checkModuleDescriptors(projectDir, findings) {
|
|
109
110
|
const packagesDir = path.join(projectDir, 'packages');
|
|
110
111
|
if (!existsSync(packagesDir)) return;
|
|
111
112
|
const entries = await readdir(packagesDir, { withFileTypes: true });
|
|
@@ -114,8 +115,8 @@ async function checkComponentDescriptors(projectDir, findings) {
|
|
|
114
115
|
if (isExemptPackage(entry.name)) continue;
|
|
115
116
|
const pkgDir = path.join(packagesDir, entry.name);
|
|
116
117
|
if (!existsSync(path.join(pkgDir, 'package.json'))) continue;
|
|
117
|
-
if (!existsSync(path.join(pkgDir, 'tbox.
|
|
118
|
-
findings.
|
|
118
|
+
if (!existsSync(path.join(pkgDir, 'tbox.module.json'))) {
|
|
119
|
+
findings.missingModuleDescriptors.push(`packages/${entry.name}/`);
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
}
|
|
@@ -144,11 +145,11 @@ async function main() {
|
|
|
144
145
|
const findings = {
|
|
145
146
|
nestedLockfiles: [],
|
|
146
147
|
detachedViteConfigs: [],
|
|
147
|
-
|
|
148
|
+
missingModuleDescriptors: [],
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
await walk(projectDir, findings);
|
|
151
|
-
await
|
|
152
|
+
await checkModuleDescriptors(projectDir, findings);
|
|
152
153
|
|
|
153
154
|
if (Object.values(findings).some((items) => items.length > 0)) {
|
|
154
155
|
printFailure(findings);
|
package/scripts/stop.sh
CHANGED
|
@@ -1,26 +1,60 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# 停止本应用全部进程。杀面收敛原则(docs/postmortem/0001):
|
|
3
|
+
# 1) cmdline 匹配:锚定本项目路径,逐字符 ERE 字面量化(BSD/GNU 无歧义),[^ ]* 锁单参数
|
|
4
|
+
# 2) 端口定位:只取 LISTEN 态——持有连接的客户端(编排探活/代理父进程)不入杀面
|
|
5
|
+
# PORT 契约:须与启动环境一致(生产形态 cmdline 不含路径,端口是唯一定位手段)
|
|
6
|
+
# 边界:根级浅层 <P>/node_modules/.bin/x 直调形态不在 pattern 内(模板内无此调用形态;.pnpm 深路径匹配)
|
|
2
7
|
|
|
3
8
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
9
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
5
10
|
cd "$PROJECT_DIR"
|
|
6
|
-
|
|
7
11
|
PORT=${PORT:-8000}
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
esc_ere() { # 元字符以 [c] 字面量化;单遍扫描防插入物自污染(BSD \+ 为量词,已实证)。
|
|
14
|
+
# 特例:^ 用 [\^]——裸置双引擎皆为行首锚(实证),[^] 则是非法未闭合括号表达式;\ 用 [\\]
|
|
15
|
+
local s="$1" i c out=""
|
|
16
|
+
for ((i=0; i<${#s}; i++)); do
|
|
17
|
+
c=${s:$i:1}
|
|
18
|
+
case $c in
|
|
19
|
+
'.'|'+'|'*'|'?'|'$'|'('|')'|'{'|'}'|'|'|'['|']') out+="[$c]" ;;
|
|
20
|
+
'^') out+='[\^]' ;;
|
|
21
|
+
'\') out+='[\\]' ;;
|
|
22
|
+
*) out+=$c ;;
|
|
23
|
+
esac
|
|
14
24
|
done
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
printf '%s' "$out"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
LISTEN_TOOLS_OK=1 # 主作用域预计算(listen_pids 经命令替换运行于子 shell,内赋值不外传——R4b 实证)
|
|
29
|
+
command -v lsof >/dev/null 2>&1 || command -v ss >/dev/null 2>&1 || LISTEN_TOOLS_OK=0
|
|
30
|
+
listen_pids() { # 两级降级:lsof(macOS 恒有)→ ss(iproute2);参数化端口便于多端口扩展
|
|
31
|
+
local p=${1:-$PORT}
|
|
32
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
33
|
+
lsof -nP -t -i :"$p" -sTCP:LISTEN 2>/dev/null | sort -u
|
|
34
|
+
elif command -v ss >/dev/null 2>&1; then
|
|
35
|
+
ss -ltnpH "sport = :$p" 2>/dev/null | grep -o 'pid=[0-9]*' | cut -d= -f2 | sort -u
|
|
36
|
+
else
|
|
37
|
+
echo "Warning: lsof/ss 均不可用,端口清理跳过(cmdline 兜底仍执行)" >&2
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
20
40
|
|
|
21
41
|
echo "=== Cleaning up residual processes ==="
|
|
22
|
-
|
|
23
|
-
pkill -f "
|
|
24
|
-
pkill -f "vite" 2>/dev/null || true
|
|
42
|
+
ESC=$(esc_ere "$PROJECT_DIR")
|
|
43
|
+
pkill -TERM -f "$ESC/[^ ]*/node_modules" 2>/dev/null || true # 先于端口杀:杜绝 watch 复活窗口
|
|
25
44
|
|
|
45
|
+
PIDS=$(listen_pids "$PORT")
|
|
46
|
+
if [ -n "$PIDS" ]; then
|
|
47
|
+
echo "=== Stopping server on port $PORT ==="
|
|
48
|
+
for PID in $PIDS; do echo "Killing process $PID..."; kill -9 "$PID" 2>/dev/null || true; done
|
|
49
|
+
sleep 1
|
|
50
|
+
if [ -n "$(listen_pids "$PORT")" ]; then
|
|
51
|
+
echo "Warning: Port $PORT still in use"
|
|
52
|
+
else
|
|
53
|
+
echo "Port $PORT is now free"
|
|
54
|
+
fi
|
|
55
|
+
elif [ "$LISTEN_TOOLS_OK" = 1 ]; then
|
|
56
|
+
echo "=== Port $PORT: no listener ==="
|
|
57
|
+
else
|
|
58
|
+
echo "=== Port $PORT: listener status unknown(定位工具缺失)==="
|
|
59
|
+
fi
|
|
26
60
|
echo "=== All services stopped ==="
|
|
@@ -43,7 +43,7 @@ function readManifestModules() {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/** 收集 packages/* 下本地模块 id(有 tbox.
|
|
46
|
+
/** 收集 packages/* 下本地模块 id(有 tbox.module.json) */
|
|
47
47
|
async function listLocalModuleIds() {
|
|
48
48
|
const packagesDir = path.join(appDir, 'packages');
|
|
49
49
|
if (!existsSync(packagesDir)) return [];
|
|
@@ -51,7 +51,7 @@ async function listLocalModuleIds() {
|
|
|
51
51
|
const ids = [];
|
|
52
52
|
for (const entry of entries) {
|
|
53
53
|
if (!entry.isDirectory()) continue;
|
|
54
|
-
if (existsSync(path.join(packagesDir, entry.name, 'tbox.
|
|
54
|
+
if (existsSync(path.join(packagesDir, entry.name, 'tbox.module.json'))) {
|
|
55
55
|
ids.push(entry.name);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -100,12 +100,12 @@ async function main() {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// ① 漂移 = 0:声明(manifest + 本地模块)vs 服务端 modules.ts 实际装配
|
|
103
|
-
// 契约包(无 tbox.
|
|
103
|
+
// 契约包(无 tbox.module.json / 无 serverModule)不参与 modules.ts 装配,不计入声明
|
|
104
104
|
// (客户端 modules.ts 装配漂移由 tbox-app module doctor 全量校验兜底)
|
|
105
105
|
const npmModules = readManifestModules();
|
|
106
106
|
const declared = new Set([
|
|
107
107
|
...npmModules
|
|
108
|
-
.filter((m) => existsSync(path.join(appDir, 'packages', m.id, 'tbox.
|
|
108
|
+
.filter((m) => existsSync(path.join(appDir, 'packages', m.id, 'tbox.module.json')))
|
|
109
109
|
.map((m) => m.id),
|
|
110
110
|
...(await listLocalModuleIds()),
|
|
111
111
|
]);
|
package/tsconfig.base.json
CHANGED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================================
|
|
3
|
-
* 平台配套 babel plugin - 给每个 JSXElement 注入 data-code-inspect-* 属性。
|
|
4
|
-
* 配合 tbox 平台 inspector runtime(/__vs_inspector__.js)使用,让 host 端微调浮层
|
|
5
|
-
* 能拿到源码位置,直接走「直改源码」快路径,而不是把意图发回 LLM 重新生成代码。
|
|
6
|
-
*
|
|
7
|
-
* 注入的属性(与 inspector runtime 期望严格对齐,名字别改):
|
|
8
|
-
* data-code-inspect-loc-filepath = 绝对路径
|
|
9
|
-
* data-code-inspect-loc-line = "{line}:{column}" (1-based, 来自 JSX opening tag)
|
|
10
|
-
* data-code-inspect-jsx-tag = JSX 标签名 (如 "div" / "Form.Item")
|
|
11
|
-
* data-code-inspect-component = 所在组件名 (爬 AST 找最近的函数/类组件)
|
|
12
|
-
*
|
|
13
|
-
* Vite 接入:在 vite.config.ts 里 `react({ babel: { plugins: [...] } })`,仅 dev(command==='serve')启用。
|
|
14
|
-
* ============================================================================
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const LOC_FILEPATH_ATTRIBUTE_NAME = 'data-code-inspect-loc-filepath';
|
|
18
|
-
const LOC_LINE_ATTRIBUTE_NAME = 'data-code-inspect-loc-line';
|
|
19
|
-
const TAG_ATTRIBUTE_NAME = 'data-code-inspect-jsx-tag';
|
|
20
|
-
const COMPONENT_ATTRIBUTE_NAME = 'data-code-inspect-component';
|
|
21
|
-
|
|
22
|
-
const PLUGIN_NAME = 'code-inspect-plugin';
|
|
23
|
-
|
|
24
|
-
function getJsxName(nameNode) {
|
|
25
|
-
if (nameNode.type === 'JSXIdentifier') return nameNode.name;
|
|
26
|
-
if (nameNode.type === 'JSXMemberExpression') {
|
|
27
|
-
return getJsxName(nameNode.object) + '.' + nameNode.property.name;
|
|
28
|
-
}
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getCurrentComponentName(path) {
|
|
33
|
-
let current = path;
|
|
34
|
-
while (current) {
|
|
35
|
-
if (current.isFunctionDeclaration() && current.node.id) {
|
|
36
|
-
return current.node.id.name;
|
|
37
|
-
}
|
|
38
|
-
if (
|
|
39
|
-
current.isVariableDeclarator() &&
|
|
40
|
-
current.node.id &&
|
|
41
|
-
current.node.init
|
|
42
|
-
) {
|
|
43
|
-
const init = current.node.init;
|
|
44
|
-
if (
|
|
45
|
-
init.type === 'ArrowFunctionExpression' ||
|
|
46
|
-
init.type === 'FunctionExpression'
|
|
47
|
-
) {
|
|
48
|
-
return current.node.id.name;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (current.isClassDeclaration() && current.node.id) {
|
|
52
|
-
return current.node.id.name;
|
|
53
|
-
}
|
|
54
|
-
if (
|
|
55
|
-
(current.isFunctionExpression() ||
|
|
56
|
-
current.isArrowFunctionExpression() ||
|
|
57
|
-
current.isCallExpression()) &&
|
|
58
|
-
current.parent &&
|
|
59
|
-
current.parent.type === 'VariableDeclarator' &&
|
|
60
|
-
current.parent.id
|
|
61
|
-
) {
|
|
62
|
-
return current.parent.id.name;
|
|
63
|
-
}
|
|
64
|
-
current = current.parentPath;
|
|
65
|
-
}
|
|
66
|
-
return 'Unknown';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function CodeInspectPlugin(babel) {
|
|
70
|
-
const t = babel.types;
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
name: PLUGIN_NAME,
|
|
74
|
-
visitor: {
|
|
75
|
-
JSXElement(path) {
|
|
76
|
-
const node = path.node;
|
|
77
|
-
const nodeName = getJsxName(node.openingElement.name);
|
|
78
|
-
if (!nodeName) return;
|
|
79
|
-
if (!node.loc) return;
|
|
80
|
-
|
|
81
|
-
const filename = path.hub.file.opts.filename || 'unknown';
|
|
82
|
-
const { line, column } = node.loc.start;
|
|
83
|
-
const componentName = getCurrentComponentName(path);
|
|
84
|
-
|
|
85
|
-
node.openingElement.attributes.push(
|
|
86
|
-
t.jsxAttribute(
|
|
87
|
-
t.jsxIdentifier(LOC_FILEPATH_ATTRIBUTE_NAME),
|
|
88
|
-
t.stringLiteral(filename),
|
|
89
|
-
),
|
|
90
|
-
t.jsxAttribute(
|
|
91
|
-
t.jsxIdentifier(LOC_LINE_ATTRIBUTE_NAME),
|
|
92
|
-
t.stringLiteral(`${line}:${column}`),
|
|
93
|
-
),
|
|
94
|
-
t.jsxAttribute(
|
|
95
|
-
t.jsxIdentifier(TAG_ATTRIBUTE_NAME),
|
|
96
|
-
t.stringLiteral(nodeName),
|
|
97
|
-
),
|
|
98
|
-
t.jsxAttribute(
|
|
99
|
-
t.jsxIdentifier(COMPONENT_ATTRIBUTE_NAME),
|
|
100
|
-
t.stringLiteral(componentName),
|
|
101
|
-
),
|
|
102
|
-
);
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
module.exports = CodeInspectPlugin;
|
|
109
|
-
module.exports.CodeInspectPlugin = CodeInspectPlugin;
|
|
110
|
-
module.exports.PLUGIN_NAME = PLUGIN_NAME;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export type PullStatus = 'idle' | 'pulling' | 'ready' | 'loading' | 'noMore';
|
|
4
|
-
|
|
5
|
-
const DAMPING = 0.4;
|
|
6
|
-
const THRESHOLD = 60;
|
|
7
|
-
|
|
8
|
-
interface Options {
|
|
9
|
-
isAtTopRef: React.MutableRefObject<boolean>;
|
|
10
|
-
isLoadingHistory: boolean;
|
|
11
|
-
hasMore: boolean;
|
|
12
|
-
onLoad: () => Promise<void>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** 列表顶部下拉加载历史(纯 H5 触摸,去 Taro)。 */
|
|
16
|
-
export function usePullDownHistory({ isAtTopRef, isLoadingHistory, hasMore, onLoad }: Options) {
|
|
17
|
-
const [pullStatus, setPullStatus] = useState<PullStatus>('idle');
|
|
18
|
-
const [pullDistance, setPullDistance] = useState(0);
|
|
19
|
-
const startYRef = useRef(0);
|
|
20
|
-
const isPullingRef = useRef(false);
|
|
21
|
-
|
|
22
|
-
const handleTouchStart = useCallback(
|
|
23
|
-
(e: React.TouchEvent) => {
|
|
24
|
-
if (isLoadingHistory || pullStatus === 'loading') return;
|
|
25
|
-
const touch = e.touches[0];
|
|
26
|
-
if (!touch) return;
|
|
27
|
-
startYRef.current = touch.clientY;
|
|
28
|
-
isPullingRef.current = isAtTopRef.current;
|
|
29
|
-
},
|
|
30
|
-
[isLoadingHistory, pullStatus, isAtTopRef],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const handleTouchMove = useCallback(
|
|
34
|
-
(e: React.TouchEvent) => {
|
|
35
|
-
if (!isPullingRef.current) return;
|
|
36
|
-
if (isLoadingHistory || pullStatus === 'loading') return;
|
|
37
|
-
if (!hasMore && pullStatus !== 'pulling' && pullStatus !== 'ready') return;
|
|
38
|
-
|
|
39
|
-
const touch = e.touches[0];
|
|
40
|
-
if (!touch) return;
|
|
41
|
-
const deltaY = touch.clientY - startYRef.current;
|
|
42
|
-
|
|
43
|
-
if (deltaY <= 0) {
|
|
44
|
-
if (pullStatus !== 'idle') {
|
|
45
|
-
setPullStatus('idle');
|
|
46
|
-
setPullDistance(0);
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
} catch {
|
|
54
|
-
/* passive listener, ignore */
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const distance = deltaY * DAMPING;
|
|
58
|
-
if (!hasMore) {
|
|
59
|
-
setPullStatus('noMore');
|
|
60
|
-
setPullDistance(Math.min(distance, THRESHOLD));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
setPullDistance(distance);
|
|
64
|
-
setPullStatus(distance > THRESHOLD ? 'ready' : 'pulling');
|
|
65
|
-
},
|
|
66
|
-
[isLoadingHistory, pullStatus, hasMore],
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const handleTouchEnd = useCallback(() => {
|
|
70
|
-
if (!isPullingRef.current) return;
|
|
71
|
-
isPullingRef.current = false;
|
|
72
|
-
|
|
73
|
-
if (pullStatus === 'ready' && hasMore) {
|
|
74
|
-
setPullStatus('loading');
|
|
75
|
-
setPullDistance(THRESHOLD);
|
|
76
|
-
onLoad()
|
|
77
|
-
.then(() => {
|
|
78
|
-
setPullStatus('idle');
|
|
79
|
-
setPullDistance(0);
|
|
80
|
-
})
|
|
81
|
-
.catch(() => {
|
|
82
|
-
setPullStatus('idle');
|
|
83
|
-
setPullDistance(0);
|
|
84
|
-
});
|
|
85
|
-
} else if (pullStatus === 'noMore') {
|
|
86
|
-
setTimeout(() => {
|
|
87
|
-
setPullStatus('idle');
|
|
88
|
-
setPullDistance(0);
|
|
89
|
-
}, 1500);
|
|
90
|
-
} else {
|
|
91
|
-
setPullStatus('idle');
|
|
92
|
-
setPullDistance(0);
|
|
93
|
-
}
|
|
94
|
-
}, [pullStatus, hasMore, onLoad]);
|
|
95
|
-
|
|
96
|
-
return { pullStatus, pullDistance, handleTouchStart, handleTouchMove, handleTouchEnd };
|
|
97
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: shopping-guide-demo
|
|
3
|
-
description: 商圈导购:找店、找商品、楼层导航、优惠活动、停车指引
|
|
4
|
-
metadata:
|
|
5
|
-
routingTerms: [找店, 导购, 楼层, 导航, 优惠, 活动, 停车, 商场, 逛]
|
|
6
|
-
---
|
|
7
|
-
# 商圈导购 SOP
|
|
8
|
-
|
|
9
|
-
## 适用意图
|
|
10
|
-
用户询问商场内找店、找商品、楼层分布、优惠活动、停车导航等(如"哪里有运动品牌"、"3 楼有什么吃的"、"最近有什么活动"、"车停哪里")时,遵循本规范回答。非商场相关提问不要套用本规范。
|
|
11
|
-
|
|
12
|
-
## 回答组织方式
|
|
13
|
-
1. 先判断用户意图归属(找店 / 优惠活动 / 停车导航),命中后从对应 reference 查找资料;
|
|
14
|
-
2. 回答按"结论先行"组织:先给出店铺/楼层/活动/车位位置,再补充营业时间、规则等细节;
|
|
15
|
-
3. 信息缺失时如实说明"资料未收录",不要编造楼层或活动信息;
|
|
16
|
-
4. 涉及楼层请使用「1F/2F/B1/B2」格式,停车场使用「B1/B2」格式。
|
|
17
|
-
|
|
18
|
-
## 全局约束
|
|
19
|
-
- 只回答与商场导购相关的内容;用户问及其他领域时正常转回通用助手;
|
|
20
|
-
- 禁止编造未收录的店铺、活动或停车信息;
|
|
21
|
-
- 回答保持简洁,一次回答聚焦一个问题(复合问题先回答优先级最高的意图)。
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# 楼层与店铺分布
|
|
2
|
-
|
|
3
|
-
## 楼层总览
|
|
4
|
-
| 楼层 | 业态 | 代表店铺 |
|
|
5
|
-
|---|---|---|
|
|
6
|
-
| B2 | 停车场 | — |
|
|
7
|
-
| B1 | 餐饮 + 超市 | 烘焙工坊、精品超市、美食广场 |
|
|
8
|
-
| 1F | 美妆 + 珠宝 | 美妆集合店、金饰专柜 |
|
|
9
|
-
| 2F | 服饰 | 运动品牌集合店、女装、男装 |
|
|
10
|
-
| 3F | 亲子 + 家居 | 儿童乐园、母婴、家居生活馆 |
|
|
11
|
-
| 4F | 影院 + 餐饮 | 电影院、火锅、日料 |
|
|
12
|
-
|
|
13
|
-
## 找店指引
|
|
14
|
-
- 问「运动品牌/鞋子」→ 2F 运动品牌集合店;
|
|
15
|
-
- 问「美妆/护肤」→ 1F 美妆集合店;
|
|
16
|
-
- 问「吃饭/餐厅」→ B1 美食广场(快餐)与 4F(正餐);
|
|
17
|
-
- 问「超市」→ B1 精品超市。
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* feature 门控纯函数(011 D10 / 评审 M2 修复):
|
|
3
|
-
* 组件未声明 enabledFeatures = 全启用(MallDeploymentComponent 语义,contracts-mall deployment.ts);
|
|
4
|
-
* 声明了 → 白名单包含才放行。提取为纯函数供单测与装配共用。
|
|
5
|
-
*/
|
|
6
|
-
export interface FeatureGateComponent {
|
|
7
|
-
componentId: string;
|
|
8
|
-
enabledFeatures?: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** 构造门控判定(components 任一命中即 true) */
|
|
12
|
-
export function createFeatureGate(components: readonly FeatureGateComponent[]) {
|
|
13
|
-
return (featureId: string): boolean =>
|
|
14
|
-
components.some(
|
|
15
|
-
(c) => !c.enabledFeatures || c.enabledFeatures.includes(featureId),
|
|
16
|
-
);
|
|
17
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createFeatureGate } from '../src/config/feature-gate.js';
|
|
3
|
-
|
|
4
|
-
describe('createFeatureGate(011 评审 M2:未声明 enabledFeatures = 全启用)', () => {
|
|
5
|
-
it('组件未声明 enabledFeatures → 任意 feature 放行', () => {
|
|
6
|
-
const gate = createFeatureGate([{ componentId: 'shopping-guide' }]);
|
|
7
|
-
expect(gate('merchant-discovery')).toBe(true);
|
|
8
|
-
expect(gate('anything')).toBe(true);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('声明白名单 → 包含才放行', () => {
|
|
12
|
-
const gate = createFeatureGate([
|
|
13
|
-
{ componentId: 'shopping-guide', enabledFeatures: ['merchant-discovery', 'product-search'] },
|
|
14
|
-
]);
|
|
15
|
-
expect(gate('merchant-discovery')).toBe(true);
|
|
16
|
-
expect(gate('product-search')).toBe(true);
|
|
17
|
-
expect(gate('navigation')).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('多组件任一命中即放行;空数组 → 全禁用', () => {
|
|
21
|
-
const gate = createFeatureGate([
|
|
22
|
-
{ componentId: 'member', enabledFeatures: ['member-points'] },
|
|
23
|
-
{ componentId: 'shopping-guide' },
|
|
24
|
-
]);
|
|
25
|
-
expect(gate('member-points')).toBe(true);
|
|
26
|
-
expect(gate('navigation')).toBe(true);
|
|
27
|
-
const none = createFeatureGate([]);
|
|
28
|
-
expect(none('x')).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
});
|