@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.
Files changed (81) hide show
  1. package/.gitignore +6 -0
  2. package/.tbox/app.json +12 -3
  3. package/AGENTS.md +17 -6
  4. package/ENV.md +36 -5
  5. package/README.md +23 -7
  6. package/apps/client/package.json +3 -2
  7. package/apps/client/src/app.tsx +28 -0
  8. package/apps/client/src/cards/CardRenderer.tsx +4 -5
  9. package/apps/client/src/components/ActionBar/DebugButton.tsx +5 -1
  10. package/apps/client/src/components/ActionBar/index.tsx +7 -1
  11. package/apps/client/src/components/ChatInput/index.less +54 -0
  12. package/apps/client/src/components/ChatInput/index.tsx +33 -9
  13. package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +20 -1
  14. package/apps/client/src/components/MessageBubble/index.less +12 -1
  15. package/apps/client/src/components/MessageBubble/index.tsx +16 -8
  16. package/apps/client/src/components/PullDownIndicator/index.less +1 -1
  17. package/apps/client/src/components/PullDownIndicator/index.tsx +3 -4
  18. package/apps/client/src/components/ToolCallBlock/index.less +29 -0
  19. package/apps/client/src/components/ToolCallBlock/index.tsx +58 -24
  20. package/apps/client/src/hooks/useMediaUpload.ts +7 -2
  21. package/apps/client/src/main.tsx +4 -1
  22. package/apps/client/src/pages/index/index.tsx +35 -13
  23. package/apps/client/src/platform/AgentChatApp.tsx +5 -1
  24. package/apps/client/src/store/chat-config.ts +12 -1
  25. package/apps/client/src/store/chat.ts +62 -92
  26. package/apps/client/vite.config.ts +46 -26
  27. package/apps/server/package.json +5 -11
  28. package/apps/server/src/app.ts +452 -0
  29. package/apps/server/src/cli/collect-registry.ts +7 -6
  30. package/apps/server/src/config/deployment.ts +36 -70
  31. package/apps/server/src/config/integration.ts +81 -0
  32. package/apps/server/src/dev.ts +81 -0
  33. package/apps/server/src/index.ts +23 -376
  34. package/apps/server/src/intent-routes.ts +11 -4
  35. package/apps/server/src/modules.ts +3 -0
  36. package/apps/server/tests/cards-registry.test.ts +9 -17
  37. package/apps/server/tests/deployment-loader.test.ts +140 -0
  38. package/apps/server/tests/intent-dispatcher.test.ts +58 -0
  39. package/apps/server/tests/tool-runtime.test.ts +97 -23
  40. package/apps/server/tsup.config.ts +15 -2
  41. package/config/chat.json +4 -1
  42. package/config/llm_provider.json +3 -1
  43. package/package.json +19 -11
  44. package/packages/contracts-mall/package.json +30 -0
  45. package/packages/contracts-mall/src/index.ts +1 -0
  46. package/packages/contracts-mall/src/runtime.ts +1 -0
  47. package/packages/contracts-mall/src/server/index.ts +1 -0
  48. package/packages/contracts-mall/tsconfig.json +7 -0
  49. package/packages/module-weather/package.json +4 -4
  50. package/packages/module-weather/skills/weather-guide/SKILL.md +3 -0
  51. package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
  52. package/packages/module-weather/src/server/index.ts +1 -0
  53. package/packages/module-weather/src/server/tool.ts +24 -24
  54. package/packages/provider-custom/AGENTS.md +33 -0
  55. package/packages/provider-custom/LICENSE +21 -0
  56. package/packages/provider-custom/README.md +8 -0
  57. package/packages/provider-custom/package.json +62 -0
  58. package/packages/provider-custom/src/index.ts +5 -0
  59. package/packages/provider-custom/src/server/index.ts +40 -0
  60. package/packages/provider-custom/tbox.module.json +29 -0
  61. package/packages/provider-custom/tests/provider-custom.test.ts +17 -0
  62. package/packages/provider-custom/tsconfig.json +7 -0
  63. package/packages/provider-custom/tsup.config.ts +18 -0
  64. package/pnpm-lock.template.yaml +6297 -0
  65. package/pnpm-workspace.template.yaml +45 -46
  66. package/scripts/build.sh +6 -12
  67. package/scripts/check-workspace.mjs +11 -10
  68. package/scripts/stop.sh +48 -14
  69. package/scripts/verify-generated.mjs +4 -4
  70. package/tsconfig.base.json +1 -0
  71. package/apps/client/babel-plugins/code-inspect.cjs +0 -110
  72. package/apps/client/src/hooks/usePullDownHistory.ts +0 -97
  73. package/apps/server/skills/shopping-guide-demo/SKILL.md +0 -21
  74. package/apps/server/skills/shopping-guide-demo/references/guide/parking.md +0 -11
  75. package/apps/server/skills/shopping-guide-demo/references/mall-directory.md +0 -17
  76. package/apps/server/skills/shopping-guide-demo/references/promotion.md +0 -10
  77. package/apps/server/src/config/feature-gate.ts +0 -17
  78. package/apps/server/tests/feature-gate.test.ts +0 -30
  79. package/config/deployments/development.json +0 -27
  80. package/scripts/check-inspector.sh +0 -91
  81. /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
- # 同步规则:catalog 与根 pnpm-workspace.yaml 保持并集一致(单向复制,未引用条目无害)。
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: ^5.7.0
16
- tsx: ^4.19.0
17
- zod: ^3.24.0
18
- vite: ^6.4.3
19
- vitest: ^4.1.4
20
- react: ^18.3.1
21
- react-dom: ^18.3.1
22
- "@types/react": ^18.3.12
23
- "@types/react-dom": ^18.3.1
24
- "@types/node": ^24.0.0
25
- express: ^4.21.0
26
- "@types/express": ^4.17.21
27
- ws: ^8.18.0
28
- "@types/ws": ^8.5.0
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": ^0.2.0
38
- "@tbox.cn/app-contracts": ^0.1.0
39
- "@tbox.cn/app-contracts-mall": ^0.2.0
40
- zustand: ^4.5.5
41
- commander: ^12.1.0
42
- diff: ^7.0.0
43
- "@types/diff": ^7.0.0
44
- eslint: ^9.0.0
45
- typescript-eslint: ^8.57.2
46
- eslint-plugin-react-hooks: ^5.2.0
47
- tsup: ^8.0.0
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": ^4.3.3
53
- less: ^4.2.0
54
- "@phosphor-icons/react": ^2.1.10
55
- antd-mobile: ^5.42.3
56
- react-markdown: ^9.1.0
57
- remark-gfm: ^4.0.1
58
- "@types/supertest": ^7.2.0
59
- supertest: ^7.2.2
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
- node "$SCRIPT_DIR/check-workspace.mjs"
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
- echo "Building client (H5)..."
30
- pnpm --filter @app/client build
31
-
26
+ # 构建序列单源 = package.json build / build:client(守卫经 check 组合注入);本脚本只管环境与 install
32
27
  if [ "$SKIP_SERVER" = false ]; then
33
- echo "Building server..."
34
- pnpm --filter @app/server build
28
+ pnpm build
35
29
  else
36
- echo "Skipping server build (--skip-server)"
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.missingComponentDescriptors.length > 0) {
94
- console.error('\npackages/* 缺少 tbox.component.json(contracts / app-sdk / shared 除外):');
95
- for (const file of findings.missingComponentDescriptors) console.error(` - ${file}`);
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.component.json(002 §2.3 / 003 §4.1) */
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.component.json(002 §2.3 / 003 §4.1) */
108
- async function checkComponentDescriptors(projectDir, findings) {
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.component.json'))) {
118
- findings.missingComponentDescriptors.push(`packages/${entry.name}/`);
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
- missingComponentDescriptors: [],
148
+ missingModuleDescriptors: [],
148
149
  };
149
150
 
150
151
  await walk(projectDir, findings);
151
- await checkComponentDescriptors(projectDir, findings);
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
- echo "=== Stopping server on port $PORT ==="
10
- if lsof -i :"$PORT" > /dev/null 2>&1; then
11
- for PID in $(lsof -t -i :"$PORT"); do
12
- echo "Killing process $PID..."
13
- kill -9 "$PID" 2>/dev/null || true
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
- sleep 1
16
- lsof -i :"$PORT" > /dev/null 2>&1 && echo "Warning: Port $PORT still in use" || echo "Port $PORT is now free"
17
- else
18
- echo "Port $PORT is not in use"
19
- fi
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
- pkill -f "tsx watch src/index.ts" 2>/dev/null || true
23
- pkill -f "node.*dist/index.js" 2>/dev/null || true
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.component.json) */
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.component.json'))) {
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.component.json / 无 serverModule)不参与 modules.ts 装配,不计入声明
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.component.json')))
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
  ]);
@@ -12,6 +12,7 @@
12
12
  "isolatedModules": true,
13
13
  "allowImportingTsExtensions": true,
14
14
  "noEmit": true,
15
+ "incremental": true,
15
16
  "useDefineForClassFields": true,
16
17
  "noUnusedLocals": true,
17
18
  "noUnusedParameters": true,
@@ -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,11 +0,0 @@
1
- # 停车导航 SOP
2
-
3
- ## 停车场信息
4
- - 停车场:B2 层(含机械车位),B1 层为临时卸货区(不对外停车);
5
- - 入口:商场东侧入口(导航定位「东门停车场入口」);
6
- - 收费标准:首 2 小时免费,之后每小时 5 元,当日封顶 40 元。
7
-
8
- ## 停车指引
9
- 1. 用户问「停车/车位/车停哪」→ 告知 B2 停车场与东侧入口;
10
- 2. 问「停车优惠」→ 参考优惠活动中的「观影送停车券」;
11
- 3. 问「充电桩」→ 告知 B2 东区设有新能源充电桩(数量有限,先到先得)。
@@ -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,10 +0,0 @@
1
- # 优惠活动
2
-
3
- ## 当前活动(模板示例数据)
4
- - 周末会员日:全场服饰 8 折(2F),每周末;
5
- - 餐饮满 100 减 20:B1 与 4F 指定商户,需会员积分抵扣;
6
- - 观影送停车券:凭 4F 影院当日电影票根,可兑换 B2 停车 2 小时免费。
7
-
8
- ## 活动规则
9
- - 活动以商户现场公示为准,本资料为示例数据;
10
- - 用户问「有什么活动/优惠」→ 按上述条目逐一列出,并给出楼层位置。
@@ -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
- });